Context
Operator friction on 2026-06-26 exposed a cross-harness Stop-hook usability gap: the no-hold hook currently blocks Codex turns with a reminder that only tells the agent to emit lane-state, even though the shared hook primitive already treats a live operator dialogue as the one valid voluntary allow.
The operator explicitly noted that this works for Vega/Claude and asked whether Codex has an implementation difference. The answer from fresh V-B-A is nuanced:
- Shared primitive:
ai/scripts/lifecycle/stopHookDecision.mjs has isOperatorInLoop() and decideStopHookAction(); operator dialogue is an external allow before lane-state validation.
- Claude adapter:
.claude/hooks/laneStateStopHook.mjs reads prompting text from transcript_path, logs many real ALLOW ... live operator dialogue entries in the local Claude hook log, and its injected reminder already names live operator reply as a stop path.
- Codex adapter:
.codex/hooks/codex-lane-state-stop.mjs supports messages / conversation / transcript / transcript_path, and unit tests prove messages operator prompts allow. But the live Codex hook log for this session repeatedly blocks with source=last_assistant_message and no visible promptSource, including the operator-dialogue proof turn. The live payload likely lacks a recognized prompting-text source, and the hook does not audit enough to diagnose that without re-running a simulation.
Memory-mining note: two query_raw_memories sweeps for operator-dialogue / Stop-hook prior art failed because Memory Core is currently degraded on the embedding write canary timeout. KB and source/test sweeps were used instead.
Live latest-open sweep: checked the latest 30 open issues immediately before filing. Adjacent issues include #13623 (broad no-hold / Stop-hook content), #13751, #13822, and historical closed/parity work, but no narrow open ticket for Codex/Claude operator-dialogue discoverability plus Codex prompt-source audit.
A2A in-flight sweep: checked latest 30 all-status messages immediately before filing; no active peer claim for this Stop-hook friction.
The Problem
The hook contract is correct in principle but opaque in practice:
- Agents do not see their turn-end options. The injected Codex reminder currently says to emit
lane-state, but does not explain the valid split: autonomous wake / forced continuation needs lane-state + concrete lane movement; live operator dialogue / planning with the operator is a legitimate turn-taking allow when externally confirmable.
- Codex false-positive diagnosis is under-instrumented. When operator dialogue fails closed because prompting text is missing or unrecognized, the audit log omits
promptSource and operatorInLoop, so the next agent cannot tell whether the failure is bad copy, missing payload field, or a real autonomous turn.
- Claude/Codex parity is partly implicit. The shared primitive is common, but the two adapters have different injected prose and payload extraction paths. A future peer should not have to rediscover the distinction under a Stop-hook interruption.
- The fix must stay small. Hook reminders are turn-injected substrate. They must be concise and avoid AGENTS-style bloat; the operator explicitly called out the 24KB cap / turn-memory cost constraint.
The Architectural Reality
Relevant surfaces:
ai/scripts/lifecycle/stopHookDecision.mjs owns the shared decision semantics and schema hint.
.claude/hooks/laneStateStopHook.mjs owns Claude payload extraction, no-hold reminder prose, and block injection.
.codex/hooks/codex-lane-state-stop.mjs owns Codex payload extraction, no-hold reminder prose, audit logging, and block injection.
test/playwright/unit/hooks/laneStateStopHook.spec.mjs, codexLaneStateStopHook.spec.mjs, and stopHookDecision.spec.mjs cover the operator-dialogue carve and adapter behavior.
Key source observations:
isOperatorInLoop({stopHookActive, promptingText}) returns true only when the prompt is confirmable, not a [WAKE], and not a forced continuation.
- Claude reads the last user prompt from
transcript_path; Codex only uses messages / conversation / transcript / transcript_path for prompting text.
- The live Codex audit log only prints
source=..., not promptSource / operatorInLoop, hiding the failing branch.
The Fix
Make the hook behavior discoverable and diagnosable without reopening the no-hold premise:
- Add a short shared hook-options hint that says, in substance: autonomous wake/forced continuation must continue a lane or emit valid
lane-state; live operator dialogue/planning can stop because the human owns the next turn; if the hook cannot confirm the operator prompt, it fails closed.
- Keep the hint short enough for turn-injected use. Add a focused size/budget assertion so it cannot silently bloat toward the 24KB substrate cap.
- Update Codex audit logging to include
promptSource and operatorInLoop on ALLOW/BLOCK/WOULD-BLOCK lines.
- If feasible from the live Codex payload shape, extend Codex prompting-text extraction to the actual field that exists in production. If not, add capture/audit guidance so the next live run can identify the missing field without dumping private content.
- Bring Claude and Codex reminder prose into semantic parity: both should name the operator-dialogue/planning allow and the autonomous lane-state requirement.
- Add focused unit coverage for the concise hint, the audit fields, and the missing-prompt fail-closed diagnosis.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
| Shared Stop-hook decision copy |
stopHookDecision.mjs + operator friction |
Agents see the two valid turn-end paths: autonomous lane movement vs externally-confirmed operator dialogue |
If prompt cannot be confirmed, fail closed to autonomous lane-state requirement |
JSDoc/comment near shared hint |
Unit assertion for hint content + size |
| Codex hook audit log |
.codex/hooks/codex-lane-state-stop.mjs |
ALLOW/BLOCK/WOULD-BLOCK lines include source, promptSource, and operatorInLoop |
Redacted payload-shape capture remains optional for unknown fields |
Hook JSDoc |
Spawned-hook unit verifies log line fields |
| Codex prompting extraction |
Actual Codex Stop payload + adapter tests |
Recognize the live prompting-text field where available |
If unavailable, log promptSource=none and fail closed |
Test fixture / payload-shape note |
Unit fixture for representative payload |
| Claude hook reminder parity |
.claude/hooks/laneStateStopHook.mjs + shared primitive |
Reminder explicitly says live operator dialogue/planning is a valid turn-taking allow |
Keep current behavior if already equivalent; adjust prose only |
Existing hook header/comment |
Focused unit check on reminder content |
Decision Record impact
Aligned with ADR 0007 and the no-hold substrate from #13618 / #13623. This does not relax autonomous no-hold enforcement; it clarifies the existing operator-dialogue carve and keeps injected copy small.
Acceptance Criteria
Out of Scope
- Reopening the no-hold-state premise.
- Making
lane-state itself a stop license for autonomous turns.
- Adding a new broad skill or AGENTS.md section.
- Dumping raw Codex payload content into logs.
- Changing Memory Core / A2A behavior.
Avoided Traps
- Do not create a new valid-stop list that autonomous agents can game. The operator-dialogue allow remains externally detected, not self-declared.
- Do not bloat the runtime reminder; this is turn-injected recovery copy, not a new manual.
- Do not fork Claude and Codex semantics; keep shared decision logic authoritative.
- Do not hide the failure again behind a generic
no lane-state block emitted line when the real issue is missing promptSource.
Related
Related: #13623
Related: #13655
Related: #13661
Related: #13731
Related: #13796
Related: #13822
Origin Session ID: 35f83031-f1a6-41a7-9c3b-089b87307db9
Handoff Retrieval Hints
query_raw_memories("operator dialogue stop hook Codex promptSource lane-state no-hold")
query_raw_memories("Claude Codex Stop hook operatorInLoop promptSource live operator dialogue")
- Exact anchors:
ai/scripts/lifecycle/stopHookDecision.mjs, .codex/hooks/codex-lane-state-stop.mjs, .claude/hooks/laneStateStopHook.mjs, test/playwright/unit/hooks/codexLaneStateStopHook.spec.mjs, test/playwright/unit/hooks/laneStateStopHook.spec.mjs.
Context
Operator friction on 2026-06-26 exposed a cross-harness Stop-hook usability gap: the no-hold hook currently blocks Codex turns with a reminder that only tells the agent to emit
lane-state, even though the shared hook primitive already treats a live operator dialogue as the one valid voluntary allow.The operator explicitly noted that this works for Vega/Claude and asked whether Codex has an implementation difference. The answer from fresh V-B-A is nuanced:
ai/scripts/lifecycle/stopHookDecision.mjshasisOperatorInLoop()anddecideStopHookAction(); operator dialogue is an external allow before lane-state validation..claude/hooks/laneStateStopHook.mjsreads prompting text fromtranscript_path, logs many realALLOW ... live operator dialogueentries in the local Claude hook log, and its injected reminder already nameslive operator replyas a stop path..codex/hooks/codex-lane-state-stop.mjssupportsmessages/conversation/transcript/transcript_path, and unit tests provemessagesoperator prompts allow. But the live Codex hook log for this session repeatedly blocks withsource=last_assistant_messageand no visiblepromptSource, including the operator-dialogue proof turn. The live payload likely lacks a recognized prompting-text source, and the hook does not audit enough to diagnose that without re-running a simulation.Memory-mining note: two
query_raw_memoriessweeps for operator-dialogue / Stop-hook prior art failed because Memory Core is currently degraded on the embedding write canary timeout. KB and source/test sweeps were used instead.Live latest-open sweep: checked the latest 30 open issues immediately before filing. Adjacent issues include
#13623(broad no-hold / Stop-hook content),#13751,#13822, and historical closed/parity work, but no narrow open ticket for Codex/Claude operator-dialogue discoverability plus Codex prompt-source audit.A2A in-flight sweep: checked latest 30 all-status messages immediately before filing; no active peer claim for this Stop-hook friction.
The Problem
The hook contract is correct in principle but opaque in practice:
lane-state, but does not explain the valid split: autonomous wake / forced continuation needs lane-state + concrete lane movement; live operator dialogue / planning with the operator is a legitimate turn-taking allow when externally confirmable.promptSourceandoperatorInLoop, so the next agent cannot tell whether the failure is bad copy, missing payload field, or a real autonomous turn.The Architectural Reality
Relevant surfaces:
ai/scripts/lifecycle/stopHookDecision.mjsowns the shared decision semantics and schema hint..claude/hooks/laneStateStopHook.mjsowns Claude payload extraction, no-hold reminder prose, and block injection..codex/hooks/codex-lane-state-stop.mjsowns Codex payload extraction, no-hold reminder prose, audit logging, and block injection.test/playwright/unit/hooks/laneStateStopHook.spec.mjs,codexLaneStateStopHook.spec.mjs, andstopHookDecision.spec.mjscover the operator-dialogue carve and adapter behavior.Key source observations:
isOperatorInLoop({stopHookActive, promptingText})returns true only when the prompt is confirmable, not a[WAKE], and not a forced continuation.transcript_path; Codex only usesmessages/conversation/transcript/transcript_pathfor prompting text.source=..., notpromptSource/operatorInLoop, hiding the failing branch.The Fix
Make the hook behavior discoverable and diagnosable without reopening the no-hold premise:
lane-state; live operator dialogue/planning can stop because the human owns the next turn; if the hook cannot confirm the operator prompt, it fails closed.promptSourceandoperatorInLoopon ALLOW/BLOCK/WOULD-BLOCK lines.Contract Ledger Matrix
stopHookDecision.mjs+ operator friction.codex/hooks/codex-lane-state-stop.mjssource,promptSource, andoperatorInLooppromptSource=noneand fail closed.claude/hooks/laneStateStopHook.mjs+ shared primitiveDecision Record impact
Aligned with ADR 0007 and the no-hold substrate from
#13618/#13623. This does not relax autonomous no-hold enforcement; it clarifies the existing operator-dialogue carve and keeps injected copy small.Acceptance Criteria
source,promptSource, andoperatorInLoop.[WAKE]andstop_hook_activedo not qualify as operator dialogue.Out of Scope
lane-stateitself a stop license for autonomous turns.Avoided Traps
no lane-state block emittedline when the real issue is missingpromptSource.Related
Related: #13623 Related: #13655 Related: #13661 Related: #13731 Related: #13796 Related: #13822
Origin Session ID: 35f83031-f1a6-41a7-9c3b-089b87307db9
Handoff Retrieval Hints
query_raw_memories("operator dialogue stop hook Codex promptSource lane-state no-hold")query_raw_memories("Claude Codex Stop hook operatorInLoop promptSource live operator dialogue")ai/scripts/lifecycle/stopHookDecision.mjs,.codex/hooks/codex-lane-state-stop.mjs,.claude/hooks/laneStateStopHook.mjs,test/playwright/unit/hooks/codexLaneStateStopHook.spec.mjs,test/playwright/unit/hooks/laneStateStopHook.spec.mjs.