Command Allowlist: Always run widens the allowlist to a bare tool name without unblocking the command it was attached to

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

In my allowlist I keep a set of narrow, read-only git entries: git status, git log, git diff, git ls-tree, git rev-parse and so on. I deliberately keep a bare git entry off the list, because git covers git push --force, git reset --hard and git clean -fdx.

Clicking Always run on a command the allowlist already covers replaces that curation with a bare git entry.

git status -sb 2>$null is held for approval. Its only executable command is git status -sb, which two existing entries already match and which runs unattended without the redirect. The redirect is the sole reason for the prompt. The approval card’s only allowlist offer is Always Run ‘git’, and the hover confirms it: “Will allow: - git”. Clicking it stores git.

Two separate things are wrong with that entry.

It is strictly broader than the entry that already matched. git status covered this command. git covers every git subcommand there is.

It does not unblock the command. Reissuing the identical command with git now on the allowlist prompts again, because no allowlist entry can authorize a redirect to a file.

So the click’s only lasting effect is to widen the allowlist. Nothing is offered for the redirect, which is the part that actually needs an answer, and nothing needs to be offered for the command, which was already allowed.

Command Result
git status -sb Runs unattended, matched to the existing git status entry
git status -sb 2>$null Prompts. The only allowlist offer is “Always Run ‘git’”
…click Always run git is stored. Nothing is stored for the redirect
git status -sb 2>$null again Prompts again, despite the new entry

Reproduced twice on 3.14.7, with the git entry deleted in between.

Steps to Reproduce

Confirmed on 3.14.7 (Windows, PowerShell), Run Mode Allowlist. No hooks or extensions are needed.

  1. Open Settings → Agents → Approvals & Execution. Set Run Mode to Allowlist.
  2. Under Command allowlist, add git status. Confirm there is no bare git entry.
  3. Ask the agent to run git status -sb. → Runs with no prompt, as it should.
  4. Ask the agent to run git status -sb 2>$null. → An approval card appears. The only allowlist control reads Always Run ‘git’, and hovering it shows “Will allow: - git”. Nothing offers anything for the redirect.
  5. Click Always run. → Settings now shows a bare git entry. Nothing was added for the redirect.
  6. Ask the agent to run git status -sb 2>$null again. → It prompts again. The entry the button just added does not make the command auto-run.
  7. The new entry is unbounded in its arguments, so it now covers every git subcommand, git push --force and git reset --hard included. See the breadth measurement below if you want that step done without touching a repository.

Expected Behavior

  1. Candidates should be filtered against the effective allowlist. If git status already matches the command, there is nothing to offer for that command and it should not appear as a candidate.
  2. When the reason for the prompt is something the allowlist cannot express, the Always run control should not be offered. A redirect to a file provably cannot be auto-approved by any entry, so the button here can only widen the allowlist while leaving the command blocked. Hiding or disabling it, with a line saying why, is more useful than an offer that cannot work.

Operating System

Windows 10/11

Version Information

Version: 3.14.7 (system setup)
VS Code Extension API: 1.128.0
Commit: a758f2241ca99fecf380180b6cbdbbce0f1f42c0
Date: 2026-07-30T06:41:34.009Z
Layout: IDE
Build Type: Stable
Release Track: Default
Electron: 40.10.3
Chromium: 144.0.7559.236
Node.js: 24.15.0
V8: 14.4.258.32-electron.0
xterm.js: 6.1.0-beta.291
OS: Windows_NT x64 10.0.26200

For AI issues: which model did you use?

Not model-specific — this is terminal approval logic rather than model behavior.

For AI issues: add Request ID with privacy disabled

N/A

Additional Information

Mechanism. All of this is in resources/app/extensions/cursor-agent-exec/dist/main.js (3.14.7), with the storage half in resources/app/out/vs/workbench/workbench.desktop.main.js.

An already-allowlisted command is reclassified as unapproved purely because of the redirect, since the allowlist check is gated on redirect safety before it ever consults the entries:

async cmdIsAllowedByAllowlist(e, t) {
    return !!t.redirectsAllowlistSafe && (!!await this.isInShellAllowlist(e) || t.applyTeamAllowlist && void 0 !== t.autoRunControls && this.isInTeamAllowlist(t.autoRunControls, e))
}

git status -sb therefore lands in unapprovedCommands, and the candidate generator is run over it:

generateAllowlistPatterns(e) {
    const t = [], n = ["git","npm","yarn","pnpm","docker","pip","systemctl","uv","cargo","bun","npx"];
    for (const r of e)
        Al(r.name) || (n.includes(r.name) && r.args.length > 0 && "word" === r.args[0].type
            ? t.push(`${r.name} ${r.args[0].value}`)
            : t.push(r.name));
    return [...new Set(t)]
}

There is a branch there that would produce git status, but it needs args[0]. When the smart allowlist is active the commands have already been converted out of the classifier’s shape by Hl, which reads only arguments and discards subcommandTokens:

function Hl(e) {
    const t = e.name.trim();
    const n = e.arguments.map(e => e.trim()).filter(Boolean).map(e => new x.r6({ type: "word", value: e }));
    return new x.Pv({ name: t, args: n, fullText: [t, ...n.map(e => e.value)].join(" ").trim() })
}

With the subcommand carried in subcommandTokens, args comes back empty, the eleven-tool branch cannot fire, and the fallback t.push(r.name) yields the bare tool name. That is my reading of why git rather than git status is offered; only the outcome was measured.

Al is worth pointing at, because the “too broad to offer” concept already exists in this code. It suppresses a candidate whose name is a shell interpreter, drawn from the hardcoded set ["ash","bash","csh","dash","fish","ksh","powershell","pwsh","sh","tcsh","zsh","cmd"]. node, python, perl and ruby are not in it, so approving one specific script files a bare interpreter entry and grants arbitrary code execution. Generalizing that same instinct — never offer an entry the allowlist already covers, and never offer one broader than the command approved — is the fix for all of these at once.

Breadth of a bare entry filed this way, measured separately. Same version, same card, in a scratch directory so nothing was at risk. Remove-Item C:\Temp\probe.txt -WhatIf prompted; Always run stored Remove-Item, discarding both the path and the -WhatIf that made the command a simulation rather than a deletion. Immediately afterwards, Remove-Item C:\Temp\other.txt — a real deletion of a different path — and Remove-Item -Recurse -Force C:\Temp\somedir both ran with no prompt, each reported as matching the command allowlist. The single flag that made the approved command safe is exactly what the entry discards. That is what makes a bare git entry equivalent to allowing git push --force.

A second route, where the preview and the write disagree. git status -sb; Get-Random, with git status and git status -sb allowlisted and Get-Random not. The button reads “Always Run ‘Get-Random’ + 1” and the hover reads “Will allow: - Get-Random - git”, so the over-broad git candidate is generated on this path too, even though two entries match that half of the command. But after clicking, only Get-Random was actually stored. So something does suppress the redundant git at write time here, and that same suppression is not applied on the redirect path. Either way the preview names an entry that is not what happens.

Related threads:

The redirect half of this, acknowledged as a security issue.

My earlier report on the same matcher. The two compound badly: a beforeShellExecution hook is the only way I have found to make the deliberately broad $* entry safe, and that hook trusts the allowlist, so one Always run click on a redirect widens both gates and $out = git reset --hard HEAD~5 becomes unattended too.

Does this stop you from using Cursor

No - Cursor works, but with this issue

Hey @Leland_Hepworth
That Always Run path shouldn’t widen to bare git (or Remove-Item) without unblocking the redirected command. We’ve let the team know.

Workaround for now: approve those redirected commands one-shot, and remove any bare git entry if Always Run added one. Same surface as this related thread (different trigger). Docs: Run Modes.