Stop 11 · Who says yes
The loop wants to run a command. Someone has to decide whether it may. On this stop, that someone is you.
Every model behaviour on this stop is a written illustration, not a live API call. The numbers are computed from the assumptions stated below.
- userthe taskThe member discount in lib/cart.ts is not being applied. Fix it.
- assistantread a filetool_use: read_file("lib/cart.ts") — did not stop to ask
- tooltool_resultline 31: total = subtotal (the discount is computed and then ignored)
- assistantwrite a fileside effecttool_use: write_file("lib/cart.ts", …) — the loop stops herewaiting for approval
Reading and writing are not the same
Look at the first beats of the run above: read_file simply ran, and write_file stopped. Where that line is drawn is worth stating plainly: it is drawn at whether there is a side effect, not at how dangerous something is.
The two get conflated, and they are not the same. Reading a file can be extremely dangerous: read .env and its contents enter the , where they are resent with every later round (stop 8). Writing a file can carry no risk at all: a line appended to a log in a temporary directory. But whether a tool has a side effect can be labelled when the tool is defined — read_file has none, write_file does — while whether a call is dangerous depends on its arguments and its moment and cannot be labelled in advance. Drawing the boundary at the question that can be answered beforehand is the version of this that can actually be built.
The more useful version of the question is reversibility: if this step were wrong, could I take it back? Reading changes nothing in the outside world, so in that sense it has no side effect; but what it read has already been sent to the model and cannot be taken back, which is why reads are governed by scope (see the question below). Writing a file is nearly reversible under version control. Sending an email is not. Moving money is not. Sort your tools by reversibility and the set that genuinely needs a human nod turns out to be much smaller than instinct suggests.
Beginner question: So reads need no control at all?
They need control, of a different kind. Side effects are governed by approval; reads are governed by scope. Deciding which directories this agent can see at all is worth far more than a dialog before every read, because the first is a rule you set once and the second is a button you will get tired of. They solve different problems, and using one for the other usually does neither well.
Three answers, and what each one costs
The three buttons above are, in most systems, the whole menu. They look like three levels of strictness. They are three different promises, and the middle one almost always promises more than people think.
Allow always usually binds to the tool, not to the arguments in front of you. You clicked it while looking at write_file("lib/cart.ts"), so what you approved was write_file — and the later write_file("package.json") no longer passes through you. This is not an oversight in the implementation; the word always simply has no scope of its own. The scope has to be supplied by whoever designs it: a directory, a shape of argument, a window of time.
What the agent sees when you say no
People expect a refusal to make the agent crash or stall. It does not. In the array a refusal is an ordinary tool_result, structurally identical to a failed command or a missing file. The rule from stop 2 applies again: what the model can think of next depends on what that message says.
So a refusal deserves to be written well. Denied on its own tells the model only that this road is closed; it may try another tool, or give up with a vague account of why. Denied: the user did not approve this write, you may output the change as text instead gets you the diff — the task is finished, only delivered differently. A good refusal answers two things at once: no, and here is what you can do.
Beginner question: How does this connect to stop 10?
Directly: approval exists because the text asking for the action may not have come from you. The page at stop 10 can make an agent request an action, and the agent cannot reliably tell which sentence was yours. Approval is the remedy for exactly that confusion — it does not judge who asked, it only insists that something with a side effect gets a nod. Which is also the argument for keeping the scope of an always small: every inch you widen is an inch an injection can use.
There is no correct point on the line
An agent that asks about everything goes unused: after thirty confirmations a person writes a script to get around it. An agent that asks about nothing goes untrusted: the first time it deletes the wrong thing, it never touches production again. Every real system sits somewhere on that line, and where depends on the setting — running tests locally and sending mail to customers should not share a threshold.
A few things hold wherever you sit. Approve actions, not tool names. Show the actual arguments in the prompt, or the person is approving write_file rather than approving writing that file. Give always a scope and an expiry. And the one that matters most: if an action cannot be undone when it is wrong, it should not be guarded by a single click. Make it undoable first — stage it, produce a diff, put it in drafts.
An agent that asks about everything goes unused; an agent that asks about nothing goes untrusted. Every real system is a position on that line. The way to choose one is not to ask how risky a step is, but whether you could undo it if the step were wrong.