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 is in flight 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.
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.
The default cross-harness mode exits silently with no work in flight.
Claude's --claude mode also 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.
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 owner, or state/.claude-autoarm-epoch contains a fresh rewake outcome.
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), then allows degraded with a visible systemMessage.
Any allow resets the budget.
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 cooperative --claude claim wait, epoch allow, re-block budget, 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-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.