Block, Steer, Rewrite
A runtime hook that guards a coding agent has three responses, not one. Blocking the command is the weakest of them.
My coding agent runs a handful of read-only helper scripts constantly: the checks that tell it whether the repository is still consistent with itself. Run one by its repo-relative name and the harness matches it against my permission allowlist and lets it through. Run the exact same script by its absolute path and the allowlist misses, so the harness stops and asks me to approve it. Same script, same effect, one prompt I clear by hand every time the agent reaches for the long spelling.
The documented fix is a hook that blocks the absolute form: exit with an error, feed back “use the relative path,” let the agent try again. It works. It also costs a full round trip every time — the agent issues the command, gets refused, re-reads the correction, retypes, runs it again.
My guard does something the refusal can’t. It matches the absolute invocation, rewrites it to the allowlisted relative form, and runs that. One pass, no prompt, no retry. The agent issued a command that would have interrupted me, and got back a clean result as if it had typed the correct thing all along. The whole mechanism is one JSON object returned on a successful exit:
{"hookSpecificOutput": {
"permissionDecision": "allow",
"updatedInput": {"command": "bash scripts/check-foo.sh"}}}permissionDecision: "allow" plus updatedInput tells the harness two things: don’t ask, and run this instead of what the agent typed. The command is mutated in flight. The agent never sees a bump.
That is the top of a ladder most guard hooks never climb. A PreToolUse hook — the thing that runs before every shell command an agent issues — has three distinct responses available to it: block, steer, and rewrite. Nearly every example you will find in the wild uses only the bottom rung. This essay is about the other two, and about why the blocking refusal everyone reaches for first is the weakest tool in the set.
The three rungs
All three live in a single hook script, under two hundred lines of shell, grown one rule at a time.
Block is exit code 2. The harness feeds the hook’s stderr back to the model as an error and stops the tool call; the agent has to try again. This is where the genuine crash-guards live in my hook. Refuse a cache-clearing command while a compile is running, because it corrupts the in-flight build. Refuse a second container build when one is already going, because two at once take the machine down. Refuse a destructive reset mid-operation. Every one of these is legitimate, and every one is the same rung: stop the agent, hand back a sentence, make it reissue. The canonical “block dangerous commands” hook reproduced in every hooks tutorial is exactly this rung, and only this rung.
Steer is exit 0 plus a field called additionalContext. The command is not refused; instead the hook returns a note that the agent reads before it plans its next move. My guard uses this to nudge rather than veto. When the disk is filling ahead of a build, it clears the regenerable cache and, if space is still tight, tells the agent so. When the agent runs a heavy integration suite directly, it advises the wrapper that provisions the environment first. Birgitta Böckeler calls this style of fed-forward guidance “a good kind of prompt injection,” and that is the right frame for the rung: you are shaping the next generation, not halting the current one.
Rewrite is the lead: exit 0, permissionDecision: "allow", and updatedInput. The hook substitutes a corrected command and runs it. No message, no retry, no prompt.
Why block is the weakest rung
Look at what each rung costs the agent. A block stops the bad command and leaves all the friction in place: the agent still has to notice the refusal, parse the correction, and reissue. Steer deletes the noticing cost, because the correction is already in hand when the agent plans its next step. Rewrite deletes the retry cost outright, because there was nothing to correct — the right thing already ran.
So the rung you choose is a friction decision at least as much as a safety one. When a command is genuinely dangerous and has no safe version, block is correct: two concurrent builds, a clean mid-compile, the only acceptable outcome is that the command does not run. But a large share of what a guard actually intercepts is not dangerous at all. It is a command that is right in intent and wrong in form: the correct script by the wrong path, the correct suite without its wrapper. For that class, a refusal is busywork. You already know the correct form. The hook could simply run it.
The deny-hook people start with treats every interception as the dangerous case, because the bottom rung is the only one it has.
A layer the commit-time checks can’t reach
Why does this live in a runtime hook at all, rather than one of the check-* scripts I run before a commit? Because the two see different things. A commit-time check reads files that are about to be recorded. A command that runs and is gone — a stray path, a build that would collide with another — never reaches a commit for a git hook to inspect. The guard’s own header states the boundary plainly: these are “runtime-behaviour rules a git hook can’t catch (they never reach a commit).” It is the same check graph I have written about before, the web of artifacts that pin each other down, with one more layer added: a check that fires before execution rather than before commit.
One design rule holds that layer together: the guard fails open. Any parse problem, any unexpected input, and it exits 0 and gets out of the way. A guard that can hang the tool it guards is worse than no guard, because the failure mode is an agent wedged mid-task with no way forward. The safety it buys is not worth the tail risk of a stuck harness, so the guard is built to never be that.
Growing the ladder
Where do new rungs come from? A logged friction loop. An approved permission prompt is invisible to the agent: the tool result it gets back is identical to one that was auto-allowed. A prompt I clear leaves no trace the agent can act on, so the signal has to be captured where the harness sees it, not where the agent does. Every command that falls through the guard without being handled is appended to a local log. At the end of a work session a scan ranks what actually prompted me, grouped by pattern, and I resolve each recurring one into a fix of one of three kinds: a new allowlist entry, a new guard rule, or a change in my own habits. The middle fix is how the ladder grows a rung — a friction I hit twice becomes a steer or a rewrite the third time.
It is tempting to file this whole apparatus under “fighting permission prompts,” but that mis-frames it. The same hook pushes friction down — rewrite a right-intent command, auto-allow a read-only search pipeline — and pushes friction up, refusing the clean mid-build and the second concurrent build. It is one steering layer with a range of responses, not a campaign against prompts. The ladder is not even specific to shell commands: the block rung lives at the file-write boundary too, where a sibling hook refuses writes that would recreate a retired, ungoverned surface. Same rung, different tool.
What is mine and what isn’t
Almost none of the mechanism is mine. The hooks reference documents all of it: exit 2 feeding stderr back to the model, additionalContext for injecting a note, permissionDecision with updatedInput that “replaces a tool’s arguments before it runs.” The deny-dangerous-commands hook is the canonical worked example, reproduced everywhere. Even the underlying instinct — that a good guard should correct a mistake rather than merely stop it — is decades old. It is poka-yoke, the mistake-proofing discipline from the Toyota Production System, where the best fixture makes the wrong action quietly right, or impossible, instead of sounding an alarm and waiting for a human.
What I am adding is the framing. These three documented responses form a ladder; they trade friction off against each other; and the rung nearly everyone defaults to is the weakest of the three. The rewrite rung especially is documented but, in the guides I have read, almost never taught as what it is: a way to remove friction instead of policing it.
Limits
Rewrite is only safe when the substitution is behavior-preserving. My guard rewrites a script path into a different spelling of the same script — same working directory, same effect, no permission round trip. Rewriting a command into something that does a different thing would be a trap dressed as a convenience, because the agent would lose the ability to trust that what it typed is what ran. So the rewrite rung is deliberately narrow. It fires only for a known set of read-only helpers; a script that is not on that list gets steered with a correction, never silently changed.
The ladder also lives entirely at the command boundary. It shapes what runs and says nothing about whether the result was any good. A rewritten command can still fail on its own merits, and a guard that let a command through has not verified the work that command did. That is a separate axis with its own discipline, and I have argued elsewhere that a green result from an agent is a claim to check, not a fact to trust.
And all of it is harness plumbing. It rides on one vendor’s hook contract, and that contract moves: fields are added, behaviors change, an example that worked last month wants revisiting. The file-write guard I mentioned exists precisely because a platform feature shifted under me and I needed a hard floor under the change. This is not portable governance. It is local ergonomics for the specific tool I use every day, and at that scale it earns its keep several times a session.
Building domain-dense systems by directing coding agents is where I spend my time, and this kind of ergonomics is a small part of what makes it work. If that is your problem too, I am reachable on LinkedIn.
Written by directing an AI agent, the same way the platform it describes was built. The editing and the judgment are mine.
References
“Hooks reference,” Claude Code documentation (accessed 3 Jul 2026).
Birgitta Böckeler, “Maintainability Sensors for Coding Agents,” martinfowler.com, 19 May 2026 (accessed 3 Jul 2026).
“Poka-yoke,” Wikipedia (accessed 3 Jul 2026).
Vasyl Tretiakov, “Verify the Work,” vasyltretiakov.dev, 24 Jun 2026.
Vasyl Tretiakov, “Couple Both Ways,” vasyltretiakov.dev, 9 Jun 2026.
