Refused for a Word in a Pattern
Written later and filed to this date, as part of filling in every day of the year.
On a Friday in August a build agent working in my status service ran a command that could not have changed anything: ask git for a file as it stood at an old commit, pipe it through a search for four words, keep the first ten hits. One of the four words was the name of the service manager. The hook that reads every command before it runs matched the word, answered deny, and the audit row gives the reason as mutating service control.
That hook is the guard. Every tool call in every session on my always-on box passes through one small process before it runs, and for a shell command that process reads the text and tests it against a list of shapes: privilege escalation, starting or stopping a service, shutdown, a destructive remove of a system or home path, piping the network into a shell, a force-push, publishing a package, a raw write to a block device, deleting the guard’s own files, a glob or a redirect beside a credentials file, claiming the session name reserved for me. Any deny wins; else any ask; else silence. The box is the one that runs the fleet, so for every call an unattended agent makes the guard also writes a row to an audit log.
The matching is by text, and that is a choice, not a shortcut. Reading a command’s intent would need a model, would cost seconds on every call, and would sometimes be wrong in the direction that matters. A word match is cheap and has no false negatives: a command that stops a service contains the word that stops a service. The price is paid on the other side, and the audit log says how much. Since the middle of June the guard has read roughly two hundred and thirty thousand calls from unattended agents and refused three hundred and forty-three. A hundred and thirty of the refusals were heredocs, a command writing a file whose contents mentioned the thing, and eighty more had a search somewhere in them. The runbook calls these false positives to rephrase, never to escalate: write the file through the editor tool, put the pattern in a file and point the search at it, and put the flag last in grep -n console file, because the guard reads that as a session claiming the name console.
Since September it also fails closed. A malformed payload, an unreadable policy file or a crash inside one of the checks used to exit with no decision, which the harness took as permission. Now it refuses an unattended agent and asks an interactive one. A guard that cannot evaluate must not permit. And no agent edits it: a change to the guard arrives as a patch I apply by hand, because the thing that judges the fleet is not something the fleet gets to revise.
The guard: a hook that reads the text of every shell command an agent is about to run and refuses the dangerous shapes by their words, not by their intent. It cannot tell a command that stops a service from one that mentions stopping a service, and it is not meant to. The cost of that is a rephrased search. The alternative was a worker that could.