Hooks & Platform · how reflect captures and recalls across Claude + Codex
The short version — Two harnesses (Claude Code, Codex CLI) wire the same hook scripts
into different config files (~/.claude/settings.json vs ~/.codex/hooks.json) and share
one on-disk knowledge base (~/.reflect/ queue + ~/.learnings/ documents + GraphRAG
index). SessionStart fires the baseline recall + the bg-drainer; UserPromptSubmit
fires the intent-sharp recall with per-session dedupe; PreCompact, Stop, and
PostToolUse capture learnings into the shared store. A codex session can enqueue a
reflection that a later Claude session drains — and vice versa.
Architecture at a glance
Two harnesses, the same hook scripts, one shared knowledge base. Solid arrows are control
flow; dashed clay arrows are recall (read into context); dashed olive arrows are capture
(write to disk).
Session timeline · one coding session, end to end
A horizontal view of what fires when. Hook events show above the spine; data I/O below.
Storage layers · three tiers, one knowledge base
The pending queue, the learnings store, and the GraphRAG index. Append-only, grep-able,
portable.
Platform · adapter interface and shared knowledge base
Each harness has its own adapter that wires hooks into its own config file. Both adapters
point at the same hook scripts and the same shared knowledge base.
Install paths · Claude Code vs Codex CLI
Claude has a plugin runtime (/plugin install) so installation is two slash commands.
Codex has no plugin runtime, so the adapter does the autowire itself with one python
command.
Terminal window
# Claude Code — managed install via plugin runtime
SessionStart fires before the user has typed anything — its recall query has to be
inferred from cwd, branch, and recent commits. UserPromptSubmit has the actual user prompt
to query against, which gives much sharper hits. Both fire; UserPromptSubmit dedupes against
learnings already injected this session so the same memory doesn’t re-inject on every prompt.
Dedupe state lives at ~/.reflect/session-injected/<session_id>.json — a per-session
set of learning IDs already injected. UserPromptSubmit recall queries the KB, intersects
with the dedupe set, and only injects new hits as additionalContext.
PreCompact handles the high-cost full reflection (claude -p /reflect). Two more hooks
cover gaps:
PostToolUse captures cheap mini-learnings inline — on tool failure, arms a watcher
for the next user prompt; if the prompt looks like a correction ("try X instead"),
write a low-confidence learning directly to disk. No LLM run needed.
Stop catches short sessions that end before PreCompact ever fires. Enqueues the
transcript on agent finish; dedupes against any PreCompact entry for the same session.
Status line · making recall + capture activity visible
Both harnesses give visual feedback, but through different mechanisms.
Claude Code — hooks write ~/.reflect/last-event.json; the user’s
~/.claude/statusline.sh reads it and renders a persistent reflect fragment
(🧠 3 recalled · 1 queued).
Codex CLI — hooks declare a statusMessage field in hooks.json. Codex shows it
ephemerally during hook execution (🧠 recalling...). The static [tui] status_line
config can’t carry a custom token yet, so persistent codex-side counters wait on a
codex API extension.
Worked example · “fix the OAuth redirect bug”
A concrete walkthrough showing every hook firing in order across a morning Claude session
and an afternoon Codex session on the same repo.
09:14 · Claude SessionStart — recall on cwd=auth-service returns L₁ (“OAuth state
handling”). Injected as baseline.
09:18 · UserPromptSubmit — user types “use --insecure for local dev”. Watcher
sees correction pattern, writes mini-learning directly to disk. No LLM run.
11:05 · Stop — agent finishes. stop_reflect.py checks queue, sees PreCompact
already enqueued this session_id → skips.
14:30 · Codex SessionStart — different harness, same repo. reflect-drain-bg.sh
starts in background, finds the morning queue entry, spawns claude -p /reflect
headless. Writes L₄ (“OAuth state mismatch on redirect”).
14:31 · Codex UserPromptSubmit — user types “add OAuth refresh tokens”. Recall pulls
L₂, L₃, L₄ — including the learning Claude just wrote this morning.
The codex session benefits from the morning’s Claude work without anyone moving files
around. The queue and the learnings store are the only handoff.
UserPromptSubmit recall (intent-sharp, with dedupe)
plugins/reflect/hooks/precompact_reflect.py
PreCompact enqueue (full reflection deferred)
plugins/reflect/hooks/stop_reflect.py
Stop enqueue (short-session fallback)
plugins/reflect/hooks/posttooluse_minilearning.py
PostToolUse mini-learning capture
plugins/reflect/hooks/reflect-drain-bg.sh
SessionStart bg-drainer (shells out to claude -p)
plugins/reflect/.claude-plugin/plugin.json
Claude plugin autowire (/plugin install)
plugins/reflect/adapters/codex/codex_adapter.py
Codex installer (writes ~/.codex/hooks.json)
~/.reflect/pending_reflections.jsonl
Shared queue (any harness writes, any drains)
~/.reflect/session-injected/<session_id>.json
Per-session dedupe state
~/.reflect/last-event.json
Status line fragment source
~/.learnings/documents/
Markdown learnings + entity sidecars
~/.learnings/graphrag/
GraphRAG + vector index
FAQ
Why doesn’t SessionStart recall use the user’s first prompt?
SessionStart fires before the user has typed anything. Its query has to be inferred from
cwd, branch, and recent commits — coarse but immediate. UserPromptSubmit fills the
prompt-aware recall slot.
What stops UserPromptSubmit recall from re-injecting the same learning every prompt?
The per-session dedupe set at ~/.reflect/session-injected/<session_id>.json. Each hit
becomes a {learning_id: ts} entry; future prompts skip already-injected learnings
unless they’d be the top hit anyway.
Why does the drainer always shell out to claude instead of codex?
The /reflect skill is a Claude skill. Codex is the trigger (any SessionStart fires the
bg drainer), Claude is the worker. On codex-only machines without claude on PATH, pass
--no-bg-drain to the codex adapter to skip the drain hook.
Does Stop also fire on long sessions that hit PreCompact?
Yes, but stop_reflect.py dedupes against the queue by session_id. PreCompact gets in
first; Stop is a fallback for sessions that never compact.
Where does ~/.reflect/ live, and is it portable?
Under $HOME/.reflect/ by default; overridable via REFLECT_STATE_DIR. Contents are
JSONL/Markdown/YAML — fully grep-able, version-control friendly, and portable across
machines via filesystem sync.
Try it — see the standalone visual posters with the same diagrams: