Round-2 Codex review of the round-1 fixes: - (A) The Codex plain-stdout→additionalContext fold (F1) was not gated on exit code, so a NON-clean hook's stdout still injected: a SessionStart `echo stale; exit 2` (an emit — cannot block) wrongly injected "stale", and a UserPromptSubmit `exit 1` (non-blocking error → falls through to context) did too. Gate the fold on `output.exitCode === 0`, matching the codec's own structured-stdout rule. Guard tests for both paths, proven red without the gate. - (B) The Codex "SessionStart no-context no-op" absence test was unsound (a completed turn doesn't prove the detached hook finished). It now touches a marker and waitFor()s it before asserting no context. - (B) Both HMR tests used a no-op `true` hook, so a leaked listener would still pass. They now use a BLOCKING (exit 2) UserPromptSubmit hook and assert the post-dispose turn is NOT blocked and logs no hook/invoked — a leaked listener fails loudly.
hooks/ — hook bridges + shared protocol
The hooks subsystem lets users extend the agent at lifecycle points the way Claude Code and Codex do — by pointing a bridge plugin at an existing hooks.json (or settings) so those external shell hooks run faithfully. The canonical extension surface itself is the harness's typed interception seams (the interception-seams RFC); a "native hook" is just an ordinary cordis plugin on those seams. These packages are the bridges that translate the external shell-hook protocol onto that same surface, plus the shared wire-protocol library they build on.
| Package | Role | Shape |
|---|---|---|
hook-protocol/ |
Shared wire-protocol core: matcher primitive, exit-code/stdout codec, runHook (via ctx.bash), most-restrictive merge, hook/* session events |
library (no plugin) |
hooks-claude/ |
Bridge for a Claude Code hooks.json / settings |
plugin |
hooks-codex/ |
Bridge for a Codex hooks.json |
plugin |
Codex deliberately reimplements a subset of the Claude Code protocol (same hooks.json shape, 5 events vs CC's many, command-only, regex-only matcher, no env/substitution), so hook-protocol owns the genuinely-identical primitives and each bridge owns only what differs (its per-event stdin payload, env, and the mapping of a hook's neutral outcome onto the harness's typed Decisions). See hook-protocol/README.md.