Human-in-the-Loop: Where to Put the Approval Gate
5 min read · updated August 3, 2026
Every agent that touches production acquires an approval step. Most of them acquire a useless one, because the gate was placed where it was easy to add rather than where a human could actually catch something, and because it fires often enough that approving is a reflex.
The gate that is always approved
A gate has one job: make a bad action less likely. It fails when the approver stops reading, and the approver stops reading when the gate fires often and is nearly always fine. This is not a discipline problem; it is a design consequence, and it is measurable — count approvals, count rejections, and if the rejection rate is under a percent or two over a few hundred prompts, the gate is decorative.
Which yields the first rule: a gate that cannot be rejected is worse than no gate, because it manufactures an audit trail of human consent for actions no human evaluated. Fewer, richer gates beat many thin ones.
Four properties of blast radius
Score each tool on four axes at definition time. The score, not the vibe, decides the gate:
| Property | Description |
|---|---|
| Reversibility | Can the effect be undone by a subsequent action, with the same tools, within minutes? Editing a file in a git working tree: yes. Sending an email: no. Deleting a production row without soft-delete: no. This is the dominant axis. |
| Externality | Does anyone outside your system observe it? A customer email, a public post, a webhook to a partner, a payment. External actions cannot be reversed by fixing your state, only by an apology. |
| Breadth | One record or all of them? The same delete_users tool is a routine action with a single id and an incident with a filter expression. Breadth is often a property of the arguments, not the tool -- gate on the arguments. |
| Cost | Money spent, compute provisioned, rate limit consumed. The only axis with a natural threshold, which makes it the easiest to automate: gate above a number, allow below it. |
Breadth deserves emphasis because it is the one people get wrong. A gate defined per tool waves through delete_rows with where: '1=1' if it waved through the same tool ten minutes earlier with a single id. Gate predicates should read the arguments: “requires approval when the affected count exceeds 10, or the filter is not a primary key”.
Where the gate goes
- No gate — irreversible-but-trivial and everything reversible. Reads, searches, file edits in a scratch workspace, anything in a sandbox that gets thrown away. Gating these is how you train approvers to stop reading.
- Diff preview — reversible but broad. Show the change, let a human accept it, and make the underlying operation atomic so accepting applies all of it. Code changes and configuration edits belong here. The essential detail is that the human sees the actual diff, not the agent’s description of it.
- Per-action approval — irreversible or external. One prompt, one action, with the exact arguments shown. The run blocks. Payments, emails, deploys, deletes.
- Post-hoc notification with undo — reversible, external, frequent. Where blocking would destroy the product’s usefulness and a short undo window is genuinely available. A queued message with a 30-second cancel is a real control; a notification with no undo is not a gate at all, it is a log line.
- Hard refusal — no gate, no approval. Some actions should not be reachable by an agent, and the correct implementation is that the tool does not exist and the credential lacks the permission. “Ask a human first” is not a security boundary; it is a prompt, and prompts can be talked around.
Designing the prompt a human sees
Given that you are spending a human’s attention, spend it on the right thing. An approval prompt should show:
- The literal arguments, formatted, not summarised. If the agent is sending an email, show the email — recipient, subject, body. A model’s summary of what it is about to do is generated by the same process you are trying to check.
- The scope. “This will affect 1,284 rows” — computed by your code from a dry run, not asserted by the agent. The single highest-value line in most approval prompts.
- Why now. The two or three preceding steps, so the reviewer can see what led here. An action can be individually reasonable and wrong in context.
- What happens on rejection. Does the run halt, or does the agent get told no and continue? Both are valid; the reviewer should know which button they are pressing.
Reject with a reason, and pass it back as the tool result: REJECTED by operator: do not email customers directly; draft it for the support queue instead. The model adapts and usually finds the sanctioned path. A bare “denied” produces a retry of the same action with slightly different wording, which is the approval-loop version of the repeat problem in error recovery.
One implementation detail with outsized value: bind the approval to the exact arguments, not to the tool. Hash the canonical arguments, issue the approval against that hash, and have the executor verify it. This makes it structurally impossible for an approved send_email(to: 'ops@…') to be executed as send_email(to: 'all-customers@…') because of a retry, a resumed run, or a race — and it gives you an audit record that says what was approved rather than that something was.
Approving a plan is not approving the actions
The most attractive gate design is also the weakest: have the agent propose a plan, have a human approve the plan, then let it run unsupervised. It is attractive because it costs one interaction. It is weak because the plan is prose and the actions are not, and nothing binds them. “Clean up stale test accounts” is approvable and tells you nothing about the WHERE clause that will be generated at step 14.
Plan approval is genuinely useful for the thing it can do — catching a misunderstood objective before any budget is spent — and should be treated as scope confirmation rather than authorisation. Keep the action-level gates for the irreversible steps underneath it. The two are complementary and neither substitutes for the other.
Finally, decide the escape hatch deliberately. Every agent platform ends up with a flag that turns the gates off for a trusted, sandboxed context, because gates in a throwaway container are pure friction. Name it honestly, scope it to environments where the blast radius is genuinely zero, and log every run that used it. The alternative is not a safer system — it is the same flag, undocumented, in someone’s shell history.