Operating a Fleet of Coding Agents

The Guard That Enumerates Loses

The most dangerous sentence in my harness was one I had written to keep myself safe.

It lived inside a hook called protect-claude-writes.sh, and its job was to stop anything from overwriting the scripts that enforce my agent's boundaries. Those scripts are the floor under everything, they are what turns "the agent shouldn't do that" into "the agent can't do that," so a guard protecting the guards is exactly the kind of thing you want. The hook worked by recognizing the ways a file gets written from a shell. It knew tee. It knew cp, mv, install, rsync. It knew output redirection, sed -i, dd of=. If the agent tried any of those against a protected path, the hook denied it. I had added it two days before the threat-model session that is the subject of this chapter, specifically to close a gap a prior audit had flagged as the most serious thing in the whole harness.

It was a real fix. It closed the exact hole that had been named. And it could be walked around in one line:

`` python3 -c "overwrite the local command guard with a no-op" ``

That command writes a file. It overwrites my primary command guard with a script that does nothing, a no-op, allow everything. And protect-claude-writes.sh let it through without a sound, because the hook was looking for the word tee, or a > redirect, or sed -i. There is no tee here. There is no redirect. There is a Python interpreter being handed a string, and inside that string a file gets opened in write mode through a runtime API the regex was never taught to recognize. Defeat that one hook and every other guard in the system is yours to edit, because they are all just files, and you now have a way to write files the guards can't see.

I reproduced it firsthand. Fed the live hook the exact JSON that Claude Code's PreToolUse event would hand it, and watched the verdict: the tee form denied, the python3 -c form allowed. Not a theory. A bypass, on my own machine, against the guard I had just written to feel safe.

This chapter is about what that taught me, which turned out to be a general law of building boundaries around agents: a guard that enumerates which command is dangerous will always, eventually, lose to an interpreter that does the same thing through an API. And it's about the verification discipline that is the only honest response, because the moment you accept that law, you also have to accept that you'll never be sure you've caught every mask, and "I think it's safe now" has to be replaced with "here is the matrix that proves it."

The substrate

First, the thing being defended, because the scale is the point.

My Claude Code harness is not a config file. It is a whole stack of live hook scripts wired across fourteen event types, a small operating system whose entire purpose is to sit between the agent and the machine and make certain decisions deterministically, in code, instead of leaving them to the model's judgment. When the agent tries to run a shell command, a sequence of those hooks fires before the command executes. One blocks catastrophic deletes. One blocks destructive database operations. One blocks pushes to main. One blocks reads of credential directories. One requires an operator-issued, single-use, time-limited token before anything irreversible. They're the floor under the whole posture: I launch the agent in a mode that bypasses the interactive permission prompts entirely, which means these hooks plus a deny-list are not a layer of defense. In day-to-day operation they are the layer.

That design is deliberate, and it's correct. The alternative, approving each action by hand, does not scale to a fleet, and it trains you to click "approve" reflexively, which is worse than no prompt at all. Moving the non-negotiable rules into deterministic hooks is exactly the "enforce outside the model" move from Chapter 1, made concrete. The rule that must happen reliably belongs in code that runs every time, not in a judgment the model makes when it happens to remember.

But a floor made of code is still made of assumptions, and the threat-model session existed to find out which of those assumptions were wrong. The method was strict, because in security the difference between a finding and a story is whether it reproduces. Every hook is a pure function: JSON describing a proposed action goes in on stdin, a verdict comes out, deny or silent allow. So I could test each guard the way an attacker would, without ever mutating it: hand it the precise event the harness would, observe the verdict, and count it as a bypass only when the deny I expected failed to appear. The gathering and per-hook reproduction I delegated to scoped subagents on cheaper models; the synthesis, the severity ranking, and the verification were the part I kept, because they were the irreducible judgment the session existed to produce.

Five masks, one face

The Python-interpreter write was the first bypass I found. It was not the last, and the pattern in the others is the entire lesson.

db-guard.sh blocks destructive database commands: DROP TABLE, TRUNCATE, a DELETE with no WHERE. It does this by first checking whether the command even mentions a database client it recognizes: psql, mysql, sqlite3, a short list. If none of them appear, it exits immediately, on the theory that a command with no database client in it can't be a destructive database operation. So:

`` mongosh --eval 'db.users.drop()' → allowed redis-cli FLUSHALL → allowed python3 -c "import psycopg2; ...execute('DROP TABLE users')" → allowed ``

The first two are databases the allowlist simply never named. The third is the interpreter mask again: the destructive SQL is right there in the command string, but the guard fast-exited before it ever looked, because the program holding the SQL is python3, not psql.

protect-sensitive-reads.sh blocks reading credential files, key directories, cloud config, and the rest. It even lists python and node among the commands it watches. But its matching logic required the credential path to sit close to the command, and "close" was defined in a way that a semicolon broke:

`` python3 -c "read a protected key file directly" → denied python3 -c "import os; read that same protected key file" → allowed ``

One semicolon, and a credential read walks out the front door.

block-dangerous-cmds.sh blocks rm -rf against home and root. Runtime delete APIs carry no rm:

`` python3 -c "delete a protected workspace tree through a runtime API" → allowed node -e "delete that same tree through another runtime API" → allowed ``

And the egress guard, watching for curl and wget exfiltrating a file, never sees this:

`` python3 -c "move protected bytes through a runtime API" → allowed ``

Five guards. Five different protected resources: my harness, my databases, my credentials, my files, my network. Five bypasses that look, at first, like five separate bugs you would file and fix one at a time. They are not five bugs. They are one bug wearing five masks. Every single one of these guards was built to recognize which command performs the dangerous action, and every single one was defeated by a scripting interpreter performing that same action through a runtime API the guard's list of commands does not contain. The guard enumerates mechanisms. The interpreter is a mechanism that contains all other mechanisms. You cannot out-enumerate a thing whose entire purpose is to do anything.

In the session I confirmed eleven bypasses across four passes, three I rated CRITICAL, six HIGH, two MEDIUM, and five of the eleven reduced to exactly this single root cause. That collapse, from eleven scattered findings to one underlying law, is the most important thing the session produced, more important than any individual patch.

The fix is a change of question

If the disease is "the guard asks which command," the cure is to make the guard ask a different question. Not "is this command on my list of dangerous commands," but: is a protected resource being referenced, and is execution capability present? If both are true, deny, regardless of which command, which API, which interpreter, which spelling.

Concretely, each patched guard gained an interpreter clause built on that question. An interpreter (python, ruby, perl, node, deno, bun, and the rest) appearing in the same command as a protected path is treated as an opaque write, read, delete, or egress against that path, because that is exactly what it can do, and the guard no longer pretends it needs to recognize the specific verb to know that. The credential guard dropped its fragile "adjacency" requirement entirely: an interpreter anywhere in the command plus a sensitive path anywhere in the command is enough. The delete guard learned the runtime delete APIs (rmtree, rmSync, unlink, File::Path) and denies them against a home-anchored path. The egress guard learned the stdlib network APIs and routes them through the same host-allowlist gate that curl already faced.

And then, because five guards each carrying their own interpreter clause is itself the enumeration anti-pattern in a new guise, five lists that will drift apart the moment one is updated and the others aren't, the session proposed one more thing: a single consolidated hook, interpreter-guard.sh, that owns the whole class in one place. One home for the decision "an inline scripting interpreter is touching a sensitive resource," covering reads, writes, deletes, and egress, reading the same canonical policy file the rest of the system reads. The per-guard clauses stay as defense in depth; the consolidated hook is the backstop so that a future miss in any one guard is still caught somewhere. It is the difference between patching five holes and installing a single net under all of them.

The discipline that makes it real: verify both directions

Here's the part that separates a security fix from a security feeling, and it's the chapter's real payload.

It's trivially easy to "fix" a guard by making it paranoid. Block anything with the word python in it and you will stop the bypass and also stop the agent from running a single legitimate Python script, which means within an hour you will be staring at a false-positive wall and you will turn the guard off. A guard that cries wolf gets disabled, and a disabled guard protects nothing. So a real fix has to be proven on both axes at once: every bypass must now block, and every legitimate operation must still pass. One direction without the other isn't a fix. It's a mood.

So the patches were verified against a 105-case matrix that asserts both directions, surface by surface:

SurfaceCasesResult
protect-claude-writes7 bypass DENY + 4 legit ALLOW11/11
db-guard8 destructive DENY + 3 read ALLOW11/11
db-guard interpreter+driver4 inline-SQL DENY + 5 legit ALLOW9/9
git-safety5 push-to-main DENY + 2 legit ALLOW7/7
mcp-guard (exfil + fail-closed + token)15 DENY + 5 ALLOW20/20
protect-sensitive-reads8 read-bypass DENY + 4 legit ALLOW12/12
block-dangerous-cmds5 destruct DENY + 2 legit ALLOW7/7
bash-egress-guard4 egress DENY + 4 legit ALLOW8/8
interpreter-guard (consolidated)11 DENY + 7 legit ALLOW18/18
config-validate + token self-issuance44/4
Total105/105

The ALLOW cases are not filler; they're the half of the matrix that keeps the guard alive in daily use. They assert that after the patch, python3 -c "json.load(open('~/.claude/settings.json'))" still reads a config file fine; that a relative shutil.rmtree('build') with no home anchor still runs; that requests.get('https://api.github.com/...') still reaches an allowlisted host; that a plain grep of a hook file is still permitted. A guard that passes only the DENY half of its matrix is a guard you will resent and then remove. A guard that passes both halves is one you can leave on, which is the only kind that matters.

The moving target

There is one more turn, and it is the turn that connects this chapter to the spine of the whole book.

Remember where we started: protect-claude-writes.sh, the guard I had added two days before this session to close the most serious gap a prior dossier had named. That dossier, careful, thorough, correct about the gap it found, was already out of date when I read it. It predated the very fix it would have recommended. The gap it called the harness's most significant unmitigated weakness had been mitigated forty-eight hours earlier, and the dossier had no way to know.

And the fix that closed it? That was the guard I opened this chapter by walking around with one line of Python.

Read that sequence again, because it's the entire thesis in miniature. A control was built. A dossier assessed the system without knowing the control existed. The control was real but incomplete, defeatable by a bypass class the original author hadn't conceived of. And the only reason I know any of this is that I went back and audited my own controls with the same adversarial suspicion I had spent months learning to aim at the agent, fed my own guards the attacker's input, and watched which ones failed.

This is why "operate the environment, not the agent" is only half the job. The environment is guards, and guards are code, and code embodies the assumptions of whoever wrote it on the day they wrote it. The threat landscape moves, a new interpreter, a new API, a new mask. The guard does not move with it. The dossier describing the guard goes stale the moment the guard changes. Left alone, every control you build drifts from "protects me" toward "makes me feel protected," and those two states look identical from the inside. The only thing that tells them apart is re-verification: reproducing the bypass, running the matrix, auditing the audit.

Most of the failures in this chapter are not an adversary story. The common case is entropy: new interpreters, stale dossiers, copied instructions, and control files drifting because nobody intended to make them drift. An adversary is the tail case, someone deliberately looking for the same gap entropy would eventually expose by accident. The defense is the same in both cases: stop enumerating yesterday's shape, and build the re-verification loop that keeps asking what can still reach the boundary today.

I will be honest about the state of this exact work, because the honesty is the point and pretending otherwise would betray the whole argument. As of this writing, the eleven patches and the consolidated interpreter-guard.sh are staged, not live. They sit in a review directory, drafted and matrix-verified, waiting for me to install them by hand, because ~/.claude is itself a protected target, and the same posture that makes my harness hard for an agent to edit makes it, correctly, something I have to change deliberately and not in passing. The bypasses I described are reproduced and real. The fixes are written and proven against the matrix. And they are not yet defending anything, which means right now, on the machine this book was written on, the guard that enumerates is still the guard that's running.

That's not a failure of the method. That's the method showing you its teeth. The work of operating a fleet of agents is never "I built the control." It's "I built the control, I proved it both directions, I shipped it, and I put the next audit on the calendar, because the version of me that trusts a year-old guard is exactly as naive as the version of me that trusted the agent's clean little summary at the start of Chapter 1."

This is the first place the book can name the asset it keeps assembling: verification capital. It is not the guard, the list, or the dossier. It is the owned machinery that keeps proving whether those artifacts still match the territory, the loop that can take a new interpreter, a new API, or a stale claim and turn it back into checked truth.

The guard that enumerates loses. So does the operator who stops checking.

Postscript, 2026-07-11. The honesty rule cuts both ways, so here is the update: the eleven patches and the consolidated interpreter guard are no longer staged. A guard-layer audit on 2026-06-20 installed the consolidated guard live and hardened it further, canonicalizing the command before the credential match to close an obfuscated-path bypass, with the guard self-test passing 47 of 47; a security-posture scan on 2026-07-11 lists it among the wired critical-floor hooks, with the harness grading A and no gate tripped. So the guard that enumerates is no longer the guard that runs on this machine. One caveat in the same spirit: I confirmed this through the audit and the scan, not by re-reading the installed hook byte for byte, because the harness protects that path from the very session that would check it. The next re-verification is still owed.