LearnNewsExamplesServices
Frontmatter
id13655
titleAlign Codex Stop hook with the no-hold gate
stateClosed
labels
bugaitestingregressionarchitecturemodel-experience
assigneesneo-gpt
createdAtJun 20, 2026, 9:34 PM
updatedAtJun 20, 2026, 10:21 PM
githubUrlhttps://github.com/neomjs/neo/issues/13655
authorneo-gpt
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 20, 2026, 10:21 PM

Align Codex Stop hook with the no-hold gate

Closed v13.1.0/archive-v13-1-0-chunk-4 bugaitestingregressionarchitecturemodel-experience
neo-gpt
neo-gpt commented on Jun 20, 2026, 9:34 PM

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

  1. 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.
  2. Update Claude to consume the shared helper without changing #13651 behavior.
  3. Update Codex to consume the shared helper while preserving Codex fail-open behavior when block/inject is unsupported.
  4. 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.
  5. 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

  • Codex no longer returns allow for a valid lane-state terminal unless operatorInLoop is true.
  • Codex no longer treats stop_hook_active / stopHookActive as an allow; it is classified as not operator-in-loop.
  • Codex supports a live-operator carve where prompting text is available and rejects [WAKE], empty, and forced-continuation prompts as autonomous.
  • Codex remains fail-open while CODEX_STOP_BLOCK_INJECTION_SUPPORTED === false: no stdout blocking decision is emitted in spawned-hook tests, including enforce mode.
  • Claude hook behavior from #13651 remains unchanged after any shared-helper extraction.
  • Focused unit coverage covers the shared decision contract through both Claude and Codex hook surfaces.

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