Approval Fatigue: The Control Degrades Every Time You Use It
Nobody decides to stop reading the prompts. Forty identical safe decisions train a keystroke that means continue.
A control whose strength is somebody's attention
Interactive approval is the most legible safety mechanism an agent can have. Before it writes a file or runs a command, it stops and asks. Nothing happens that a person did not permit. Described that way it sounds airtight.
It is airtight for the first twenty prompts. The property that makes it work — a human reading the command and deciding — is a consumable resource, and every prompt spends some. By the fortieth approval in an afternoon, the reading has stopped and the pressing has not. The control is still there in the transcript. It is no longer a control.
This is not a criticism of the mechanism, which is the right default. It is an argument that the number of prompts is the design parameter, and almost nobody treats it as one.
The failure is predictable and has a shape
Nobody decides to stop reading. What happens is that the prompts are overwhelmingly routine — list a directory, read a file, run the formatter, run the tests — and a long run of identical safe decisions trains a fast response. The approval becomes a keystroke that means "continue", because that is what it has meant forty times in a row.
Then a prompt arrives that is not routine, formatted identically, in the same position on screen, during the same flow state. It gets the same keystroke. Afterwards the transcript shows an explicit human approval of the exact thing nobody would have approved if they had read it.
What people do when the volume gets high
Two adaptations, both worse than the problem. The first is switching to a mode that stops asking, usually late in a session when the interruptions have become intolerable — which removes the control entirely, at precisely the moment the work has become complicated enough to need it.
The second is quieter: adding a broad allow rule to make one recurring prompt go away. That is fine when the rule is narrow and considered. It is usually neither, because it is written while irritated, and a rule written to silence a prompt tends to be as wide as it takes.
Go and read your permission configuration now. The entries added deliberately are narrow. The entries added to stop an interruption are broad, and you will recognise them immediately by how little thought went into the pattern.
Reduce the count before improving the prompt
The instinct is to make prompts more informative — more context, better formatting, a summary of what will happen. That helps a little and does not address the mechanism, because the problem is not that any individual prompt is unclear. It is that there are too many for any of them to be salient.
Halving the count does more for safety than doubling the information in each. The target is a volume at which a prompt is unusual enough to be read, which in practice means single digits per hour rather than dozens.
Allow the boring majority explicitly
Most of the volume is read-only inspection and idempotent local commands. Those deserve a considered blanket approval, written once, rather than an approval each time.
{
"permissions": {
"allow": [
"Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)",
"Bash(rg:*)", "Bash(ls:*)", "Bash(cat:*)",
"Bash(npm run test:*)", "Bash(npm run lint:*)",
"Read(./src/**)", "Read(./tests/**)"
],
"deny": [
"Bash(git push --force:*)", "Bash(git reset --hard:*)",
"Bash(git clean:*)", "Bash(rm -rf:*)",
"Bash(curl:*)", "Bash(docker system prune:*)",
"Read(./.env)", "Read(./.env.*)", "Read(~/.ssh/**)",
"Read(~/.aws/**)"
]
}
}
The allow list buys back attention. The deny list is the part that matters more, and it is the part most configurations omit entirely.
Deny is stronger than allow, for a structural reason
An allow list has to anticipate everything safe, and anything it misses becomes a prompt, so pressure pushes it wider over time. A deny list has to anticipate the things that are never acceptable, which is a much shorter and more stable list, and pressure does not push on it at all, because nothing in daily work is blocked by it.
There is also a composition problem that allow lists do not solve. Approving
ls is safe. It does not follow that a command line beginning with
ls is safe, because a shell line can chain, pipe, substitute and
redirect. Pattern-matching the front of a command line and concluding
something about the whole line is the failure mode
shell execution blast radius
covers in more depth, and it is the reason to keep allow patterns anchored to
whole invocations rather than prefixes wherever the tooling permits.
The prompt cannot show you consequence
This is the limitation underneath all of the above. A confirmation dialog
for git status and one for git push --force origin
main are the same size, in the same place, with the same buttons. The
interface renders the command; it does not render what the command does.
Severity is not visible, so it has to be encoded. Anything genuinely irreversible should not be arriving as an ordinary approval at all — it should be denied outright, or routed through something that looks different enough to break the rhythm. Which actions belong in that category is worked through in gating irreversible actions; the point here is that the ordinary prompt is the wrong vehicle for them regardless of how carefully it is written.
Make the sharp ones look different
A hook can intercept a command before it runs and change what the human sees — add a warning, print what will be lost, or refuse. That is a different signal channel from the standard prompt, and different is the whole point.
#!/usr/bin/env bash
# PreToolUse hook: refuse the unrecoverable, and make the rest conspicuous.
input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
case "$cmd" in
*"git push --force"*|*"git push -f"*)
jq -n '{hookSpecificOutput: {hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "force push is denied; open a PR"}}'
exit 0 ;;
*"git reset --hard"*|*"git clean -fd"*)
n=$(git status --porcelain | wc -l | tr -d ' ')
echo "DESTRUCTIVE: discards $n uncommitted file(s)" >&2
;;
esac
echo '{}'
The count in that warning is doing the real work. "This discards 14 uncommitted files" is information a human can act on. "Run git reset --hard?" is a command they already read as routine.
Batching destroys the signal
An approval covering a plan of nine steps is not nine approvals. It is one decision about a summary, and the summary was written by the thing asking for permission. That is fine for low-stakes sequences and it should not be how a destructive step gets authorised, because the destructive step was described in a clause the reader skimmed.
The workable rule: batch the routine freely, and let anything on the deny or warn list break the batch and arrive on its own.
Timing works against you
Prompts arrive mid-task, while the person is thinking about the problem rather than about permissions. That is the worst possible moment for a security decision and it is structurally unavoidable, which is another reason to move the important decisions out of the interactive path entirely and into configuration written when nobody was mid-flow.
Configuration is a decision made calmly, once, and applied consistently. A prompt is a decision made under interruption, repeatedly, by someone who wants to get back to what they were doing. Put the things that matter in the first category.
Solo laptop versus shared infrastructure
On your own machine with everything in version control and pushed, the real blast radius of most mistakes is an hour. Prompts can be relatively sparse because recovery is cheap.
Anything touching shared state changes the calculation completely: a staging database, a deployment, a production credential, a shared branch. There, recovery is not an hour and the mistake is not yours alone. The approval volume should be near zero and the sharp actions should be denied outright, with the human step being a deliberate separate action rather than a prompt in a coding session.
Where prompts genuinely work
For the unusual. A first write to a file outside the project. A network call to a host the agent has not touched before. Installing something global. Anything that has not happened in this session yet.
Novelty is the property that makes a prompt readable, and it is available to build on: an approval system that asks about first occurrences and stays quiet about repetitions produces few prompts, and every one of them is interesting. That is the shape worth aiming at, and it is nearly the opposite of asking about everything equally.
Count your prompts before changing anything
The argument in this article is quantitative, so it is worth measuring rather than estimating. Most people are surprised by the number, in both directions: some are approving four things an hour and worrying about a problem they do not have, others are past sixty and had not noticed.
A hook that logs every permission request with a timestamp gives you the distribution in a week. What you want from it is not the total but the shape: which commands generate most of the volume, and whether the count spikes late in long sessions. Both point directly at what to allow-list, and the second tells you whether fatigue is actually your failure mode or whether you are solving a problem you read about.
Repetition is the signal to automate, not to approve faster
Any command you have approved more than five times in a week is telling you something. Either it is safe and routine, in which case it belongs in the allow list, or it is not routine and it should not be happening five times a week without someone examining why.
That framing makes the allow list a product of observation rather than irritation, which changes what ends up in it. A rule added because the logs show forty identical approvals is narrow and specific. A rule added at the fortieth prompt is whatever made the prompt stop.
The transcript is not consent, and it will be read as consent
Worth stating plainly because it has consequences beyond the technical. When something goes wrong, the record shows a human approval immediately before the damaging action. That is what an incident review sees, what a compliance process sees, and what the person themselves sees.
It is also, often, not what happened: the approval was a reflex trained by thirty-nine preceding decisions. Nobody is lying, and the artefact still implies deliberation that did not occur. If your organisation treats approval records as evidence of review — and most do implicitly — then the approval volume is not just a safety parameter, it is the thing that decides whether your records mean anything.
Designing for the assumption that it will be pressed
The robust position is to assume every prompt will be approved and ask whether the system is still acceptable. That sounds defeatist and it is just honest: the prompts you rely on are the ones most likely to be answered automatically, because reliance implies frequency.
Under that assumption the design changes. Irreversible actions get denied in configuration rather than gated by a prompt, because a denial does not depend on attention. Recoverable actions get prompts, because if one is approved by reflex the cost is an hour. And the things in between get a hook that makes them look different, on the theory that breaking the visual rhythm is the only reliable way to interrupt a trained response.
What a healthy setup looks like from the outside
Few prompts, most of them about something that has not happened before in this session. A deny list of a dozen entries that has not needed changing in months. An allow list assembled from logged repetition rather than from frustration. A hook that annotates two or three genuinely destructive commands with what they will destroy. And no mode that turns approvals off, because the volume never got high enough to want one.
That configuration takes an afternoon and is mostly a matter of writing down decisions you have already made. The version that fails is not the one with a bad rule; it is the one where nobody chose, so the volume grew until somebody chose the only option available in the moment, which was to stop being asked.
Interactive approval is a control that spends attention, and at forty prompts an afternoon the attention is gone while the transcript still records consent. Treat prompt volume as the design parameter: allow the read-only majority explicitly, deny the short list of never-acceptable actions outright, and use a hook to make destructive commands look different and state what will be lost. Do not let irreversible actions arrive as ordinary prompts, and do not let a nine-step batch approval carry one of them.