A lossless transcript vault for Claude Code and a Codex-native local recall plugin. Claude Code captures every user message, assistant turn, tool call + result, skill load, and file snapshot into a local SQLite database. Codex captures every payload delivered through its supported lifecycle hooks: sessions, prompts, documented assistant messages, local tool calls/results, supported write snapshots, subagent stop messages, and raw hook payloads. Both expose the archive over MCP for cross-session recall.
Runs entirely locally. No cloud, no required API keys. The base schema is
lifted verbatim from hermes-lcm
so vaults stay cross-compatible between agents.
Base hooks deliberately do not parse arbitrary transcripts: their format
is unstable. The optional capture subsystem parses only a caller-authorized,
version-gated official rollout JSONL path. Separately, documented Stop.last_assistant_message and
SubagentStop.last_assistant_message fields preserve assistant responses when
Codex supplies them. Every delivered lifecycle payload is retained in bounded,
recursively redacted form in the vault's additive raw_events table. Hosted tools may bypass local
PreToolUse/PostToolUse hooks entirely, so Codex exposes no supported hook
payload from which this plugin can capture their activity.
Hooks alone do not expose every hosted activity. The optional capture layer
adds an lcm_events audit tool (eleven tools total) and accepts only explicit,
caller-supplied sources: app-server JSON-RPC notifications for controlled
sessions, one authorized rollout JSONL path for passive TUI recovery, and
already-received OTEL JSON records for codex.skill.injected. It never scans
~/.codex, reads Codex databases, intercepts traffic, or treats file changes
as skill loads.
| Mode | Coverage | Guarantee |
|---|---|---|
| Controlled app-server | ordered item start/delta/completion, including native/hosted tools and emitted reasoning when supplied | live, best effort on reconnect |
| Passive TUI rollout | canonical persisted items after the recorder flushes | best effort; buffered, rotated, archived, and ephemeral sessions can have gaps |
| OTEL | telemetry skill injection and correlation metadata | optional/non-authoritative; redacted |
Capture is version-gated to Codex 0.145.x and preserves unsupported records as
raw normalized envelopes instead of guessing. Rollout ingestion requires a
specific absolute .jsonl path, workspace/session metadata match, and an
explicit trusted rollout root; only incomplete trailing records retry on the
next call, while complete malformed or oversized records are quarantined. Set LCM_ROLLOUT_ROOT=/absolute/approved/rollouts and hooks will
sync the host-supplied transcript_path on SessionStart, Stop, SubagentStop,
and SessionEnd. For a controlled run, proxy Codex without changing its stdout:
python3 -m codex_adapter.capture --vault "$LCM_VAULT_PATH" --session-id "$SESSION_ID" --workspace "$PWD" exec -- codex exec --json --ephemeral 'run tests'For Codex's OTLP/HTTP binary logs exporter, run the built-in loopback receiver:
LCM_OTEL_TOKEN=choose-a-local-token python3 -m codex_adapter.capture --vault "$LCM_VAULT_PATH" --session-id "$SESSION_ID" --workspace "$PWD" otel-http --port 4318Configure Codex exactly as documented:
[otel]
exporter = { otlp-http = { endpoint = "http://127.0.0.1:4318/v1/logs", protocol = "binary", headers = { "Authorization" = "Bearer choose-a-local-token" } } }The receiver supports official application/x-protobuf binary OTLP logs (and JSON only for local diagnostics). It binds only 127.0.0.1, requires LCM_OTEL_TOKEN, and
has bounded request/record limits. Payloads are local-only, bounded by the
ingester, and may contain secrets; protect the vault filesystem permissions.
Disable capture by not invoking these opt-in ingestion functions. Removing the
plugin/runtime never removes vault or event data; delete the vault yourself to
discard it.
The bundled hook configuration registers every current Codex lifecycle event:
SessionStart, SessionEnd, UserPromptSubmit, PreToolUse,
PermissionRequest, PostToolUse, PreCompact, PostCompact,
SubagentStart, SubagentStop, and Stop. Events without a portable message
mapping are retained in raw_events.
The repo-local Codex plugin is at plugins/codex-lcm, with its marketplace
entry at .agents/plugins/marketplace.json. Install a durable runtime first;
it lives at ${XDG_DATA_HOME:-$HOME/.local/share}/codex-lcm/venv and neither
requires nor uses a checkout virtualenv at runtime.
python3 -m codex_adapter.install
codex plugin marketplace add "$PWD"
codex plugin add codex-lcm@codex-lcm-localTrust its hooks through /hooks, then start a new Codex thread (or restart
Codex) so the plugin's hooks and MCP server are loaded. The launchers use only
the persistent runtime interpreter. CODEX_LCM_PYTHON may override it for
testing/managed deployments, but must be an absolute executable path. A
relative XDG_DATA_HOME is ignored and safely falls back to $HOME/.local/share.
To upgrade or repair the runtime after updating this checkout, run
python3 -m codex_adapter.install again. To remove only the runtime, run
python3 -m codex_adapter.install --uninstall; this never deletes the vault.
Runtime replacement is serialized across installer processes and retains the
previous runtime until the staged interpreter imports successfully. Uninstall
uses the same lock and refuses symlinked runtime paths.
If Codex reports this plugin as disabled after a metadata update, refresh its
cached bundle with the documented remove/add sequence; current Codex does not
provide a codex plugin enable command:
codex plugin remove codex-lcm@codex-lcm-local
codex plugin add codex-lcm@codex-lcm-localInvoke $codex-lcm when you want explicit recall guidance. lcm_recent,
lcm_grep, and the other lcm_* MCP tools are also available after a new
thread starts. Recall includes documented assistant response text when the host
supplies last_assistant_message; it cannot include unhooked hosted-tool activity.
Hooks and the MCP server both use ~/.local/share/codex-lcm/vault.sqlite by
default, so they share one vault even when Codex does not pass PLUGIN_DATA to
the MCP process. The vault stays
local and may contain prompts, tool inputs/results, and supported file
snapshots. Set LCM_VAULT_PATH explicitly to use a different or intentionally
shared vault; Codex never silently moves or overwrites the Claude default
vault.
Remove the plugin before removing its marketplace:
codex plugin remove codex-lcm@codex-lcm-local
codex plugin marketplace remove codex-lcm-localRemove the vault yourself only if you want to discard its local history; runtime uninstall and vault deletion are intentionally separate operations.
Windows is not supported: the bundled hook command is a
POSIX shell script and the documented activation path uses python3.
Every Claude Code event, via six lifecycle hooks, is appended to the vault:
| Hook | Captured |
|---|---|
SessionStart |
session row + lineage link; injects the session-id context block |
UserPromptSubmit |
user message; detects recall intent and pre-injects recent context |
PreToolUse |
tool call (tool_use) + file snapshot & structure summary |
PostToolUse |
tool result (tool_result) + file snapshot & structure summary |
Stop |
assistant turn text |
SessionEnd |
session close |
Additional capture detail:
- File snapshots — file content read/written by tools is stored inline
(blob), capped at
LCM_MAX_SNAPSHOT_BYTES(oversize falls back to anoversize://URI placeholder). - Deterministic structure summaries — at snapshot time,
explorer.pyproduces a short, stdlib-only summary per file:.py→ defs/classes,.json→ top-level keys + types,.sql→ tables/views, else a text head/tail preview. No LLM involved. - Recall-intent injection — when a prompt matches phrases like "remember",
"catch me up", or "last N messages", the
UserPromptSubmithook pre-fetches recent lineage messages and injects them asadditionalContext, so Claude can answer without spending a tool call. - Crash-safe — hooks never block Claude Code. Any exception is logged to
~/.local/share/claude-lcm/hook.logand a permissive{"continue": true}is emitted; a no-op stand-in engine is used if the vault can't open.
Eleven lcm_* tools are exposed over a stdio MCP server. All are deterministic
— no LLM calls.
lcm_grep— full-history search over raw messages. FTS5 syntax (keywords,"quoted phrases",OR,NOT).match_mode="fts5"(default) or"literal"(escapes punctuation like:[-). On an FTS5 parse error it auto-retries as a quoted literal and surfaces an explicitfts5_parse_errorrather than returning a false "no matches".lcm_recent— the most recent N messages, newest-first. Ideal after/clearto recall what was being discussed. Acceptsnas an alias forlimit.
lcm_events— bounded normalized capture envelopes from opt-in app-server, rollout, or OTEL ingestion; filter by source, kind, and session. Spawned-agent events are hidden by default and included withinclude_subagents=true, matching the message and tool-call audit controls.lcm_tool_calls— structured tool-call audit: eachtool_usepaired with itstool_result, with parsedargsand a truncatedresult. Pairs bytool_call_idwhen present, else by same-tool-name call order.group_by="call"(flat, newest-first) or"turn"(grouped under the assistant turn). Defaults toscope="session".lcm_whoami— the calling session's identity + lineage:session_id,parent_session_id, the full lineage chain,started_at, workspace. Falls back to a best-effortCLAUDE_PROJECT_DIR→ latest-session guess whensession_idis omitted (flagged viaresolved_via).lcm_describe— metadata for a file snapshot or a session overview. Pass asnapshot_id(int) or a file path (string) asid; omitidfor a session overview. For paths, passsession_idto scope to a lineage.
lcm_mark— record a named, first-class bookmark / protocol marker (e.g.name="ml-intern:active"). Optionally passstore_idto bookmark and pin a specific message. Prefer this over embedding magic marker strings in the transcript.lcm_marks— list marks, optionally filtered by name.
lcm_status— quick health overview: session count, message count, vault size on disk, active config.lcm_doctor— diagnostics: database integrity, FTS5 sync, orphaned DAG nodes, config validation.
lcm_expand— recover original detail behind a summary node. The DAG is empty in v1, so this is a no-op that redirects you tolcm_grep.
Recall and audit tools accept a shared scope parameter:
| Scope | Meaning |
|---|---|
lineage |
walk parent_session_id transitively — includes sessions chained by /clear (default for recall tools) |
workspace |
every session in the same project (sanitized cwd) |
session |
the current session_id only — point-in-time audits (default for audit tools) |
auto |
deterministically session if the current session has rows of its own, else lineage |
Claude Code does not pass the session id to MCP servers (only
CLAUDE_PROJECT_DIR). The SessionStart hook injects a context block telling
Claude its session_id and to pass it on every lcm_* call. This is the only
identity channel; lcm_whoami's CLAUDE_PROJECT_DIR fallback is the safety net
when it's missing. Subagents share the parent's session_id but do not
inherit the injected text — pass it into the subagent prompt explicitly.
The original Claude Code adapter remains available and uses the Claude-default vault path. It requires Python 3.11+.
# One-time setup
python3 -m venv .venv && .venv/bin/pip install mcp pytest
# Install the adapter into ~/.claude (hooks + MCP server)
.venv/bin/python -m adapter.install --dry-run # preview
.venv/bin/python -m adapter.install # install
.venv/bin/python -m adapter.install --uninstall # removeThe installer is idempotent: hook commands carry a # claude-lcm sentinel for
re-entrant detection, backups are written with a .clcm.bak suffix, hook
commands are pinned to .venv/bin/python (else sys.executable), and
PYTHONPATH=<repo_root> is prepended so neither the hooks nor the MCP server
need a pip install.
PYTHONPATH= PYTHONNOUSERSITE=1 .venv/bin/python -m pytest tests/(PYTHONPATH= + PYTHONNOUSERSITE=1 disable ROS 2 launch_testing plugins
dragged in via user site-packages on some machines.)
| Var | Purpose | Default |
|---|---|---|
LCM_VAULT_PATH |
SQLite vault file path | Host-specific: Codex ~/.local/share/codex-lcm/vault.sqlite; Claude ~/.local/share/claude-lcm/vault.sqlite |
LCM_MAX_SNAPSHOT_BYTES |
Per-snapshot blob cap; oversize → oversize:// URI |
2 MiB |
LCM_HOOK_LOG |
Hook crash log path | Host-specific: Codex ~/.local/share/codex-lcm/hook.log; Claude ~/.local/share/claude-lcm/hook.log |
LCM_SESSION_ID |
Override session id (hook testing) | unset |
LCM_LOG_LEVEL |
Python log level | WARNING |
Three layers; claude_lcm/ knows nothing about Claude Code.
store.py— SQLiteMessageStore, WAL mode, FTS5 virtual table mirrored offmessagesvia triggers. Base schema lifted from hermes-lcm; extensions (sessions,skill_loads,file_snapshots,marks) are additive. New columns arrive via idempotentPRAGMA table_info-guarded migrations onopen()— never destructive ALTERs.engine.py—ClaudeLcmEngineowns oneMessageStore+ oneSummaryDAGbound to a single session:open_session,ingest_*,grep,close.tools.py/schemas.py— thelcm_*handlers and their JSON schemas.explorer.py— deterministic, stdlib-only file-structure summarizer. Every failure path returnsNone, so it's always safe to call from a hook.dag.py—SummaryDAGfor hierarchical compaction. Empty in v1; the table exists but nothing writes to it (compaction is v2).config.py,workspace.py,session_patterns.py,tokens.py— helpers (config dataclass, git-remote fingerprinting, turn grouping, optional tiktoken token counting).
adapter/hooks/*.py— one short-lived process per hook event.adapter/hooks/_common.py— crash-safe engine context manager +safe_main.adapter/mcp_server.py— stdio MCP server exposing the ten tools.adapter/install.py— idempotent installer.
Raw hermes-lcm files kept for diff reference. Not installed, not imported.
Diff against these before changing any lifted module — preserving the base
schema is what keeps vaults cross-agent compatible.
v1 is lossless transcripts only — message store + FTS5, no compaction, no local LLM. Deliberately out of scope until re-planned:
- Compaction / summary nodes — v2 (
dag.pystays empty;lcm_expanddegrades gracefully). - Local LLM — v2.
- agentfs-backed external snapshots — v3 (the
file_snapshots.external_uricolumn already exists; flipping from inline blob to URI is the schema-compatible path).
The base schema must not change — breaking cross-agent vault compatibility is the one-way door this project is designed to avoid.
The repository keeps the original Claude adapter for existing users while the
published distribution is named codex-lcm. Its import packages remain
claude_lcm, adapter, and codex_adapter for compatibility with existing
vault tooling and hook commands.