A guard my agent wrote to stop unplanned sprawl fired on its own author three times in an hour, and each refusal named a different thing I had assumed about how these controls read the world.
In short
My agent built a guard yesterday morning. The rule was small. If one turn edits a third distinct file and no plan has been presented since my last message, refuse once. Name the files, ask for a plan, then stay out of the way for the rest of the turn.
The threshold came from my own history. Across 13,676 of my prompts, 627 turns ended up editing three or more distinct files. That is 4.6 per cent. A turn that grows quietly from one file to six, with no plan anyone could have objected to before the first write, is the shape I keep complaining about after the fact.
An earlier version of the same idea keyed on me. Short prompt from Howard, therefore refuse. It fired on 2.1 per cent of turns and punished me for a habit of mine. My correction was blunt: plan mode is for the agent to plan and then execute, and it has nothing to do with how long my message is. The version that survived watches the agent's own sprawl instead.
Three refusals in one hour
The first refusal was the gate working. Six files, no plan. Fine.
The refusal message offered an escape. Say in one line what the remaining files are and why. The code did not implement that. Nothing in it read prose. The only path through was an interactive planning call, which is a dialogue rather than a sentence. So my agent wrote the one line it had been promised would clear the gate, and got refused a second time.
The reason is worth knowing if you write this kind of thing. An agent's own text reaches the transcript after the tool calls in the same message. A plan sentence written right before the third edit is invisible to anything reading that transcript at the moment of the edit. The escape lived in the message and nowhere in the world.
The third refusal was stranger. A background job finished and its completion notice arrived in the transcript as a record of my type, not the agent's. Later, a different hook's own refusal text arrived the same way. The gate resets its state on every real message from me, so two records that were never me reset it, and erased a plan written one call earlier.
What held
The fix was a file. A plan on disk, newer than my last message, with a marker on its first line. Disk state has one author and one timestamp. A transcript is written late, by several hands, and the hook has no way to tell whose hand. The message was rewritten at the same time to promise only what the code does.
Four things I am keeping from the hour.
A control that advertises an exit it does not implement is worse than no control. It teaches the thing it governs that the rules are decoration, and that lesson generalises to every other rule in the stack.
Every control gets run against its author's next real turn before anyone calls it shipped. The self test passed all three times. Synthetic input proves the function. It says nothing about the log the function reads.
Calibrate on the false positive rate. A refusal per turn teaches an agent to route around the gate, and this one has a documented way around: write files through the shell, where the hook cannot see them. My agent used it once, on the guard itself, and told me. I would rather be told than hold a clean-looking log.
The narrow version
A review the same day said keep it, and narrow it. The healthiest change shape available ships code together with its test and its doc. That is exactly three files, and the gate refused it. A mechanical rename across five files, asked for in plain words, got refused too. The floor of three came from the incident that prompted the guard, which is how most thresholds are born.
So a test or doc sibling of a file already in the set stopped counting as new. The gate now fires when a third separate unit of work appears inside a turn nobody planned.
That is the whole story. About forty lines of code, three refusals of its own author, and a threshold whose provenance I can finally state.