# The Guard That Doesn't Trust Itself

I spent two rounds trying to break my own agent-safety guards. Every bypass I found collapsed into thirteen shapes, and those into three root causes, and the highest-leverage fix was never more pattern-matching.

There's a chapter in my book called "The Guard That Enumerates Loses." Its argument is simple: a safety check that works by listing the dangerous things (block `rm`, block this network tool, block a push to the main branch) can always be walked around, because the list is finite and the ways to do damage are not. That's the *why*. This note is the *where*: I ran two adversarial tournaments against my own guards, red-team against blue-team, and tried to find every hole. The interesting part wasn't the holes. It was that they all rhymed.

Across both rounds, every confirmed bypass collapsed into thirteen distinct patterns I ended up naming, and those thirteen collapsed into **three root causes.** Once you can name the three, the whole field stops looking like a game of whack-a-mole and starts looking like three design assumptions, each one quietly false.

## Root one: the guard reads text; the computer runs meaning

The first family is the one the book chapter is about. A guard scans the *text* of a command for dangerous-looking tokens. But the thing that actually executes doesn't care about the text; it cares about what the text *means*, and there are endless ways to mean the same thing without looking like it.

A guard that blocks the verb "copy a file" doesn't block a one-line script that calls the filesystem API to write the same bytes to the same place. A path the guard would recognize can be assembled at runtime from pieces, or reached through a symlink, or spelled with shell escapes that the scanner sees as harmless hex while the shell expands them into the forbidden word. The guard is reading; the interpreter is doing. That gap has no natural ceiling; there is always one more interpreter, one more encoding, one more way to spell it.

## Root two: a door the guard isn't standing in front of

A guard is written to sit in front of one channel (say, the agent's built-in file-editing tool) and is simply invisible to the same operation performed through a different channel, like a tool provided by a plugin. Same write, same destination, different door; the guard is watching the wrong one.

The sharpest version of this is the deliberate exception. Every guard has carve-outs (a folder it's allowed to write, a config block it's allowed to set) and each carve-out is an unguarded path into something sensitive. And the nastiest: one tool reaching across to overwrite the *policy file another guard depends on.* A write tool the policy didn't think to protect can replace the rulebook every other guard reads.

## Root three: the guard trusts itself

This is the one I didn't see coming, and it's where the most severe findings lived. Every guard implicitly trusts its own machinery: its state store, its config file, the tools it shells out to, even its own report that it ran at all. None of that trust was earned.

- An authorization system checked whether a fresh token-file existed in a directory, but never protected *who could write to that directory.* Any write primitive could mint its own permission slip.

- Guards loaded their rulebook from "the path in this environment variable, or the default if it's unset." Point the variable at an empty-but-valid rulebook and the fallback never fires; the guard cheerfully enforces nothing.

- Every guard emitted its denials by shelling out to a small text tool, called by bare name. Replace that tool on the path with a no-op and *every* guard fails open at once.

- A "work is done and verified" gate could be satisfied by an empty commit with no tests. The proxy for "verified" was something the agent controlled.

- A timeout, a never-installed backstop, a missing dependency: each resolved to a quiet *allow.*

The safety layer applied a careful threat model to everything except itself. It was the unreliable narrator I distrust in APIs and models, turned on my own guards.

## Why "add more patterns" is the wrong instinct

The reflex, every time you find a bypass, is to add its pattern to the denylist. But the tournaments produced new findings every single round (there was no round where the well ran dry) precisely because per-call text-scanning on a finite list is *not a convergent model.* It generates an endless sequence of holes, not a shrinking one.

The thing that actually converges is a different posture: stop enumerating what's dangerous (you can't finish) and start protecting what's worth protecting (it's fixed). "The protected resource is referenced *and* execution capability is present, therefore deny," a rule keyed on the thing you care about, not the spelling of the attack. And the single highest-leverage move fell straight out of root three: **three of the four most severe findings closed by simply protecting the guard's own trust anchors:** its token store, its rulebook, the path to its rulebook, its toolchain. Not one new pattern. Just making the gate hold itself to the standard it holds everything else to.

## The residual I can't pattern my way out of

Here's the honest floor. Every guard above assumes the adversary is the agent emitting a dangerous call. The realistic adversary is the *content the agent reads:* a poisoned row returned by a legitimate query, a planted instruction in a file or an email body. None of that ever appears as a tool call with a scary argument. It arrives as an innocent read that returns hostile content, and slides into the model's context wearing the same clothes as the truth. No scan of *outbound* commands touches it. The fix there is architectural, not another regex: treat everything a tool returns as untrusted environmental data, not as instructions. It's the work I'd reach for next.

A few other limits worth stating plainly: static scanning genuinely cannot catch a target assembled at runtime, so some of this is backstopped by a human reading the diff. The convergence I'm claiming is at the level of the *three root causes*, not a promise that every individual hole is found; new instances of "one more interpreter" will always exist. And the most sobering finding of all: a few hardening fixes that a prior pass *reported* as shipped turned out never to have been installed. A guard you believe is in place and isn't is worse than no guard, because you stop watching the door.

A guard's account of its own coverage is not evidence. You have to check it, including, especially, when the thing you're checking is the checker.
