This is the authoritative current contract for the "no turn ends blind" primary backstop referenced from AGENTS.md section 8.
The predicate lives in bin/fm-turnend-guard.sh.
Primary scope lives in bin/fm-primary-scope-lib.sh, shared with the native session-start nudge in sessionstart-nudge.md.
Harness hook files adapt each enabled primary harness integration's turn-end mechanism to that shared predicate.
Related PreToolUse guards deny unsafe commands before execution rather than detecting a blind turn end afterward.
Their separate owners are arm-pretool-check.md, cd-guard.md, and subagent-guard.md.
Do not infer this guard's scope, loop safety, or compatibility tradeoffs for those guards.
bin/fm-guard.sh is a pull-based warning that runs only when another supervision command invokes it.
The turn-end guard closes the remaining gap at the primary's own turn boundary.
When work, a process-event source, or X-mode relay polling needs supervision and no identity-matched watcher has a fresh beacon, the harness integration must either block the turn end or force one bounded follow-up that uses the recovery instruction from the emitted session-start protocol.
Both guards require the same live lock, process identity, home/path binding, and fresh-beacon predicate.
The guard remains a backstop; watcher-continuity.md owns normal continuity.
The guard first calls the shared primary scope.
A secondmate home runs its own primary Firstmate session, so a genuine .fm-secondmate-home marker includes it whether the home is a linked worktree or plain clone.
The marker must be a regular non-symlink file whose whitespace-stripped first line is a non-empty identifier containing only letters, digits, dots, underscores, and dashes.
An unmarked checkout or invalid marker falls through to the git-dir check.
That check keeps crewmate and scout linked worktrees inert because their git dir differs from their git common dir.
It also requires AGENTS.md, bin/, and the effective state directory.
For an in-scope primary, the guard counts in-flight work from state/*.meta.
Registered state/procevent/*.source records also require supervision even though they have no task metadata.
The default cross-harness mode exits silently with no supervision need.
Every mode treats state/x-watch.check.sh as supervision need, so X-mode relay polling remains guarded without an in-flight task.
Otherwise it calls fm_watcher_healthy <state-dir> <watch-path> [grace-seconds] [home] from bin/fm-wake-lib.sh, the same identity-matched lock and fresh-beacon check used by bin/fm-watch-arm.sh.
bin/fm-guard.sh uses that same check rather than treating the status helper's fresh-beacon field as sufficient.
A stale beacon blocks even when a watcher pid is live.
A fresh leftover beacon blocks when the lock is missing, dead, or identity-mismatched.
FM_STATE_OVERRIDE wins over FM_HOME/state, and FM_HOME wins over repository-root state/.
FM_GUARD_GRACE controls beacon freshness and defaults to 300 seconds.
If jq is missing or hook stdin is empty, the guard exits 0 because it cannot safely read loop-guard fields.
- Claude registers two
Stophooks in.claude/settings.json, both anchored throughCLAUDE_PROJECT_DIR:bin/fm-turnend-guard.sh --claude, andbin/fm-claude-stop-autoarm.shwithasyncRewake: trueandtimeout: 28800. - Codex registers a
Stophook in.codex/hooks.json, anchors the executable to the hook process working directory, verifies a Firstmate-shaped hook-bearing root, and passes the original payload to the shared guard. - OpenCode listens for
session.idlein.opencode/plugins/fm-primary-turnend-guard.js, lets the watcher coordinator act first, and callsclient.session.promptAsynconce when the guard returns 2. - Pi listens for
agent_settledin.pi/extensions/fm-primary-turnend-guard.ts, runs once per logical agent run, and callspi.sendUserMessage(..., { deliverAs: "followUp" })once when the guard returns 2. - Grok registers a
Stophook in.grok/hooks/fm-primary-turnend-guard.jsonand delegates capability selection tobin/fm-turnend-guard-grok.sh. The tracked Claude Stop entries are inert whenGROK_AGENTis present, so Grok's Claude-compatible settings loading cannot create a second continuation path.
Claude and Codex can block a Stop directly with exit status 2 and stderr.
Both payloads carry stop_hook_active.
In the default Codex mode, a true value lets the second stop finish after one forced continuation.
Claude runs the guard with --claude, which ignores stop_hook_active and cooperates with the Stop-owned auto-arm.
Claude Code sets stop_hook_active=true on every stop after any stop-hook continuation, including asyncRewake rewakes, which re-opened the 2026-07-21 blind window under the default one-shot behavior.
The Claude mode waits up to FM_CLAUDE_AUTOARM_SYNC_WAIT_MS (default 800 milliseconds) and allows the stop when the watcher is healthy, state/.claude-autoarm.lock has a live autoarm role owner whose eventual failure must exit 2, or state/.claude-autoarm-epoch contains a fresh actionable rewake owned by this event epoch.
Fresh failed and failed-suppressed outcomes enter or advance the failure progression instead of acting as unconditional recovery proof.
The auto-arm itself rechecks the healthy watcher predicate and retries a bounded number of times before reporting a genuine failure.
The first fresh exhausted-failure epoch preserves its handoff without consuming a blocked-stop count, while later fresh failed epochs advance the same monotonic progression instead of resetting it.
When none of those proofs appears, it re-blocks up to FM_CLAUDE_TURNEND_BLOCK_BUDGET times (default 3, below Claude's 8-block override).
In Claude mode, positive watcher recovery clears the block budget, failure notice, and attended alarm together under the existing budget lock before either hook reports ordinary recovery.
The one loud attended fail-open is available only when the auto-arm has recorded an exhausted failure, its one notice is already consumed, the block budget is exhausted, and a final check finds neither a healthy watcher nor an automatic continuation.
Each epoch identity is accounted at most once under the budget lock.
Whenever both coordination locks are needed, positive auto-arm recovery and the terminal check acquire the auto-arm owner lock before the budget lock.
After that alarm, the Stop auto-arm suppresses further exit-2 continuations until positive watcher recovery, so the final fail-open remains reachable.
The alarm cannot repeat during that failure episode, and a later unhealthy stop blocks again.
A positively verified healthy watcher clears the failure notice, alarm, and block budget for a future independent episode.
A Claude failure notice describes the automatic mechanism as broken and does not direct a routine manual background arm.
OpenCode, Pi, and pi-signed expose passive callbacks for this purpose.
Their adapters fail open at the hook boundary to protect the user session but schedule one bounded follow-up when the predicate blocks.
The generated prompts use the canonical turn-end-guard kind after the U+2063 FIRSTMATE_OP: prefix, so Ahoy does not treat them as captain messages.
Each passive adapter owns a loop latch.
Pi keeps the latch across internal tool turns and clears it only when the generated follow-up settles or delivery fails.
OpenCode's forced follow-up is supported for persistent TUI sessions and remains fail-open in headless opencode run.
Grok makes exactly one typed capability decision from each running Stop payload.
A boolean stopHookActive selects native blocking, including both false on the initial stop and true on the bounded continuation.
The camel-case field has precedence when both spellings appear; when it is absent, a boolean stop_hook_active selects the same native path for compatibility.
The native path returns the shared guard's status and stderr to the same Grok process and never starts grok --resume.
When both capability spellings are absent, the adapter preserves one pre-native grok --resume fallback guarded by GROK_TURNEND_GUARD_ACTIVE and intentionally omits --permission-mode.
Malformed JSON, a selected field with a non-boolean type, missing jq, missing hook prerequisites, or an already-active legacy guard allows the stop without starting either continuation path.
Grok's project hook requires the checkout to be trusted with /hooks-trust or launch-time --trust; genuine pre-native builds can run the same tracked hook from an isolated global hook directory.
If a passive adapter cannot invoke its SDK, or the Grok legacy fallback cannot find grok or a session id, the next pull-based fm-guard.sh call reports the problem.
That warning uses bin/fm-supervision-instructions.sh --repair-line, so it always points to the active harness protocol rather than embedding another repair command.
- Child crewmate and scout worktrees are outside scope.
- A valid secondmate home is in scope; an idle secondmate endpoint with no X-mode relay poll remains healthy because it has no supervision need.
- The direct-blocking and bounded passive-follow-up split is limited to the primary integrations listed above.
- OpenCode headless mode and untrusted Grok project hooks remain fail-open at the host boundary.
- Kimi Code CLI 0.29.1 exposes only global
[[hooks]]configuration in~/.kimi-code/config.toml, including aStopevent with snake_case payload fieldshook_event_name,session_id,cwd, andstop_hook_active. - Kimi has no project-level hook configuration and remains outside the primary guard integrations above.
- Captain-approved Kimi crew wake support uses
bin/fm-kimi-turnend-hook.shto edit only one marker-delimited Firstmate region in that global config and install a silent always-zero hook. - The hook remains inert unless the payload
cwdcontains a per-task token pointer that resolves through Firstmate's private registry to onestate/<id>.turn-endedmarker. - Installation refuses before writing unless
python3withtomllibandjqare available. - If
jqis removed after installation, the hook remains silent and exits 0, turn-end wakes stop, and Kimi crews fall back to idle detection. - Unreadable hook input remains fail-open.
- No harness adapter uses a shell ampersand to manufacture supervision.
tests/fm-turnend-guard.test.sh covers the predicate, main and secondmate primary scope, child-worktree exclusion, FM_HOME and FM_STATE_OVERRIDE precedence, the live-lock and fresh-beacon guard predicate, the cooperative --claude claim wait, monotonic failed-epoch progression, bounded attended fail-open, post-alarm continuation suppression, positive recovery reset, Pi logical-run latching, missing-jq behavior, all five primary registrations, Grok native and legacy selection, typed field precedence, malformed input, and exactly-one-path safety.
tests/fm-guard-stale-banner.test.sh covers the matching pull-guard predicate, including the fresh-leftover-beacon negative control.
tests/fm-kimi-harness.test.sh covers the separate Kimi crew hook's format preservation, idempotence, refusal cases, token guard, spawn registration, and teardown cleanup.
tests/fm-supervision-instructions.test.sh covers recovery-line ownership and pi-signed's identity-preserving reuse of Pi's protocol.
FM_PI_LIVE_E2E=1 tests/fm-pi-primary-live-e2e.test.sh is the opt-in isolated Pi path.
verification/supervision.md records the active cross-harness empirical evidence, including the 2026-07-24 Claude asyncRewake revalidation.