Summary
A one-shot claude -p run inside a firstmate primary checkout wedges into repeated forced continuations whenever supervision is needed and the watcher beacon is stale.
--claude mode of bin/fm-turnend-guard.sh intentionally ignores stop_hook_active (the 2026-07-21 incident). That is correct for a durable interactive session. It is wrong for a one-shot headless process: there is no next turn that can repair the watcher, so the guard keeps returning exit 2 until Claude's consecutive-block override fires.
bin/fm-claude-stop-autoarm.sh is registered on the same Stop event with "asyncRewake": true and "timeout": 28800. On a one-shot process that timeout can hold the CLI open for hours even after the 8-block cap. The hook's own header already calls this "the exact wedge grok 1.0.0 produced".
This is a second-order effect of #2485: the 8h auto-arm timeout SIGTERMs the process group, the watcher dies silently, and the next claude -p in the primary home is the thing that cannot be diagnosed. Related family: #2940 (Claude Stop contract edge not covered). Shape of the ask matches #1016 (a primary-home restriction that is too blunt and should be narrowed).
I have a local workaround and am not blocked. Happy to send a PR in whatever shape you prefer.
Why this is a Claude-only hole
docs/turnend-guard.md ## Compatibility limits documents headless behavior for every other harness:
- OpenCode remains fail-open in headless
opencode run
- Cursor's
stop step does not fire in headless cursor-agent -p
- Grok: do not run the primary as a one-shot headless process
- Kimi is out of scope entirely
Claude is the only harness whose Stop hook does fire in headless mode and can block the turn end. Everywhere else the missing case degrades to a no-op; here it degrades to a wedge.
--claude mode dropped the loop-guard to stop asyncRewake auto-arm from reopening the 2026-07-21 blind window. That accident requires a durable session. A one-shot run has no later turn to protect.
Upstream verification already uses claude -p inside a primary / scratch primary (docs/verification/supervision.md, docs/subagent-guard.md). "Just don't run headless in the primary home" is not the project's own practice.
## Regression coverage lists ~20 fm-turnend-guard cases and none for headless.
Measured, firstmate-independent repro (2026-08-24, Claude Code 2.1.233)
Temp dir, no firstmate files, one Stop hook that always exits 2:
{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"echo BLOCKED >&2; exit 2"}]}]}}
claude -p 'reply with exactly: ok' < /dev/null
No timeout wrapper.
Result:
- elapsed 54s, process exited 0 (not an infinite hang)
- Stop hook fired 10 times (1×
stop_hook_active=false, then 9× true)
- stdout empty — the model answer is consumed by the forced-continuation loop
- so the official 8-consecutive-block cap does eventually end the turn, but a one-shot probe still burns a burst of model turns and can look wedged inside a 60s observer window
The original in-home incident (timeout 60) was killed before this cap. The "process never exits" observation is therefore not explained by the guard (capped) and points at the 28800s auto-arm hook holding the one-shot process.
Ancestry is a usable headless signal
A Stop hook under claude -p walked to claude -p reply with exactly: ok at depth 3 (HIT_PRINT_FLAG). Env CLAUDE_CODE_ENTRYPOINT=sdk-cli. This particular headless path did not stop at claude daemon run (#2314). #1641's warning against leaked env flags still applies; process structure is the preferred signal.
Proposed shape (please confirm or name another)
Treat a headless Claude session as default-mode semantics:
- New predicate
fm_harness_session_is_headless on bin/fm-session-lock-lib.sh (ancestry ps -o args= contains -p / --print; undetectable ancestry returns 1 so current behavior is preserved).
fm-turnend-guard.sh --claude: if headless, honor stop_hook_active (banner once, then allow). Skip the auto-arm cooperative wait / epoch ledger on that path.
fm-claude-stop-autoarm.sh: stand down immediately on headless (exit 0). A watcher armed in the foreground of a one-shot process dies when claude -p exits, and the 28800s timeout can hold that process open.
I can implement that on a fork with tests + docs/turnend-guard.md / docs/watcher-continuity.md updates. If you would rather both Stop hooks silently exit 0 in headless, or docs-only for the guard, say so — I will follow that.
Local workaround (already in use)
claude -p --settings '{"disableAllHooks": true}' "..." < /dev/null
--settings clears the hook registry for that invocation only. Alternatives: cd out of the primary home, or claude --bare -p.
Artifacts from the original in-home incident
- session
86b5d8f4-3bde-4ac5-a659-8fcad30f5a5f.jsonl lines 14–15: model printed ok, then TURN WOULD END BLIND (last beat: 14652s ago)
- debug
fa487699-d094-45dc-9c35-b22a1f0da1d6.txt: two [API REQUEST]s, first byte 247ms, then Hook Stop (Stop) error:
Happy to attach redacted excerpts if useful.
Summary
A one-shot
claude -prun inside a firstmate primary checkout wedges into repeated forced continuations whenever supervision is needed and the watcher beacon is stale.--claudemode ofbin/fm-turnend-guard.shintentionally ignoresstop_hook_active(the 2026-07-21 incident). That is correct for a durable interactive session. It is wrong for a one-shot headless process: there is no next turn that can repair the watcher, so the guard keeps returning exit 2 until Claude's consecutive-block override fires.bin/fm-claude-stop-autoarm.shis registered on the same Stop event with"asyncRewake": trueand"timeout": 28800. On a one-shot process that timeout can hold the CLI open for hours even after the 8-block cap. The hook's own header already calls this "the exact wedge grok 1.0.0 produced".This is a second-order effect of #2485: the 8h auto-arm timeout SIGTERMs the process group, the watcher dies silently, and the next
claude -pin the primary home is the thing that cannot be diagnosed. Related family: #2940 (Claude Stop contract edge not covered). Shape of the ask matches #1016 (a primary-home restriction that is too blunt and should be narrowed).I have a local workaround and am not blocked. Happy to send a PR in whatever shape you prefer.
Why this is a Claude-only hole
docs/turnend-guard.md## Compatibility limitsdocuments headless behavior for every other harness:opencode runstopstep does not fire in headlesscursor-agent -pClaude is the only harness whose Stop hook does fire in headless mode and can block the turn end. Everywhere else the missing case degrades to a no-op; here it degrades to a wedge.
--claudemode dropped the loop-guard to stop asyncRewake auto-arm from reopening the 2026-07-21 blind window. That accident requires a durable session. A one-shot run has no later turn to protect.Upstream verification already uses
claude -pinside a primary / scratch primary (docs/verification/supervision.md,docs/subagent-guard.md). "Just don't run headless in the primary home" is not the project's own practice.## Regression coveragelists ~20fm-turnend-guardcases and none for headless.Measured, firstmate-independent repro (2026-08-24, Claude Code 2.1.233)
Temp dir, no firstmate files, one Stop hook that always exits 2:
{"hooks":{"Stop":[{"hooks":[{"type":"command","command":"echo BLOCKED >&2; exit 2"}]}]}}No
timeoutwrapper.Result:
stop_hook_active=false, then 9×true)The original in-home incident (
timeout 60) was killed before this cap. The "process never exits" observation is therefore not explained by the guard (capped) and points at the 28800s auto-arm hook holding the one-shot process.Ancestry is a usable headless signal
A Stop hook under
claude -pwalked toclaude -p reply with exactly: okat depth 3 (HIT_PRINT_FLAG). EnvCLAUDE_CODE_ENTRYPOINT=sdk-cli. This particular headless path did not stop atclaude daemon run(#2314).#1641's warning against leaked env flags still applies; process structure is the preferred signal.Proposed shape (please confirm or name another)
Treat a headless Claude session as default-mode semantics:
fm_harness_session_is_headlessonbin/fm-session-lock-lib.sh(ancestryps -o args=contains-p/--print; undetectable ancestry returns 1 so current behavior is preserved).fm-turnend-guard.sh --claude: if headless, honorstop_hook_active(banner once, then allow). Skip the auto-arm cooperative wait / epoch ledger on that path.fm-claude-stop-autoarm.sh: stand down immediately on headless (exit 0). A watcher armed in the foreground of a one-shot process dies whenclaude -pexits, and the 28800s timeout can hold that process open.I can implement that on a fork with tests +
docs/turnend-guard.md/docs/watcher-continuity.mdupdates. If you would rather both Stop hooks silentlyexit 0in headless, or docs-only for the guard, say so — I will follow that.Local workaround (already in use)
--settingsclears the hook registry for that invocation only. Alternatives:cdout of the primary home, orclaude --bare -p.Artifacts from the original in-home incident
86b5d8f4-3bde-4ac5-a659-8fcad30f5a5f.jsonllines 14–15: model printedok, thenTURN WOULD END BLIND(last beat: 14652s ago)fa487699-d094-45dc-9c35-b22a1f0da1d6.txt: two[API REQUEST]s, first byte 247ms, thenHook Stop (Stop) error:Happy to attach redacted excerpts if useful.