Context
Vega surfaced a same-day parity gap after #13651 merged: Claude's Stop hook no longer treats a valid lane-state block as permission to end the turn, but the Codex Stop hook still does. This matters because #13622/#13625 added the Codex Stop-hook probe as the cross-harness no-hold parity surface; leaving Codex on the old semantics makes GPT the weak harness for the exact announce-without-execute loophole #13649 removed from Claude.
Release classification: post-release / boardless harness hardening. This is urgent Agent OS stability work, but not a v13 release gate.
Live latest-open sweep: checked latest 20 open issues at 2026-06-20T19:33:02Z; no equivalent Codex Stop-hook terminal-loophole ticket found. Exact live searches for Codex Stop hook valid terminal operatorInLoop, decideCodexHookAction, and codex lane-state stop hook found no open equivalent. A2A in-flight claim sweep: checked latest 30 all-state messages immediately before creation; Vega's direct [codex-parity] handoff is the only overlapping signal and asks GPT to decide/drive this scope. Memory Core sweep for Codex hook parity decideCodexHookAction valid terminal loophole operatorInLoop shared module and related framings returned no prior-session hit. Knowledge Base search for the same scope returned no specific indexed answer.
The Problem
Current origin/dev has split semantics across harnesses:
- Claude:
.claude/hooks/laneStateStopHook.mjs now makes operatorInLoop the only voluntary allow. Lines 117-134 state and implement that a valid lane-state terminal is a declaration, not a stop license.
- Codex:
.codex/hooks/codex-lane-state-stop.mjs still returns allow for verdict.valid at lines 223-228, and classifyCodexStopPayload() still treats stop_hook_active / stopHookActive as an allow at lines 263-267.
- Codex tests encode the stale behavior:
test/playwright/unit/hooks/codexLaneStateStopHook.spec.mjs lines 91-98 expect a valid representative payload to allow, and lines 121-129 expect the loop guard to allow.
So Codex still has both loopholes #13651 removed for Claude: valid terminal as stop-license and forced-continuation loop guard as stop-license. It also lacks a Codex-side operatorInLoop carve, so the one legitimate voluntary stop (live human turn-taking) is not modeled.
Architectural Reality
Relevant surfaces:
ai/scripts/lifecycle/parseLaneState.mjs and validateLaneStateTerminal.mjs are already the shared pure lane-state lifecycle seam used by both hooks.
.claude/hooks/laneStateStopHook.mjs owns Claude-specific Stop payload extraction and blocking stdout behavior.
.codex/hooks/codex-lane-state-stop.mjs owns Codex-specific Stop payload extraction, redacted payload shape capture, and fail-open logging while CODEX_STOP_BLOCK_INJECTION_SUPPORTED remains false.
- Codex cannot claim enforcement parity until block/inject semantics are proven; this ticket is about decision parity in the audit/fail-open layer, not about emitting a Codex blocking decision.
Pre-Flight (structural fast-path): authoring ai/scripts/lifecycle/stopHookDecision.mjs matches the sibling pattern of ai/scripts/lifecycle/parseLaneState.mjs and ai/scripts/lifecycle/validateLaneStateTerminal.mjs in ai/scripts/lifecycle/; all are pure lane-state lifecycle helpers consumed by harness hooks; sibling-file-lift applies; no novel directory choice. Map-maintenance: not needed; the existing lifecycle helper directory role is already represented by sibling files.
The Fix
- Extract the shared no-hold Stop-hook decision primitives into a pure lifecycle helper so Claude and Codex cannot drift again:
parseOutcomeToVerdict for the 3-bucket parse result mapping;
isOperatorInLoop for the external human-vs-wake/forced-continuation check;
- a decision helper that makes
operatorInLoop the only allow and otherwise returns block/would-block according to enforcement and block-injection support.
- Update Claude to consume the shared helper without changing #13651 behavior.
- Update Codex to consume the shared helper while preserving Codex fail-open behavior when block/inject is unsupported.
- Add Codex input-resolution support for the prompting user text where available, so a live operator dialogue can allow but
[WAKE], empty prompt, and stop-hook-active cannot.
- Update focused unit coverage for both hooks: valid terminal + no operator is would-block/block-equivalent, stop-hook-active is not an allow, live operator prompt is the only allow, and Codex still writes no block decision to stdout while block/inject support is false.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| Shared Stop-hook decision helper |
#13649 / #13651 semantics and current Claude hook JSDoc |
One pure helper defines parse verdict mapping, operator-in-loop gating, and no-hold decision semantics for harness Stop hooks. |
If a harness lacks prompting text, fail closed to autonomous for the voluntary-stop decision. |
Helper JSDoc names the no-hold gate and harness-specific block support boundary. |
Focused unit coverage imports helper through Claude and Codex hooks. |
Codex decideCodexHookAction |
.codex/hooks/codex-lane-state-stop.mjs + #13622 fail-open contract |
A valid terminal without live operator dialogue no longer allows; it would-blocks/logs the no-hold reminder. |
Enforce mode still fail-opens with a block/inject-unproven suffix while support remains false. |
Codex hook JSDoc and tests document audit-only semantics. |
codexLaneStateStopHook.spec.mjs covers valid/no-operator, operator, stop-hook-active, invalid, and spawned hook cases. |
Claude decideHookAction |
#13649/#13651 merged behavior |
Behavior remains unchanged while using the shared helper. |
Hook failures still allow/audit as before. |
Existing Claude hook JSDoc stays source-of-authority. |
laneStateStopHook.spec.mjs remains green. |
Decision Record impact
None. This aligns existing no-hold hook semantics across harness adapters; it does not amend ADR 0002 or the wake-substrate architecture.
Acceptance Criteria
Out of Scope
- Proving or enabling Codex block/inject semantics.
- Changing the Codex wake-submit route or nonce causality classifier (#13480/#13636 lineage).
- Changing the canonical no-hold reminder content from #13623.
- Adding COP ratio observability or L-collab tripwire behavior.
- Editing ignored local
.codex/config.toml.
Avoided Traps
- Do not recreate the
verdict.valid -> allow loophole under a Codex-specific name.
- Do not treat
stop_hook_active as a human turn; it is a forced continuation signal.
- Do not claim enforcement parity for Codex while block/inject remains unproven.
- Do not duplicate the decision semantics in two hook files if a shared pure helper can remove the drift source.
Related
Parent / related: #13618, #13623
Related: #13622, #13625, #13649, #13651, #13480, #13636
Origin Session ID: 019ee5c2-82ba-7b73-8812-df59106ff61a
Handoff Retrieval Hints: Codex Stop hook valid terminal loophole operatorInLoop decideCodexHookAction stop_hook_active #13651 shared stopHookDecision
Context
Vega surfaced a same-day parity gap after #13651 merged: Claude's Stop hook no longer treats a valid
lane-stateblock as permission to end the turn, but the Codex Stop hook still does. This matters because #13622/#13625 added the Codex Stop-hook probe as the cross-harness no-hold parity surface; leaving Codex on the old semantics makes GPT the weak harness for the exact announce-without-execute loophole #13649 removed from Claude.Release classification: post-release / boardless harness hardening. This is urgent Agent OS stability work, but not a v13 release gate.
Live latest-open sweep: checked latest 20 open issues at 2026-06-20T19:33:02Z; no equivalent Codex Stop-hook terminal-loophole ticket found. Exact live searches for
Codex Stop hook valid terminal operatorInLoop,decideCodexHookAction, andcodex lane-state stop hookfound no open equivalent. A2A in-flight claim sweep: checked latest 30 all-state messages immediately before creation; Vega's direct[codex-parity]handoff is the only overlapping signal and asks GPT to decide/drive this scope. Memory Core sweep forCodex hook parity decideCodexHookAction valid terminal loophole operatorInLoop shared moduleand related framings returned no prior-session hit. Knowledge Base search for the same scope returned no specific indexed answer.The Problem
Current
origin/devhas split semantics across harnesses:.claude/hooks/laneStateStopHook.mjsnow makesoperatorInLoopthe only voluntary allow. Lines 117-134 state and implement that a valid lane-state terminal is a declaration, not a stop license..codex/hooks/codex-lane-state-stop.mjsstill returnsallowforverdict.validat lines 223-228, andclassifyCodexStopPayload()still treatsstop_hook_active/stopHookActiveas an allow at lines 263-267.test/playwright/unit/hooks/codexLaneStateStopHook.spec.mjslines 91-98 expect a valid representative payload to allow, and lines 121-129 expect the loop guard to allow.So Codex still has both loopholes #13651 removed for Claude: valid terminal as stop-license and forced-continuation loop guard as stop-license. It also lacks a Codex-side
operatorInLoopcarve, so the one legitimate voluntary stop (live human turn-taking) is not modeled.Architectural Reality
Relevant surfaces:
ai/scripts/lifecycle/parseLaneState.mjsandvalidateLaneStateTerminal.mjsare already the shared pure lane-state lifecycle seam used by both hooks..claude/hooks/laneStateStopHook.mjsowns Claude-specific Stop payload extraction and blocking stdout behavior..codex/hooks/codex-lane-state-stop.mjsowns Codex-specific Stop payload extraction, redacted payload shape capture, and fail-open logging whileCODEX_STOP_BLOCK_INJECTION_SUPPORTEDremains false.Pre-Flight (structural fast-path): authoring
ai/scripts/lifecycle/stopHookDecision.mjsmatches the sibling pattern ofai/scripts/lifecycle/parseLaneState.mjsandai/scripts/lifecycle/validateLaneStateTerminal.mjsinai/scripts/lifecycle/; all are pure lane-state lifecycle helpers consumed by harness hooks; sibling-file-lift applies; no novel directory choice. Map-maintenance: not needed; the existing lifecycle helper directory role is already represented by sibling files.The Fix
parseOutcomeToVerdictfor the 3-bucket parse result mapping;isOperatorInLoopfor the external human-vs-wake/forced-continuation check;operatorInLoopthe only allow and otherwise returns block/would-block according to enforcement and block-injection support.[WAKE], empty prompt, and stop-hook-active cannot.Contract Ledger Matrix
decideCodexHookAction.codex/hooks/codex-lane-state-stop.mjs+ #13622 fail-open contractcodexLaneStateStopHook.spec.mjscovers valid/no-operator, operator, stop-hook-active, invalid, and spawned hook cases.decideHookActionlaneStateStopHook.spec.mjsremains green.Decision Record impact
None. This aligns existing no-hold hook semantics across harness adapters; it does not amend ADR 0002 or the wake-substrate architecture.
Acceptance Criteria
allowfor a valid lane-state terminal unlessoperatorInLoopis true.stop_hook_active/stopHookActiveas an allow; it is classified as not operator-in-loop.[WAKE], empty, and forced-continuation prompts as autonomous.CODEX_STOP_BLOCK_INJECTION_SUPPORTED === false: no stdout blocking decision is emitted in spawned-hook tests, including enforce mode.Out of Scope
.codex/config.toml.Avoided Traps
verdict.valid -> allowloophole under a Codex-specific name.stop_hook_activeas a human turn; it is a forced continuation signal.Related
Parent / related: #13618, #13623
Related: #13622, #13625, #13649, #13651, #13480, #13636
Origin Session ID: 019ee5c2-82ba-7b73-8812-df59106ff61a
Handoff Retrieval Hints:
Codex Stop hook valid terminal loophole operatorInLoop decideCodexHookAction stop_hook_active #13651 shared stopHookDecision