Skip to content

Repository files navigation

codex-lcm

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.

Codex plugin

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 4318

Configure 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.

Install

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-local

Trust 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-local

Daily use

Invoke $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.

Privacy and vault location

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.

Uninstall

Remove the plugin before removing its marketplace:

codex plugin remove codex-lcm@codex-lcm-local
codex plugin marketplace remove codex-lcm-local

Remove 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.


What it captures

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 an oversize:// URI placeholder).
  • Deterministic structure summaries — at snapshot time, explorer.py produces 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 UserPromptSubmit hook pre-fetches recent lineage messages and injects them as additionalContext, 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.log and a permissive {"continue": true} is emitted; a no-op stand-in engine is used if the vault can't open.

MCP tools

Eleven lcm_* tools are exposed over a stdio MCP server. All are deterministic — no LLM calls.

Recall

  • 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 explicit fts5_parse_error rather than returning a false "no matches".
  • lcm_recent — the most recent N messages, newest-first. Ideal after /clear to recall what was being discussed. Accepts n as an alias for limit.

Audit / observability

  • 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 with include_subagents=true, matching the message and tool-call audit controls.
  • lcm_tool_calls — structured tool-call audit: each tool_use paired with its tool_result, with parsed args and a truncated result. Pairs by tool_call_id when present, else by same-tool-name call order. group_by="call" (flat, newest-first) or "turn" (grouped under the assistant turn). Defaults to scope="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-effort CLAUDE_PROJECT_DIR → latest-session guess when session_id is omitted (flagged via resolved_via).
  • lcm_describe — metadata for a file snapshot or a session overview. Pass a snapshot_id (int) or a file path (string) as id; omit id for a session overview. For paths, pass session_id to scope to a lineage.

Marks (write tools)

  • lcm_mark — record a named, first-class bookmark / protocol marker (e.g. name="ml-intern:active"). Optionally pass store_id to bookmark and pin a specific message. Prefer this over embedding magic marker strings in the transcript.
  • lcm_marks — list marks, optionally filtered by name.

Vault status

  • 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.

Deferred to v2 (degrades gracefully)

  • lcm_expand — recover original detail behind a summary node. The DAG is empty in v1, so this is a no-op that redirects you to lcm_grep.

Scope model

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

Session identity

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.


Claude Code adapter (legacy-compatible)

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  # remove

The 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.

Tests

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.)


Configuration

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

Architecture

Three layers; claude_lcm/ knows nothing about Claude Code.

claude_lcm/ — host-agnostic engine

  • store.py — SQLite MessageStore, WAL mode, FTS5 virtual table mirrored off messages via triggers. Base schema lifted from hermes-lcm; extensions (sessions, skill_loads, file_snapshots, marks) are additive. New columns arrive via idempotent PRAGMA table_info-guarded migrations on open() — never destructive ALTERs.
  • engine.pyClaudeLcmEngine owns one MessageStore + one SummaryDAG bound to a single session: open_session, ingest_*, grep, close.
  • tools.py / schemas.py — the lcm_* handlers and their JSON schemas.
  • explorer.py — deterministic, stdlib-only file-structure summarizer. Every failure path returns None, so it's always safe to call from a hook.
  • dag.pySummaryDAG for 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/ — the Claude Code bridge

  • 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.

vendor/ — reference copy of upstream

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.


Status & roadmap

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.py stays empty; lcm_expand degrades gracefully).
  • Local LLM — v2.
  • agentfs-backed external snapshots — v3 (the file_snapshots.external_uri column 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.

About

Local Codex lifecycle vault and cross-session recall

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages