Fences, Not Boundaries

Fences, Not Boundaries

Every repo in this fleet gives an agent a shell. That is the premise: work proceeds without a person typing the commands. It is also the problem. The failure we actually plan for is not an adversary. It is a competent agent, deep in a task, running the command that looked correct given the context it had at that moment. Recursive deletes aimed one directory too high. A force push onto the branch someone else was building from. A DELETE whose WHERE clause got lost in a refactor of the query string.

The archivist's lamp burned through its shade that evening; the courier never arrived.
The archivist's lamp burned through its shade that evening; the courier never arrived.

Our answer is a layer of Claude Code hooks that runs underneath every agent, in every mode. This is a description of what that layer does, what it deliberately does not do, and why we wrote the second part down.

Three events, one shape

Claude Code emits lifecycle events that an external script can subscribe to, and the script's exit status can veto the action before it happens. The mechanics are documented in the hooks reference; the design decisions are ours. We use three of those events.

PreToolUse is where refusal lives. Before a Bash invocation reaches the shell, block-destructive-bash.sh inspects the command text and rejects a fixed set of destructive shapes: recursive force removal on dangerous paths, force push, hard reset, SQL DROP and TRUNCATE, a DELETE with no WHERE, cluster deletions scoped to a namespace or to everything. The same event carries the Agent Teams guards, which enforce lead only operations, writes into the System Zone, and which skills a given role may invoke.

PostToolUse is where memory lives. Mutating commands and file modifications are appended to an audit trail by mutation-logger.sh and write-mutation-logger.sh.

Stop is where persistence lives. During autonomous runs, run-mode-stop-guard.sh challenges premature exit, because an agent that quits early on a long run fails silently, and silent failure is the expensive kind.

The important property is that none of this is a judgment call. It is deterministic shell, evaluated outside the model's context window. An agent cannot reason its way past a hook the way it can reason its way past an instruction in a prompt, because the hook is not in the conversation. Asking a model to police its own tool calls puts the guard and the guarded in the same process, which is not a control.

Saying what the fence is

The line in our own documentation that matters most is the disclaimer. block-destructive-bash.sh is a fence against routine mistakes, not a hardened security boundary. The accepted bypass classes are enumerated in the Loa hooks reference alongside the pattern details, so that anyone reading the code knows exactly which shapes fall through.

We write that down because the alternative is worse. A control whose limits are undocumented gets treated as complete, and then load accumulates behind it that it was never built to carry. Command text matching is defeatable by anyone who wants to defeat it: variable indirection, encoding, a wrapper script, an equivalent operation spelled differently. None of those are what a working agent does by accident, and accident is the threat model. The fence is sized for the traffic it actually sees.

This is the defense in depth) argument stated honestly rather than aspirationally. Marcus Ranum made the general version of the point decades ago in The Six Dumbest Ideas in Computer Security: enumerating badness catches known shapes and nothing else. We use enumerated badness where it fits, at the level of command shapes, and we use default deny where the stakes justify it.

The gate keeps its own weather.

Deny rules do the work patterns cannot

Underneath the pattern matching sits a separate mechanism with a different character. A deny rule set removes whole regions of the filesystem from reach: the SSH, cloud provider, cluster, and GPG credential directories, plus credential stores. That configuration is declarative and lives beside the settings the harness reads at startup.

The distinction is worth holding onto. Pattern blocking asks whether this particular command looks dangerous. Deny rules assert that a class of material is out of scope regardless of what any command intends. The second kind is stronger and cheaper, and it is the one that scales, because it does not require us to anticipate the phrasing. It is ordinary least privilege, applied to a process that is unusually creative about finding paths.

An agent has no legitimate reason to read a private key in the course of writing code. Removing the capability costs us nothing and removes an entire category of question.

The audit trail is the product

Both logging hooks append to a line delimited JSON file in the run directory. The format is JSON Lines: one object per line, append only, readable by streaming tools without parsing the whole file. The blocking hook writes there too, so refusals and completions land in the same stream.

We think of this as the highest yield component in the layer, above the blocking itself. Blocking prevents a specific known bad shape. The log makes every other mistake investigable after the fact. When an autonomous run produces a result nobody expected, the question is always the same: what did it actually do, in what order. A trail of mutating commands and file modifications answers that in minutes. Its absence turns the same question into an afternoon of reconstruction from git history and vibes.

Guardrails get the attention because refusal is visible. Records are what let a one operator fleet run repos it is not watching.

Identical in every mode

The layer is active in interactive, autonomous, and simstim modes. Not similar across modes. The same hooks, the same deny rules.

The temptation runs the other way. Interactive sessions have a human in the loop, so surely they can afford looser rules, and autonomous runs are where the risk concentrates, so surely they need something special. We rejected that for two reasons. Configuration that varies by mode is configuration that will be wrong in the mode that matters, and the divergence will be discovered during the incident rather than before it. And a hook that only fires in autonomous mode is a hook whose behavior nobody has exercised, because most hours are interactive hours. Uniformity means the guard is tested continuously by ordinary work.

What we would tell someone building this

  1. Put the control outside the model's context, in deterministic code, on a lifecycle event the harness enforces.
  2. Prefer capability removal to pattern matching wherever the capability is not needed, and accept that pattern matching is a fence.
  3. Document the bypass classes in the same file as the patterns, so the limits travel with the code.
  4. Log mutations unconditionally, in a format a stream processor can read, and treat that log as the deliverable rather than a byproduct.
  5. Keep one configuration across all modes.

None of this makes an agent safe in the sense that word usually carries. It makes routine mistakes expensive to commit and cheap to reconstruct, which is the tradeoff a fleet this size can actually maintain. The security overview for the harness covers the layers beneath ours. We build on top of it and try to be precise about where our part stops.


Get the next post

Free membership: new posts on how the fleet is built, delivered by email. Subscribe free

№ 5939