# 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:

| Surface | Cases | Result |
|---|---|---|
| `protect-claude-writes` | 7 bypass DENY + 4 legit ALLOW | 11/11 |
| `db-guard` | 8 destructive DENY + 3 read ALLOW | 11/11 |
| `db-guard` interpreter+driver | 4 inline-SQL DENY + 5 legit ALLOW | 9/9 |
| `git-safety` | 5 push-to-main DENY + 2 legit ALLOW | 7/7 |
| `mcp-guard` (exfil + fail-closed + token) | 15 DENY + 5 ALLOW | 20/20 |
| `protect-sensitive-reads` | 8 read-bypass DENY + 4 legit ALLOW | 12/12 |
| `block-dangerous-cmds` | 5 destruct DENY + 2 legit ALLOW | 7/7 |
| `bash-egress-guard` | 4 egress DENY + 4 legit ALLOW | 8/8 |
| `interpreter-guard` (consolidated) | 11 DENY + 7 legit ALLOW | 18/18 |
| config-validate + token self-issuance | 4 | 4/4 |
| **Total** | | **105/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.
