Command Allowlist: an entry for a cmdlet that takes a script block auto-runs any command inside the braces

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

Cursor splits a command into fragments and matches each fragment against the allowlist, but it never descends into a PowerShell script block. ForEach-Object { ... } is parsed as the command ForEach-Object with the braces as an argument, so the entry ForEach-Object matches that fragment whole and the commands inside the braces are never matched against anything. Any allowlisted cmdlet that accepts a script block is therefore a general-purpose execution channel, and no entry can narrow it.

Measured on 3.14.7 with Write-Output, ForEach-Object, Sort-Object and Select-Object on the allowlist, and Get-Random, New-Item <file> and Remove-Item not on it:

Command Result
Get-Random Approval prompt
Write-Output "probe" | ForEach-Object { Get-Random } Runs with no prompt, prints a random number
Write-Output "probe" | ForEach-Object { New-Item scratch-proof.txt -Value "written with no approval" } Creates the file with no prompt
Write-Output "probe" | ForEach-Object { Remove-Item scratch-proof.txt } Deletes the file with no prompt
Get-ChildItem .cursor/rules | Sort-Object { Get-Random } Runs with no prompt; the block is evaluated once per object, so the order differs run to run
Get-ChildItem .cursor/rules | Group-Object { Get-Random } Approval prompt

The last row is the control, and it is what makes the cause unambiguous: Group-Object is the one cmdlet here that is not an allowlist entry, and the identical shape prompts for it. The entry on the outer cmdlet is the entire difference. Every auto-run above reported “This command ran outside the sandbox (no restrictions) because it matched the user’s command allowlist.”

This is not specific to ForEach-Object, and it is not specific to a pipeline. Where-Object, Sort-Object, Select-Object and Measure-Object all take a script block, and so does any function the user happens to have defined.

The mechanism is in resources/app/extensions/cursor-agent-exec/dist/main.js (3.14.7). A parsed command is { name, subcommandTokens, arguments }, and the suggested-entry builder confirms what is treated as a command name: it emits name, then name plus each subcommand token, and finally [name, ...arguments].join(" "). A script block lands in arguments, so it is only ever text belonging to the outer cmdlet — never a command in its own right. matchesShell then matches the fragment on the first of its four tests:

matchesShell(e, t) {
    const n = Wl(e);
    if (!n) return !1;
    const r = t.trim(),
        o = this.extractBaseCommand(r);
    if (null !== n.argsPattern) { /* ... */ }
    return !!this.matchGlob(n.fullPattern, r)
        || !!r.startsWith(`${n.fullPattern} `)   // <-- entry plus a space
        || !!this.matchGlob(n.fullPattern, o)
        || o === n.fullPattern;
}

Two things make this worse than the raw capability suggests. The block-taking cmdlets are the ones a careful user allowlists first, because ForEach-Object, Where-Object and Sort-Object read as harmless formatting and filtering helpers — I added all four of mine for exactly that reason, and nothing in the UI hints that the entry carries arbitrary execution with it. And the harm is invisible in the transcript: the command looks like the read-only pipeline it is 99% of the time, so there is no prompt to notice and nothing to review after the fact.

Steps to Reproduce

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

  1. Open Settings → Agents → Approvals & Execution. Set Run Mode to Allowlist.
  2. Under Command allowlist, add two entries: Write-Output and ForEach-Object.
  3. Confirm Get-Random, New-Item and Remove-Item are not on the allowlist.
  4. Ask the agent to run Get-Random. → An approval prompt appears, as it should.
  5. Ask the agent to run Write-Output "probe" | ForEach-Object { Get-Random }. → It runs with no prompt and prints a number, and the tool result reports that it matched the command allowlist and ran outside the sandbox.
  6. Replace the block’s contents with anything you like. Write-Output "probe" | ForEach-Object { New-Item scratch-proof.txt } creates the file unattended, and the same shape with Remove-Item scratch-proof.txt deletes it, which shows the bypass is not limited to read-only commands.
  7. For the control, add Get-ChildItem and Sort-Object but not Group-Object, then compare Get-ChildItem . | Sort-Object { Get-Random } against Get-ChildItem . | Group-Object { Get-Random }. The first runs unattended; the second prompts.

Expected Behavior

  1. The contents of a script block should be parsed as commands and matched against the allowlist on their own terms. ForEach-Object { Get-Random } should require approval exactly as Get-Random does.
  2. An allowlist entry for a cmdlet should grant that cmdlet its arguments, not the right to execute a fresh command. This is the same principle as the assignment right-hand side in my other bug report referenced below: whatever the shell will execute is a command, whatever it looks like syntactically.
  3. A construct the parser cannot decompose should fail closed Prompt rather than treat the enclosing fragment as fully matched. This matters more than enumerating the cmdlets that take a block, because enumerating them cannot work: a user-defined function taking a [scriptblock] parameter is indistinguishable from a built-in, so the general rule has to be “braces contain code I have not checked” rather than a list of names. The parsing result already carries parsingFailed and hasCommandSubstitution, so nested execution is a concept the model has; a script block should join it.

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

Related threads:

The same failure through an assignment’s right-hand side. Distinct precondition, as described above, and a fix for one does not fix the other.

Relevant because a beforeShellExecution hook is currently the only way to workaround this, and that race means the hook intermittently does not run at all. A mitigation that fails silently is not much of a mitigation.

Community thread on the general theme of the allowlist granting more than it appears to.

Earlier reports of the same class through a different construct, both closed:

Those were about command chaining, and && is split correctly now. Assignment and script blocks are the two constructs still outstanding, which suggests the durable fix is a parser that treats every executable position as one rather than a list of the positions found so far.

Workaround, for anyone who finds this thread. Until the matcher descends into a block, the only lever is a beforeShellExecution hook, because a hook’s ask overrides an allowlist match. Mine treats a { as a construct to inspect in its own right, walks the block statement by statement, and returns ask unless every command inside is itself on the allowlist. Two things to know if you try this. Masking quoted strings first is essential, or rg -n "a{2}" src and every JSON argument start prompting. And the guard has to inspect a block whether or not the command contains anything else suspicious: mine originally reached its block-reading code only when a $ happened to appear elsewhere, so the identical block was checked in $out = ... | Sort-Object { Get-Random } and waved through without the assignment.

Does this stop you from using Cursor

No - Cursor works, but with this issue

Hey @Leland_Hepworth,

For now, in Settings → Agents → Approvals & Execution, avoid allowlisting bare block-taking cmdlets (ForEach-Object, Where-Object, Sort-Object, Select-Object, and similar) if those entries would wrap other work. Your beforeShellExecution hook that forces ask when braces appear is a solid extra check (with the race caveat you linked). Related sibling on the same matcher family: variable-assignment allowlist entry. More on the controls: Run Modes.

We’ve let the team know. I’ll post here if there’s something useful to share.