Approval UX fails the moment the human stops reading the payload.
A confirmation dialog is only oversight if a human meaningfully evaluates what they approve. Most deployed approval flows do not clear that bar: they ask too often, about the wrong things, with payloads no one reads, and produce the well-documented failure mode of confirmation fatigue — users blindly clicking Approve to clear notifications. This essay is about designing gates that route the rare consequential decision to a human who actually looks, and get out of the way otherwise.
Tier gates by consequence and reversibility, not by action type.
"Confirm before any write" is the anti-pattern that manufactures fatigue. The dimension that matters is not what kind of action it is but how bad the worst case is and how cheaply it can be undone. A useful three-tier model:
- Auto — reversible, low blast radius, in-distribution. Execute and log; no prompt. (Drafting, read-only queries, idempotent retries.)
- Notify — consequential but reversible. Execute, but surface prominently with one-click undo. (Sending an internal message, creating a ticket.)
- Gate — irreversible or high blast radius. Block on explicit human approval. (Money movement, external email, deletes, production deploys.)
The oversight level is a property of the decision, computed from risk and context — not a static flag on the tool.
Pin the approved payload, or you are confirming a different action than the one that runs.
The most common HITL bug in production: the approval UI shows the user one set of arguments, but the agent re-runs an LLM call on resume and the args mutate between approval and execution. The user approved an email to alice@; the executed call went to all-staff@. The fix is structural — hash the exact payload at interrupt time, show that payload, and refuse to execute if the hash drifts.
# Approve the bytes that will run, not a regenerated paraphrase payload = tool_call.freeze() h = sha256(payload) decision = await human.review(payload, digest=h) if decision.approved and sha256(payload) == h: await execute(payload) else: await abort("payload drifted or rejected")
If your agent re-plans on resume, "approved" means nothing without payload pinning. Treat a hash mismatch as a hard failure, never as a re-prompt.
Batch and defer to fight decision fatigue.
Ten approvals in a row degrade into ten reflexive clicks. Reduce the count of decisions, not just their friction: batch homogeneous low-stakes actions into one reviewable list ("approve all 12 of these label changes"), and defer interrupts so the agent does parallelizable safe work first and presents one consolidated decision point instead of stuttering. The metric to drive down is approvals per completed task; the metric to protect is time spent on each gated payload — a gate the user clears in under a second is not a gate.
Defaults are the real policy — choose them adversarially.
Whatever the dialog pre-selects is what a fatigued user will choose. So the default is your safety policy, regardless of what the docs say. Two rules: (1) the default must be the safe, reversible option, never the destructive one, even though that costs a click on the common path; (2) never put a destructive primary button under the keyboard-Enter / muscle-memory position. Design the default for the user who isn't reading, because under load that is every user.
Irreversible actions need a different interaction, not a louder dialog.
For genuinely unrecoverable actions (wire transfer, prod data delete, sending to a customer), a yes/no modal is structurally too weak — it is the same gesture the user has been reflexively approving all day. Escalate the modality: require re-typing the target ("type the table name to confirm"), enforce a cool-down delay that makes the consequence concrete, or require a second approver for the highest tier. Better still, engineer reversibility in first — a 30-second send delay with undo converts most "irreversible" actions into reversible ones and removes the need for the heavy gate entirely.
Before adding a scarier confirmation, ask whether you can make the action reversible instead. A soft-delete plus undo beats the strongest modal that still gets reflex-clicked.
When NOT to add an approval gate.
If the action is cheaply reversible and low blast radius, a gate doesn't add safety — it adds fatigue that you then pay back on the gate that actually mattered.