Most users should start with native imports for supported agents. Use a custom history plugin when a local tool stores useful agent history in a format ctx does not read yet.
A plugin has two parts:
ctx-history-plugin.json, a local manifest that names the history source and command;- a script or binary that reads the tool's native storage and writes
ctx-history-jsonl-v1JSONL to stdout.
ctx discovers the manifest, runs the command during explicit import or search-time refresh, validates the JSONL stream, imports it into the local SQLite index, and stores the cursor emitted by the plugin. On the next run, ctx passes that cursor back to the command.
Manifest
Place the manifest at $CTX_DATA_ROOT/plugins/<plugin>/ctx-history-plugin.json
or point CTX_HISTORY_PLUGIN_PATH at a manifest or directory.
{
"schema_version": 1,
"name": "example-agent",
"display_name": "Example Agent",
"version": "0.1.0",
"history_sources": [
{
"id": "default",
"provider_key": "example-agent",
"source_id": "default",
"source_format": "example-agent-sqlite-v1",
"enabled": true,
"refresh": "auto",
"command": ["example-agent-to-ctx", "export"],
"timeout_seconds": 300
}
]
}The command is an argv array. ctx does not run it through a shell.
Import and search
ctx sources
ctx import --history-source example-agent/default
ctx import --history-source-manifest ./ctx-history-plugin.json
ctx search "release notes" --history-source example-agent/defaultSelectors use plugin/source or provider_key/source_id. Bare names are not
accepted because many integrations use ids such as default.
enabled: true lets ctx import --all run the source. refresh: "auto" lets
ctx search run the source during the normal pre-search refresh. Use
--refresh off when a search should read only the current index.
Command contract
ctx sets runtime environment variables before invoking the command, including:
CTX_HISTORY_SOURCE, such asexample-agent/default;CTX_HISTORY_PROVIDER_KEY;CTX_HISTORY_SOURCE_ID;CTX_HISTORY_SOURCE_FORMAT;CTX_HISTORY_CURSOR_STREAM;CTX_HISTORY_CURSOR_FILE, when a previous cursor exists;CTX_HISTORY_FULL_RESCAN,1or0.
The command must write only ctx-history-jsonl-v1 JSONL records to stdout.
Progress and warnings should go to stderr.
The cursor belongs to the plugin. It can be a byte offset, SQLite row id, JSON map, sync token, or any other string the plugin can use on the next run.
cursor_path = os.environ.get("CTX_HISTORY_CURSOR_FILE")
cursor = read_cursor(cursor_path) if cursor_path else {}
for row in read_new_messages_after(cursor):
emit_session_or_event(row)
emit_source_cursor({"message_id": latest_message_id})For the full manifest and stream schema, run:
ctx docs show history-source-plugins
ctx docs show custom-history-import-formatThe same docs are in the public repository: