Observed against codex-cli 0.150.1 (macOS arm64), not only reasoned from the docs. Moved verbatim from the README; every defect found on the way is recorded here in the order it was found.
This repo ships a declarative Codex layer -- .codex-plugin/plugin.json, a self-referential marketplace entry at .agents/plugins/marketplace.json, and hooks/hooks.codex.json, which binds Codex's lifecycle events to the same scripts/*.sh Claude Code already uses (#410). No new hook code was written for it. Install it with:
codex plugin marketplace add Digital-Process-Tools/claude-remember
codex plugin install remember
The Codex-side manifest is a second file, hooks/hooks.codex.json, rather than a shared one -- Codex's default convention would otherwise point at the exact same hooks/hooks.json path Claude Code's manifest already uses by its own default, and the two hosts' hook shapes are close but not identical (Codex hooks support fields, like matcher and timeout, that this repo's Claude-side manifest does not use).
Observed against codex-cli 0.150.1 (macOS arm64), not only reasoned from the docs. Marketplace discovery and codex plugin install remember both work as documented; SessionStart, UserPromptSubmit and SessionEnd all fire; .remember/ is created in the Codex working directory; #407's session-id-over-basename keying and #411's stdin-cwd fallback for session-start-hook.sh/session-end-hook.sh both hold up against a real Codex transcript and a real Codex cwd payload. tests/test_codex_manifest_410.py still only checks the manifests themselves; the live-install claims above are a separate, later observation, not that test suite.
Extraction itself did not work until #443. Codex writes a different transcript envelope than Claude Code -- every line is {"timestamp", "ordinal", "type", "payload"}, with the role and text one level inside payload -- and pipeline/extract.py originally only recognised Claude Code's shape, so a real Codex session with a real human prompt and a real reply extracted 0 exchanges and saved nothing, with every outward signal (hooks firing, .remember/ present) suggesting it had worked. pipeline/host.py now sniffs a transcript's envelope from its own first line and reads either shape; an envelope matching neither is reported as "unrecognised" rather than silently counted as an empty session. A trimmed, sanitised capture of the reproducing session lives at tests/fixtures/codex-rollout.jsonl.
Per-turn capture failed outright before #444 -- scripts/user-prompt-hook.sh and scripts/post-tool-hook.sh had no way to resolve PROJECT_DIR on a host that never sets CLAUDE_PROJECT_DIR, and || exit 0'd silently. Fixed by giving both hooks the same stdin-cwd fallback #411 already gave session-start-hook.sh/session-end-hook.sh.
Codex's UserPromptSubmit stdout contract is not the same as Claude Code's, and until #451/#452 that mismatch marked every prompt on Codex Failed regardless of exit code. Claude Code treats this hook's plain stdout as additionalContext, byte for byte -- the two hosts diverge on the way out, not the way in. Codex's own hook engine sniffs the first non-whitespace byte of a UserPromptSubmit handler's stdout: { or [ means "this claims to be my JSON contract" (codex-rs/hooks/src/engine/output_parser.rs::looks_like_json), and a byte that opens with one of those but fails to parse against Codex's own user-prompt-submit.command.output.schema.json marks the whole run HookRunStatus::Failed -- not because the hook errored, but because Codex tried to read plain text as its own wire format. scripts/user-prompt-hook.sh prints [HH:MM TZ -- user], a stamp that opens with [, so every prompt on Codex read as failed once #444 made the hook print anything on that host at all. The hook branches on whether CLAUDE_PROJECT_DIR is set -- Claude Code always sets it, and until #456 this repo assumed Codex and Gemini CLI never do. That assumption was wrong for Gemini CLI specifically (settled from documentation, not a live session -- Gemini CLI's own bundled docs list CLAUDE_PROJECT_DIR as a compatibility alias it does set, the same name Codex is already known to set). #534 considered narrowing the gate to a Codex-specific signature instead (CODEX_SESSION_ID/CODEX_THREAD_ID, the pair pipeline/host.py's CODEX.signature_vars uses) so the JSON envelope would be Codex-only by construction rather than riding on "CLAUDE_PROJECT_DIR happens to be unset" -- and rejected it, self-reviewed out before merge: #465 already measured, live, that neither variable survives into a process Codex spawns as a HOOK (scripts/user-prompt-hook.sh is registered as exactly that in hooks/hooks.codex.json) -- only into a Codex TOOL-SHELL command, which tests/fixtures/codex-env-463.txt was itself captured from. Gating this script on that pair would have silently disabled the envelope on every real Codex invocation and reopened #451/#452. The gate stays on CLAUDE_PROJECT_DIR, unchanged -- still exactly right for Codex (confirmed absent, live, #463) -- and Gemini, per its own claimed behaviour, now simply takes the same branch Claude Code already does: plain stdout, no JSON envelope. That is REASONED, not OBSERVED, the same limit this section already carried for every host besides Codex 0.150.1 -- Gemini CLI's own BeforeAgent stdout contract has never been observed, live or otherwise, and #532 is why. scripts/session-start-hook.sh was checked against three store shapes (a full store, a repeated-handoff delivery, an empty store) and does not have the same defect -- but the correct claim is about the first byte of stdout, matching Codex's own sniffing behaviour, not "every line": the script has exactly one line that opens with [ ("[already delivered ... ]", printed once the handoff has already been shown), and it is always preceded on the same run by an unconditional echo "=== LAST HANDOFF ===", so the first stdout byte in all three exercised shapes is = or a letter, never {/[ (tests/test_codex_upsubmit_stdout_451.py::test_session_start_first_byte_is_never_bracket_or_brace). OBSERVED for those three shapes, not proven for every branch of a 1400-line script. Gemini CLI's own stdout contract has not been observed at all, live or otherwise.
PostToolUse does fire, and does carry a usable payload -- but the per-tool-call save it triggers was itself silently rejected until #468. post-tool-hook.sh derived the session id it hands save-session.sh from the resolved transcript's own basename, which is the session id verbatim on Claude Code but not on Codex (rollout-<date>-<uuid>.jsonl) -- so every incremental save on Codex forked save-session.sh with an id its own [a-f0-9-]+ validation gate was always going to reject, into an autonomous log nobody reads. SessionEnd was unaffected (it passes the stdin session id directly), which is why a Codex session still looked fully captured end to end: only the incremental saves between tool calls were silently lost, and any end that is not a clean SessionEnd -- a crash, a kill -- loses the whole session. The hook now prefers the session id it was actually invoked with, falling back to the basename only when nothing usable arrived on stdin, the same precedence #407 already gave the transcript path.
Not yet known, rebuilt from what is actually still unknown today (not from what was unknown when this section was first written): whether codex resume behaves correctly once extraction returns a non-zero count; and whether any of this holds on Gemini CLI at all -- everything in this section is OBSERVED against codex-cli 0.150.1 specifically. The REMEMBER_HOOK_CWD fallback no longer needs to be REASONED about for Gemini the way it used to be: Gemini CLI's own bundled docs say it sets CLAUDE_PROJECT_DIR directly (#456), so that fallback is expected to go unused on Gemini rather than be load-bearing for it -- still unverified against a live Gemini install (#532), but no longer resting on a belief ("Gemini never sets it") known to be wrong. The stdout envelope's gate is unchanged (#534 considered, then rejected, narrowing it to Codex's own signature, because #465 already found that signature does not survive into a Codex-spawned HOOK process at all -- see the Codex section above): Gemini setting CLAUDE_PROJECT_DIR just moves it into the same branch Claude Code already takes, plain stdout -- REASONED as the safer default given the old belief was wrong, not OBSERVED against what Gemini's own BeforeAgent contract actually wants. Summarization still shells claude -p regardless of host, so even correct extraction and a correct resume leave the Codex-native execution path from #406 open.