[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

Status Fixed / completed
Maintainer reply None cached
Activity 4 comments · opened Jun 6, 2026 · closed Aug 17, 2026

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.

  1. Start Claude Code (v2.1.167) in manual-approve mode (default permission mode, no auto-accept/bypass).
  2. Run multiple agents/subagents concurrently, each producing commands that require approval (e.g., Bash calls not on the allow list).
  3. 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.
  4. Review the displayed command and decide to approve it.
  5. 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.
  6. 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_

View original on GitHub ↗

3 Comments

yurukusa · 2 months ago

The user-side layer that holds even before this is fixed: a PreToolUse hook 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. A PreToolUse hook 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:

{ "hooks": { "PreToolUse": [ { "matcher": "Bash",
  "hooks": [ { "type": "command", "command": "bash /path/to/dangerous-command-guard.sh" } ] } ] } }

…where the hook reads the real tool_input.command and exit 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.

Aron-Lomner · 2 months ago

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.

rpelevin · 2 months ago

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:

  • approval prompt id;
  • tool kind, such as Bash;
  • exact command or command digest;
  • working directory / permission context;
  • requesting agent or subagent id;
  • displayed_at timestamp;
  • decision: approved, denied, expired, cancelled;
  • decided_at timestamp.

The UI action should submit the approval_prompt_id for 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:

  1. when two approval-required commands arrive, the first displayed prompt remains active until approved or denied.
  2. a new pending command increments an N pending indicator but does not steal the active approval target.
  3. pressing approve resolves only the displayed prompt id and command digest.
  4. if the command, cwd, agent id, or permission context changes after display, the old approval is invalid.
  5. resolving one prompt then shows the next queued prompt and requires a fresh decision.

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.

Showing cached comments. Read the full discussion on GitHub ↗