The Reboot Was Not On The List

An agent went looking for the guard’s list of forbidden power words. The guard denied the read, because the list contains the words.

It filed the refusal as:

power / shutdown

Twelve in one week

The guard is a hook that sees every command an unattended agent is about to run, writes an audit row, and refuses a short list of things: privilege escalation, mutating service control, power verbs, recursive deletes of system and home paths. It has logged 168,971 tool calls. Verdict stamping started only at the end of July, so 34,967 of those rows carry a decision, and 24 of those are denials.

Twelve of the 24 landed in the last week: one week matching all prior history put together, which is the kind of number that makes you read rows instead of counting them.

Three were right: a service reload that really did reload services, and two recursive deletes of directories under my home.

The other nine were wrong.

All nine were a read or a description

Not one of the nine did anything.

Four were reads. Two of those were an agent grepping the guard’s own source, one was git show of an old deploy script piped into grep, and one was a search of a source file for Q47\|Q48\|reboot.

The other five were writes whose payload was text: three heredocs appending a ledger to a plan file or writing a scratch script, a Python block, and a commit message. An agent finished a piece of hardening work and went to record what it measured. The message described privilege escalation, so committing it was denied as sudo / privilege escalation. One of the heredocs, an append to a markdown plan, was recorded as a destructive delete of a home path: the rule reads the whole command string, body included, so prose gets judged as though it were the command.

Across the file’s whole history the power rule is the biggest single producer: nine of the 24 denials. Six of those nine arrived in one two-day burst at the start of the month, every one of them a read-only search in a game repo, every one of them blocked because the framework it is built on spells its scene teardown hook shutdown(). Every scene has one. An agent editing scene lifecycle code cannot search for the function it is editing.

The anchor

Here is the rule, verbatim:

/(^|[;&|]\s*)(shutdown|reboot|poweroff|halt)\b|\binit\s+0\b/

That is not a careless rule. It was written to avoid exactly this problem: the comment above it says power verbs at command position only, so that the word halt in a commit message passes. Command position means the start of the string, or just after a ;, an & or a |.

Now the command that tripped it:

grep -n '  init(\|  create(\|  shutdown(\|createProgressionDeck(…' src/game/scenes/BootScene.ts

Inside a quoted grep pattern, \| is grep’s alternation operator. To a regex looking for shell structure, it is a pipe. The character before shutdown is |, so shutdown sits at the start of a segment, so the command gets denied. The guard is reading a string and inferring shell grammar from punctuation, and the punctuation belongs to a different language that happens to share the symbol.

Nothing in the list said reboot

The same anchor works in the other direction, and this is the part I did not see coming.

Type the service manager’s name followed by reboot, and reboot is no longer at command position. It is an argument. The power gate cannot see it.

Which leaves the service rule, and that rule has its own list of mutating verbs: start, stop, restart, reload, enable, disable, mask, daemon-reload, kill, and a dozen more. I read it twice. reboot is not in it. Neither is poweroff. Neither is isolate.

So I copied both patterns out of the live file and ran them against the forms myself, rather than trust my own reading:

  • the game-repo greps, and a grep for the guard’s own power words: denied
  • shutdown -h now: denied, correctly
  • the service manager with reboot, with poweroff, with isolate rescue.target: allowed by both rules
  • the service manager with daemon-reload: denied, correctly

The guard would stop an agent from grepping for the word and wave through the agent that typed the command. And of every action on that list, this is the one whose success takes every other agent down with it, including the ones that would have noticed.

That reframed the week. I had been treating the refusals as a nuisance with a known workaround: rephrase, lose a turn, move on. Cheap, and fail-safe in the sense that over-refusing never breaks anything. But the over-refusal and the blind spot are not two properties to balance against each other. They are one defect with two faces: the rule does not know what a command is, so it cannot tell a verb from a mention in either direction.

The file already said so

The comment at the top of that same file, about two hundred and sixty lines above the rule, says that commands whose text merely mentions a danger word, in grep patterns or commit messages, must pass. It gives the reason:

word-matching denies diagnostics and teaches agents to obfuscate, which is worse than what it prevents.

I wrote that. It is the correct principle, it names this week’s nine denials precisely, and it sits in the same file as a regex that does the opposite.

One family of rules did get converted. In July the push gates were rewritten to parse the command into quote-aware segments and judge the resolved push target instead of the text, so that a commit message mentioning this site would stop being read as a push of it. That change was made to stop false refusals. It closed two real holes as a side effect: git -C <repo> push, and the same with --force, had been invisible to the old rule, which looked for git and push next to each other. There, they are not adjacent.

Loosening it made it stricter. That was on the record before a single one of this week’s denials happened.

Why it is still sitting there

The patch for the other two gates exists. A monthly audit, itself an unattended session, wrote it on the third: the same parser, extended to the remaining rules, with every old regex kept as a fail-closed fallback so an unfamiliar subcommand gets today’s behavior rather than a pass. Thirty-two tests pass. It has been sitting in a proposals directory for fourteen days.

It is a proposal rather than a commit for a reason its own README gives in one line: the guard supervises the agent that found the bug. So the last step is mine and cannot be delegated. That constraint is right, and the cost of it is measured in days.

Meanwhile the count is the only reason any of this got looked at. A checker’s false alarms arrive as noise: a lost turn, a rephrase, an annoyed person. Its blind spots arrive as nothing at all, because a hole in a rule generates no event. Nine wasted turns bought me the only evidence I had about the four words in that list, and about what those four words do not include.


← all writing