# How Turn-End Guard and Sub-Agent Guard Backstops Work in Firstmate

> Understand how Firstmate's turn-end guard and sub-agent guard backstops prevent unsupervised turn endings and unauthorized delegation. Learn about these essential safety features.

- Repository: [Kun Chen/firstmate](https://github.com/kunchenguid/firstmate)
- Tags: internals
- Published: 2026-08-13

---

**The turn-end guard and sub-agent guard in Firstmate are safety backstops that prevent primary sessions from ending turns without supervision and from delegating work outside the fleet via untracked sub-agents.**

Firstmate implements a robust supervision cycle through two critical safety mechanisms defined in the `kunchenguid/firstmate` repository. These guards ensure that no primary session can terminate a turn while a watcher remains unhealthy or tasks are in-flight without oversight, nor can they spawn hidden sub-agents that bypass fleet tracking. Understanding how these components inspect harness payloads, validate watcher health, and classify tool shapes is essential for maintaining operational integrity in multi-agent workflows.

## What Is the Turn-End Guard?

The **turn-end guard** guarantees that no turn ends "blind"—that is, without proper supervision. Before a primary (or second-mate primary) can finish its turn, the guard verifies that the fleet's watcher is healthy and that no unsupervised work remains in flight. If these conditions are not met, the guard blocks the turn termination and forces a bounded follow-up turn, ensuring continuous oversight.

## How the Turn-End Guard Works

The primary entry point for this backstop is [`bin/fm-turnend-guard.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-turnend-guard.sh), which each harness invokes via its stop hook. The script executes a strict verification pipeline before allowing a turn to conclude.

### Payload Verification and Scope Checking

The guard first validates that the payload originates from an expected harness—Claude, Codex, OpenCode, Pi, Grok, or Cursor. It then verifies the **primary-scope marker** by calling `fm_primary_scope_matches` from [`bin/fm-primary-scope-lib.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-primary-scope-lib.sh) to ensure the session is operating within a genuine primary checkout rather than a child worktree. This prevents the guard from misfiring on secondary or sandboxed contexts.

### Watcher Health Validation

Once scope is verified, the guard calls `fm_supervision_status`, which delegates to `fm_watcher_healthy` in [`bin/fm-wake-lib.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-wake-lib.sh). If the watcher reports healthy, the guard exits silently with status `0`, permitting the turn to end. If the watcher is unhealthy or tasks remain in-flight without supervision, the script prints a descriptive banner to `stderr` and exits with status `2`, forcing the supervisor to create a bounded follow-up turn.

### Harness-Specific Behaviors

Different harnesses integrate the guard with adapted workflows:

- **Claude**: The harness uses a **co-operative auto-arm path** that limits automatic continuations via a block-budget. If the budget exhausts, the guard provides a fail-open mechanism to prevent infinite loops.
- **Cursor**: The wrapper script [`bin/fm-turnend-guard-cursor.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-turnend-guard-cursor.sh) forwards the payload to the main guard. Exit status `2` is treated as a silent no-op, with the Cursor extension generating the follow-up turn directly.

## What Is the Sub-Agent Guard?

The **sub-agent guard** prevents primary sessions from using delegation-shaped tools—such as `Agent`, `Workflow`, or `Spawn`—that would create work the fleet cannot track. By blocking these calls at the point of use, the guard ensures all delegated work flows through [`bin/fm-spawn.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-spawn.sh), which writes mandatory `state/<id>.meta` metadata required for supervision.

## How the Sub-Agent Guard Works

The entry point is [`bin/fm-subagent-pretool-check.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-subagent-pretool-check.sh), registered as a *PreToolUse* hook in each harness. The script intercepts tool invocations before execution and applies shape-based classification to determine whether to allow or deny the call.

### Tool Shape Classification

The guard receives the tool name from the harness and checks it against a list of delegation stems, including `agent`, `subagent`, `task`, `workflow`, and `spawn`. If the tool name matches these patterns, the script checks exclusion lists (`OBSERVE_ONLY_TOOLS` or `PLAN_ONLY_TOOLS`). Tools on these lists are allowed to pass; all others are treated as unauthorized delegation attempts.

### Denial Logic and Escape Hatches

When the guard detects a prohibited delegation tool, it exits with status `2` and writes a JSON denial message to `stderr` (or `stdout` for Grok). This denial prevents the tool from spawning an undisclosed agent. For intentional exceptions, the environment variable `FM_ALLOW_SUBAGENT=1` disables the guard entirely, allowing the primary to use delegation-shaped tools without restriction.

## Key Implementation Files

The backstop layer relies on the following components in the repository:

- [`docs/turnend-guard.md`](https://github.com/kunchenguid/firstmate/blob/main/docs/turnend-guard.md) – Human-readable contract defining predicates, harness integrations, and fail-open trade-offs.
- [`bin/fm-turnend-guard.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-turnend-guard.sh) – Core script implementing the watcher health check, banner generation, and Claude auto-arm cooperation.
- [`bin/fm-turnend-guard-cursor.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-turnend-guard-cursor.sh) – Cursor-specific wrapper forwarding payloads with the `--cursor` flag.
- [`docs/subagent-guard.md`](https://github.com/kunchenguid/firstmate/blob/main/docs/subagent-guard.md) – Specification of the sub-agent guard, exclusion lists, and escape hatch behavior.
- [`bin/fm-subagent-pretool-check.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-subagent-pretool-check.sh) – Implements shape-based tool classification and enforcement logic.
- [`bin/fm-primary-scope-lib.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-primary-scope-lib.sh) – Shared library used by both guards to validate primary checkout scope.
- [`bin/fm-supervision-lib.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-supervision-lib.sh) – Provides `fm_supervision_status` and related supervision predicates.
- [`bin/fm-wake-lib.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-wake-lib.sh) – Contains low-level watcher health checks including `fm_watcher_healthy`.

## Summary

- The **turn-end guard** in [`bin/fm-turnend-guard.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-turnend-guard.sh) validates harness origin, primary scope via `fm_primary_scope_matches`, and watcher health via `fm_watcher_healthy` before allowing a turn to conclude.
- Exit status `2` from the turn-end guard triggers a bounded follow-up turn, ensuring no blind turn endings occur.
- The **sub-agent guard** in [`bin/fm-subagent-pretool-check.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-subagent-pretool-check.sh) blocks delegation-shaped tools unless they appear on exclusion lists or the `FM_ALLOW_SUBAGENT=1` escape hatch is set.
- Both guards ensure all work creation flows through [`bin/fm-spawn.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-spawn.sh), maintaining durable `state/<id>.meta` records required for fleet supervision.

## Frequently Asked Questions

### What happens when the turn-end guard blocks a turn?

When the turn-end guard detects an unhealthy watcher or in-flight work without supervision, it prints a banner describing the condition to `stderr` and exits with status `2`. According to the `kunchenguid/firstmate` source code, this exit code forces the supervisor to create a bounded follow-up turn, preventing the primary from ending its session blindly. For Claude, this integrates with an auto-arm budget system; for Cursor, the exit triggers a silent follow-up generated by the Cursor extension.

### How does the sub-agent guard distinguish between allowed and prohibited tools?

The sub-agent guard classifies tools by shape, checking if the tool name contains stems like `agent`, `workflow`, or `spawn` as implemented in [`bin/fm-subagent-pretool-check.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-subagent-pretool-check.sh). If the name matches these stems, the script consults exclusion lists (`OBSERVE_ONLY_TOOLS` and `PLAN_ONLY_TOOLS`). Tools on these lists are permitted; all other delegation-shaped tools are blocked with exit status `2` and a JSON denial message.

### Can these guards be disabled for testing or specific workflows?

The sub-agent guard supports an escape hatch via the environment variable `FM_ALLOW_SUBAGENT=1`, which disables shape-based blocking entirely. The turn-end guard does not provide a global disable switch, as it is fundamental to the supervision contract; however, the Claude harness implements a fail-open mechanism after exhausting a block-budget of automatic continuations, allowing graceful degradation rather than permanent blocking.

### Why must sub-agents be spawned through [`bin/fm-spawn.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-spawn.sh) instead of native delegation tools?

Firstmate requires all delegated work to be visible to the fleet's supervision cycle. Native delegation tools (like `Agent` or `Workflow`) create sub-processes without writing `state/<id>.meta` metadata, rendering them invisible to the watcher. The sub-agent guard forces primaries to use [`bin/fm-spawn.sh`](https://github.com/kunchenguid/firstmate/blob/main/bin/fm-spawn.sh), which creates the durable metadata records that the turn-end guard later inspects to determine if work remains in-flight.