[SECURITY-BUG ] Permission prompt is silently replaced by a newer pending approval (LIFO stack), allowing the wrong command to be approved in manual-approve mode
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
In manual-approve mode with multiple agents proposing commands concurrently, pending approval prompts behave like a LIFO stack rather than a stable queue. When a new command needing approval arrives, it is pushed on top and replaces the currently displayed prompt. After approving the top item, the view pops back to the previous one; any newly arrived item again jumps to the top.
The replacement is asynchronous — it's driven by when other agents produce commands, not by my input. That's where the danger lies. There is a window between when I've finished reviewing the displayed command, mentally committed to approving it, and physically pressing the approve key. If a newer command arrives and replaces the prompt during that window, my approval lands on the swapped-in command — one I never reviewed — instead of the one I read and decided on.
Crucially, the swap lands inside that decision-to-action window but too late for me to catch it. By the time the prompt has changed, I've already committed to the keystroke and am physically executing it; there is no opportunity to perceive the change, pause, and abort. The motor action is already underway. So the swap doesn't give me a chance to react — it just silently redirects an approval I'd already committed to a command I never saw.
To be clear: this is not the system deterministically intercepting a keystroke mid-press at the compute level. It's a human-timing race. The UI can mutate the prompt's target in the gap between human decision and human action, and in practice it does — this has happened to me multiple times.
This breaks the core contract of manual-approve mode: the prompt is supposed to guarantee that "yes" applies to the exact command displayed when I decided to approve it. If a concurrent agent can swap the payload during my reaction window, manual approval provides no real safety — it's a TOCTOU (time-of-check vs. time-of-use) gap on the approval gate itself.
I've observed this with multiple agents suggesting commands. I can't confirm whether it also occurs with the main thread alone — I don't recall hitting it there.
What Should Happen?
A permission prompt's target command should be immutable from the moment it's displayed until the user accepts or rejects it. A newly arrived approval should never replace or reorder a prompt the user is currently looking at. Specifically:
- The displayed prompt should be locked to one command; the next action only ever applies to the command currently shown.
- New pending approvals should queue behind the current one (ideally with an "N pending" indicator), never jump on top of it.
- When the current item is resolved and the next one is shown, it should require a fresh, deliberate confirmation, so the user always re-reads before approving.
Error Messages/Logs
Steps to Reproduce
Note: this is timing-dependent and won't reproduce on every approval, but it recurs reliably in practice.
- Start Claude Code (v2.1.167) in manual-approve mode (default permission mode, no auto-accept/bypass).
- Run multiple agents/subagents concurrently, each producing commands that require approval (e.g., Bash calls not on the allow list).
- Let approvals accumulate so prompts begin stacking — observe that each newly arrived command replaces the currently displayed prompt and the previous ones queue beneath it.
- Review the displayed command and decide to approve it.
- Note that new commands keep arriving asynchronously and can replace the displayed prompt at any moment, independent of your input — including during the brief window after you've committed to approving and are physically pressing the approve key.
- When a replacement lands in that window, it arrives too late for you to perceive it and abort — the keystroke is already underway — so the approval applies to the newly swapped-in command rather than the one you reviewed. The view then pops back to the previously pending item.
Claude Model
None
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
v2.1.167
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Other
Additional Information
_No response_
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
The user-side layer that holds even before this is fixed: a
PreToolUsehook closes the same TOCTOU gap from the other end. Your analysis is that approval is checked at time-of-display but the command can mutate before time-of-use. APreToolUsehook evaluates the command at time-of-use — the instant before it executes — and can hard-block it (exit 2) regardless of what was on screen or what you approved. So even if the racy swap lands your "yes" on a command you never read, a hook that gates dangerous operations (recursive deletes, destructive/irreversible ops, writes to sensitive paths) stops the dangerous ones deterministically, without depending on you having read the right prompt.It doesn't fix the bug — prompt immutability is the correct fix and your three requirements are the right spec. But it changes manual-approve from "the human is the only gate" to "human plus a content-based gate at execution time," which is robust to this race (and to ordinary misreads) because it re-checks the actual payload at the last moment:
…where the hook reads the real
tool_input.commandandexit 2s on the operations you never want auto-running. Free/MIT guards for exactly this class (banned commands, rm safety net, destructive-op and sensitive-path blocks) are maintained in cc-safe-setup if you'd rather not hand-roll and keep the patterns current.One concurrency note for the interim: the swap only happens because approvals stack. Until it's fixed, an allowlist for genuinely-safe commands (so fewer prompts queue) and/or running approval-gated agents less concurrently shrinks the race window — but the hook is the layer that actually holds when the window doesn't close in time.
This in addition to the Security Contract breach, is an embarrassingly (for a company of this scale) poor UX. You start reading and analyzing a command, half way through you are thrown a new one. So you start reviewing the new one, immediately before implementing your decision BOOM - in lands another one.
I would frame this as a TOCTOU bug in the approval binding: the user reviewed one command, but the approval action can attach to a different command before the keystroke lands.
The fix should probably make the displayed approval item immutable until it is resolved. New approvals can queue, but they should not replace the active approval target.
A verifier-friendly shape would be:
The UI action should submit the
approval_prompt_idfor the item currently displayed. If the active item changed, or the command digest no longer matches what was displayed, the approval should be rejected and the user should get a fresh prompt.Regression tests I would want:
N pendingindicator but does not steal the active approval target.That keeps manual approval bound to the exact command the human reviewed, rather than to whichever pending item happened to be on top when the keystroke arrived.