The Guard That Jailed Its Own Warden

/ Article
[ Fig. 1 ]

I built a guardrail to stop my fleet supervisor from doing hands-on work. Within forty-eight hours, the guard locked the supervisor out of its own job.

It refused to let the supervisor merge a verified pull request. It refused to let it restart a wedged seat. And when we tried to deploy a patch to fix the guard, the guard blocked the command.

The warden had built a jail, stepped inside to test the lock, and heard the latch click shut.

Here is why it happened, how we fixed it, and the rule we now enforce across our fleet.

The Drift Problem

I run a multi-agent fleet. The lead coordinator sits in a terminal pane named firstmate. Under firstmate are worker agents—we call them Second Mates—running in isolated worktrees across different harnesses: Pi, Claude Code, Grok, and Codex.

The lead coordinator has one job: decide who does the work, never how.

Its context window is the most expensive resource in the fleet. When a supervisor starts running git checkout, editing code, running pytest, or SSHing into servers, two bad things happen:

  1. Its context window clogs with compiler warnings, log dumps, and terminal noise.
  2. The rest of the fleet starves for direction while the boss acts like a junior developer.

To stop this drift, we gave the supervisor a hard boundary. We wrote a tool guard: bin/fm-jev-guard.py.

The rule sounded simple:

  • If firstmate tries to run hands-on implementation or sysadmin work, deny the command.
  • Force the supervisor to delegate the job to a worker pane using bin/fm-route-dispatch.sh or bin/fm-send.sh.

How the Guard Worked

The guard plugged into the agent harness as a PreToolUse hook. Before any bash command ran in the supervisor pane, the hook checked the command string.

It used two tiers:

[Command] 

   ├── Tier 1: Static Allowlist
   │     - Safe read commands (cat, grep, ls)
   │     - Routing commands (fm-send, fm-brief)
   │     - Queue drains (bd, tasks-axi)
   │     → ALLOW immediately

   └── Tier 2: System One Evaluation (Jev)
         - Check command against prompt criteria
         - allow_supervisor vs require_delegation
         - Deny on hands-on code or system mutations

If Tier 1 did not recognize the command, Tier 2 sent the command to a fast classifier model (api.typesafe.ai/v1/systemone) with a strict timeout.

If the model saw words like “git”, “merge”, “install”, or “service”, it emitted require_delegation. The hook returned a structured denial:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny"
  },
  "systemMessage": "[require_delegation] Command violates supervisor boundary in w1. Hands-on execution must be delegated to Second Mates."
}

The supervisor would see the denial, stop typing, and dispatch a task to a worker.

It felt like clean engineering.

The Trap: Confusing Supervision With Project Work

The design had a fatal flaw: it failed to distinguish project work from supervisor lifecycle tools.

A supervisor does not edit application code. But a supervisor does manage the workers who do. In our system, the supervisor must:

  • Merge pull requests after worker tests pass (bin/fm-pr-merge.sh).
  • Check pull request status (bin/fm-pr-check.sh).
  • Interrupt or restart dead seats (bin/fm-control.sh).
  • Tear down finished worktrees (bin/fm-teardown.sh).
  • Claim worker leases (bin/fm-lease.sh).

Because these commands were shell scripts that internally called git merge or changed system process state, the classifier treated them as forbidden project mutations.

First, a Second Mate finished a task and green checks landed on the PR. Firstmate ran:

bin/fm-pr-merge.sh 168

The guard fired: DENY. Git operations violate the supervisor boundary.

Next, a worker pane got stuck in a loop. Firstmate attempted to recover the pane:

bin/fm-control.sh restart w42:p2

The guard fired: DENY. Process management violates the supervisor boundary.

The supervisor was now helpless. It could not merge finished work. It could not restart dead workers. It sat in its pane, apologetically reporting to the human captain that it was forbidden from touching its own steering wheel.

The Fix: Fast-Passing Lifecycle Verbs

The repair took two changes:

1. Hardcode Supervisor Lifecycle Verbs in Tier 1

You cannot trust an LLM prompt or a keyword filter with the supervisor’s own controls. The lifecycle verbs must be explicit, immutable constants in the Tier 1 static allowlist:

# bin/fm-jev-guard.py

SUPERVISOR_LIFECYCLE_PREFIXES = (
    "bin/fm-control.sh",
    "bin/fm-teardown.sh",
    "bin/fm-pr-merge.sh",
    "bin/fm-pr-check.sh",
    "bin/fm-lease.sh",
    "bin/fm-crew-state.sh",
    "bin/fm-fleet-view.sh",
)

def is_fast_pass_supervisor(subcmd: str) -> bool:
    clean = subcmd.strip()
    # Lifecycle scripts bypass semantic classification entirely
    if clean.startswith(SUPERVISOR_LIFECYCLE_PREFIXES):
        return True
    ...

If a command starts with bin/fm-control.sh, it never hits the model. It never risks a hallucinated block. It passes in under 1 millisecond.

2. Treat Denials as Telemetry, Not Gospel

When a guard blocks an agent, the worst thing you can do is let that denial vanish into the terminal scrollback.

We added an append-only audit stream: .jev-guard-telemetry.jsonl. Every denial logs:

  • The exact raw command.
  • The working directory.
  • The reason (Tier 1 vs. Tier 2 model output).
  • The confidence score.

Every morning, we grep that file. If we see legitimate supervisor actions getting blocked, we patch the allowlist before the fleet wedges again.

The Rules We Keep

Building guardrails for autonomous agents taught us three practical lessons:

  1. Every allowlist must include the rails’ own repair tools. If the guard can block the command that fixes the guard, your system has a single point of failure.
  2. Supervision is a distinct technical domain. Managing workers (merging, killing, spawning, leasing) looks like system administration to a generic model. You must separate supervisor verbs from worker verbs in code, not in prompts.
  3. Denials are hypotheses, not verdicts. Log every denial to disk. A high denial rate does not mean your guard is working; it often means your supervisor is choking.