So, just another Tuesday. You ask Opus 5 for a one-line fix: a date parser is choking on a timezone suffix, change the format string. Twenty seconds later the agent reports done. You open the diff and it is not one line. It is eleven files. It rewrote the parser, renamed a helper three modules over, tidied an import block you never mentioned, and added a caching layer nobody asked for. Every test is green (actually have 11 more). The check passed, everyone's happy, except you, who now have to decide whether to read four hundred lines to approve a change that was supposed to be one.
The first half of this pair named why that decision is the hard part now. Opus 5 made writing nearly free and moved the whole cost onto review, and the reflex fix, pointing a second model at the first one, rebuilds the problem one guess at a time. That piece stopped at the diagnosis. This one is the other half: what you put in place of the second model, so the eleven-file surprise gets caught by something other than your attention at 6pm.
Anthropic documents that scope blowout as expected behavior. Its own Opus 5 guidance names it plainly: the model "can also expand the scope of a task, adding steps that weren't requested or applying its own judgment about what the task should be." A more capable model that runs longer and finishes more of the job also finishes more of the job you did not ask for. Catching that reliably takes three controls, and they are the ordinary layers of defense in depth: steer the model off the mistake, review what it did, and put a deterministic gate under both. Two of the three keep the mistake out and differ only in how far you can trust them, steering by moving the odds and the gate by refusing outright; review is how you catch what slips between them. What follows names each layer for an agent, ordered by cost rather than by when each runs, but the layered shape itself is the standard one.
Layer one: steering, and the instruction to delete
The instinct, when the agent overreaches, is to write a rule telling it to check itself. Add verify your work before finishing to your CLAUDE.md and move on. On Opus 5 that line costs you and buys nothing. The same Opus 5 guidance is blunt about it: "Claude Opus 5 verifies its own work without being told to. If your prompt contains explicit verification instructions ... remove them," because "instructions like these cause over-verification," and removing them "reduces wasted tokens with no loss in quality." The model already double-checks. Your instruction just makes it burn tokens double-checking louder, and none of that self-checking sees the problem you actually have. A model verifying its own work confirms the eleven files do what the model intended. It has no way to know the model was asked for one.
What belongs in the steering layer is the scope constraint, not the verification order. Anthropic gives you a starting shape for it:
Deliver what was asked, at the scope intended. Make routine judgment
calls yourself, and check in only when different readings of the request
would lead to materially different work. If the request seems mistaken or
a better approach exists, say so in a sentence and continue with the task
as asked rather than quietly narrowing, widening, or transforming it.
Finish the whole task, and stop short of actions that are clearly beyond
what was asked.
That block points the model the right way, and its last line already binds: it names three specific failure modes (narrowing, widening, transforming) and says what to do instead. Two of its other lines lean on words the model has to interpret, and a rule the model interprets is a rule it can set aside. "At the scope intended" asks the model to measure its edits against an intention nobody wrote down as anything checkable. "Check in only when different readings would lead to materially different work" leaves material for the model to define, so the threshold floats. "Stop short of actions clearly beyond what was asked" makes clearly the reader's judgment rather than a line the model can hold an action against.
Give the model a target it can check every edit against instead, starting with the one thing only you can supply: the set of paths this task is allowed to touch.
Write this task's scope as a named set of paths before you change
anything. For this date-parser fix the set is `src/dates/` and its
tests; write your own and keep it in view.
Change only files in the named set. Touch a path outside it only when
the change will not run otherwise, and say so before you edit it.
Make the smallest change that satisfies the request: no caching layer,
no helper rename, no refactoring pass, no new abstraction, even when it
would improve the code.
Check in before starting only when the request could mean different
files, and finish every file in the set the change needs.
If a larger change would help, name it in one sentence and make only the
asked-for one.
Every line now points back at one concrete anchor, the named set, and a rule pinned to a name you wrote down keeps its grip as the context around it grows; an abstract rule fades into it. That is the filter: a line that cannot cite the named set does not earn its place, which is what dropped the vague check-in and the maybe-later hedge from the version above. The block ships pointing at that set; you make it bind by writing your real paths into the top.
That same named set is what Layer three turns into a hard gate, so steering and enforcement guard the same line: one asks the model to stay inside it, the other refuses the write when it strays.
That is steering: it moves the odds that the next diff stays inside the ask. It does not set them to one, because everything on the context surface is an input to a probabilistic process, an instruction the model is free to weigh and set aside on the exact turn it decides the caching layer is a good idea. Steering is the first layer because it is the cheapest and it handles the common case. Lean on it for the routine change, and reach past it for the one you cannot afford to get wrong.
Layer two: procedure, and reviewing what actually ran
When a human does open the diff, the skim is the failure mode the previous article identified as well: the LGTM speedrun, a 300-line diff approved in under three minutes on a gut feeling. The reflex answer is "read more carefully," and it runs out the moment the diff is larger than what fits in your head, which is exactly the diff a scope-expanding agent produces.
A review that scales is a sorting job. Some of what a reviewer checks has a definite answer a rule can settle: whether the suite passes, whether the types hold, whether the change touched a path it was never scoped to, whether a secret landed in the diff. The rest needs a model of the world the change lives in, and only a person has that. The process is to send the first pile to something that runs every time without you, which is Layer three, and spend your own reading on the second.
What is left once the machine-checkable part is routed away is a real review, and two practices carry most of it. Neither trusts the diff on sight.
- Review by executing. Run the code the agent wrote against the case you cared about in the first place. A diff can read as plausible and still behave differently once it runs, so the caching layer it added stays a claim until you exercise it. An executing reviewer, a person or a CI job that runs the change, tests what it actually does.
-
Probe the tests with a mutation. A green suite tells you the tests pass. It does not tell you the tests would fail if the code were wrong. Change one thing the code should care about, revert a guard, flip a comparison, delete a branch, and confirm a test goes red. If nothing does, your tests pass no matter what the code does, and a scope-expanding agent leaves exactly that behind: new code, new tests, all green, and not one test that would notice if the new code were broken. This is mutation testing done by hand; tools like
mutmut,cosmic-ray, andStrykerautomate the same probe, and practitioners reach for it precisely when an agent wrote both the code and the tests that vouch for it.
A model can take the first pass over what remains, in one specific role. Anthropic's note on Opus 5 as a reviewer is worth reading against the "a model checking a model" warning from the first half: the model "reviews code with high precision and recall," and the guidance is to "ask it to report everything and filter in a separate pass." Use it as a fast first reader that surfaces candidates cheaply, with one rule: the reviewer cannot be the author. A model grading its own diff carries the blind spot that produced it, the same reason you do not review your own code by rereading it. A fresh instance of the same model clears the context it sat too close to, though not the blind spots baked into the model itself, so the strongest version of this pass is a different model or a person. Point a fresh reader at the change and filter its findings yourself, because deciding which of them matter, and whether the scope was even right, is judgment a second model cannot hand you.
Layer three: enforcement, the gate that just refuses
The instinct is to put the reviewer last, as the final gate before main. That instinct is the bug. A reviewer, a person skimming at 6pm or a model grading a model, is exactly what you cannot trust as the last word, so the deterministic gate goes under the review, not the reviewer over the gate.
Steering moves the odds; the review reads what steering let through. The third layer runs every machine-checkable question automatically, so no person re-runs it. A rule that runs this way forms no opinion about the diff, so it returns the same verdict on the hundredth change as on the first, however large the change grows.
Most of this layer is enforcement you may already own and may be running as advice instead of as a floor. The test suite, the type checker, the linter, a secret scanner: wire them as required checks in CI and put branch protection in front of the merge, so a failing check refuses the merge rather than leaving a red mark a tired reviewer waves through. Then add the checks the agent's own failure modes call for, a scope guard and a mutation gate among them. That is the outside enforcement, and it runs after the change is written, at the pull request.
One piece of this layer CI cannot give you, because CI only sees the change after it is written: a gate that fires inside the agent loop, before the write lands. That is the one that fits the scope blowout.
The gate for an agent that touches files it was never asked to touch is a blast-radius guard: refuse a write to any path outside the set this task was scoped to. Claude Code fires a PreToolUse hook before a tool call executes, and an exit code of 2 blocks the call and hands your message back to the model (per the hooks docs). Save this as .claude/hooks/guard-blast-radius.sh and make it executable:
#!/usr/bin/env bash
# guard-blast-radius.sh: refuse a Write/Edit/MultiEdit outside the files this
# task was scoped to. Runs as a Claude Code PreToolUse hook. Exit 2 blocks it.
# Requires jq.
payload=$(cat) # the pending tool call arrives as JSON on stdin
path=$(jq -r '.tool_input.file_path // ""' <<<"$payload")
cwd=$(jq -r '.cwd // ""' <<<"$payload")
# Claude Code passes file_path relative to cwd, but strip cwd anyway so the
# allow-list holds whether the path arrives relative or absolute.
rel="${path#"$cwd"/}"
# Reject .. traversal, or src/dates/../../etc/passwd slips through the prefix.
case "$rel" in *..*) echo "blocked: '$path' uses .. path traversal." >&2; exit 2;; esac
# The blast radius for this task: the only paths a write is allowed to touch.
# Keep it somewhere you edit per task, or read it from a file.
allowed='^(src/dates/|tests/dates/)'
[ -z "$path" ] && exit 0 # no path in this call, nothing to guard
if ! grep -qE "$allowed" <<<"$rel"; then
echo "blocked: '$path' is outside this task's blast radius ($allowed). \
If the change genuinely needs this file, widen the scope on purpose, not in passing." >&2
exit 2 # exit 2 == hard block; the stderr message goes back to the model
fi
exit 0
Wire it to the write tools in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{ "type": "command", "command": ".claude/hooks/guard-blast-radius.sh" }
]
}
]
}
}
Now the agent that was asked to fix a date parser tries to rename a helper three modules over. The transition fires before the write, the path fails the allow-list, and the write never lands. The model sees the refusal in the same turn and has to either stay in scope or say out loud that it needs to widen it, which is the conversation you wanted to have before eleven files changed, not after. The gate ignores the diff's contents and its size alike. It checks one path rule, the same way on the thousandth call as on the first.
One line in that script carries the point: the allowed set is yours to write, never the model's. The tempting shortcut is to let the agent write its own blast radius and have the hook read it back, but a boundary the model can edit is not a gate, it is one more instruction it can set aside. The set has to come from a human who knows what this task is allowed to touch.
Two honest limits sit on this gate. It matches the file-editing tools, Write, Edit, and MultiEdit; an agent that writes by shelling out, a sed -i or a > redirect, goes around it until you put the same rule on a Bash matcher or refuse writes from the shell. And it guards where the change lands, not what it does: the stray file it stops from being touched is the cheap failure, while a wrong line inside a file you did ask it to change is Layer two's job, never the gate's.
Advisory or blocking
A gate has two settings, and the difference is whether it can be argued with. Swap the exit 2 for a printed line and exit 0, and you have an advisory layer: it tells you the agent stepped outside the lines and trusts you to notice. Keep the exit 2 and you have a floor the model cannot route around. Advisory is the honest default while you are still learning where your own blast radius is, because a gate keyed too broadly refuses clean work, and a false block is work that cannot proceed until someone fixes the gate. Run it advisory first, watch what it would have blocked over a week, and promote the rule to a hard block once you trust it refuses only what you meant. A layer-three control that only warns is real work; the promotion to one that refuses is a separate decision you make once the evidence is in.
What none of the three can do
Be exact about the boundary, because this is easy to oversell. A gate checks the diff against a rule you wrote, and it has no model of reality to check against. Whether the caching layer was worth adding, whether the rename makes the next change cheaper or the codebase worse, whether the one-line fix was even the right fix for the bug you actually had: each of those needs a model of the world the change lives in, the product, the people using it, where the whole thing is headed. None of the three layers has that. Steering can ask for it and procedure can surface candidates, but the judgment itself stays with you.
The enforcement layer is rarely one hook. It is a set of them you design for your own blast radius, your own must-hold constraints, the paths a change should never cross, the same move as writing the architecture rules you already keep for your codebase, one level down. You are deciding, once and in advance, which of the agent's choices are yours to make and which it can make on its own.
Read the rules once, gate the rest
Three layers: the steering you write, the procedure you run, the gate you own. We run all three on our own agents, for the same reason you would: the agent is fast enough now that no single reader at the end of the day is the thing standing between its output and main.
You reached for Opus 5 to write more and decide less. Pointing a second model at the first one feels like progress and quietly hands the deciding back to a guess. The way to actually spend less of your attention is to hand the machine-checkable parts to the machine: scope, format, a forbidden path, a test that no longer bites. What is left is the one question none of the three layers can answer, whether the change was worth making at all. That is the review the eleven-file diff still needs, and it was always the point. The layers do not shrink that review; they clear the noise around it, so the judgment is what is left on your desk instead of being buried under four hundred lines.
I work on Reporails, deterministic diagnostics and governance for the instruction files, rules, and prompts that steer coding agents. It reads the steering surface you wrote down and tells you, with measured evidence, which rules couple to behavior and which are text the model can ignore.






Top comments (0)