PowerShell tool: destructive-path guard mis-extracts an unrelated quoted token as the delete target (false-positive blocks)

Status Closed — not planned
Maintainer reply None cached
Activity 4 comments · opened Jun 18, 2026 · closed Sep 21, 2026

Bug: PowerShell-tool destructive-path guard mis-extracts an unrelated quoted token as the delete target

Summary

Claude Code's built-in protected-path guard (the one that blocks destructive file ops on system/root paths with "Remove-Item on system path 'X' is blocked. This path is protected from removal.") does a crude static scan of the whole command for a path-like/quoted token near Remove-Item, and can grab a token from somewhere other than the actual delete target. When that wrong token happens to look root-like (e.g. a leading-slash string) or is just an unrelated quoted literal, the guard blocks a command that never targeted a protected path. The legitimate Remove-Item in these cases targeted an ordinary repo-relative file (e.g. .git/index.lock).

Environment

  • Claude Code on Windows 11, PowerShell tool (CLAUDE_CODE_USE_POWERSHELL_TOOL=1, pwsh 7).
  • Guard fires before execution; no user hook is involved (confirmed: the message string exists in no settings file, hook, or managed-settings path — it's the built-in).

Steps to reproduce / observed cases

Two real false-positives, both in compound (;-joined) commands whose only Remove-Item targeted a benign relative path:

  1. Commit message with a slash-token. A command that deleted .git/index.lock and then ran git commit whose message contained the substring /synthesize.
  • Expected: lock removed, commit made.
  • Actual: blocked — "Remove-Item on system path '/synthesize' is blocked. This path is protected from removal." The guard grabbed the commit-message text, not the Remove-Item argument.
  1. -split regex literal. A command of the shape Remove-Item <relative-path> ... ; ($line -split '\s+').
  • Expected: file removed, split runs.
  • Actual: blocked — "Remove-Item on system path '\s+' is blocked. This path is protected from removal." The guard grabbed the regex literal '\s+' as the "path."

A related, previously-noted variant: when the Remove-Item target is an unresolvable variable (e.g. Remove-Item $lock), the guard assumes / (root) and blocks.

Intermittency

The mis-extraction is inconsistent — minimal synthetic repros (e.g. Remove-Item .\nonexistent.txt -ErrorAction SilentlyContinue; $m = "add /synthesize route") did not trip it, while the real-world compound commands above did. So the trigger depends on command shape (likely the position/quoting of tokens relative to Remove-Item), not merely the presence of a slash-string. This makes it hard for users to predict or avoid.

Impact

  • Legitimate commits and cleanup commands are blocked, wasting tool calls and forcing awkward workarounds ([System.IO.File]::Delete('<literal>'), splitting commands, or falling back to Bash rm -f).
  • The guard cannot be disabled or scoped by the user, so there is no configuration-level mitigation — only command-rewriting.

Suggested fix

Parse the actual Remove-Item target rather than any quoted substring in the command:

  • Bind to the value of -Path / -LiteralPath, or the first positional argument of the Remove-Item invocation specifically.
  • Restrict protected-path matching to tokens that are genuine filesystem paths (drive-letter C:\…, UNC \\…, or resolvable repo-relative paths). Ignore regex/option strings, command-argument values of other commands in the pipeline (e.g. git commit -m …), and unrelated quoted text.
  • For unresolvable-variable targets, prefer "allow + warn" (or resolve the literal assignment when present) over assuming /.

Keep the genuine protection — Remove-Item C:\Windows\…, Remove-Item /, recursive-force on system roots should still block. The ask is only to stop blocking commands that merely mention a path-like or quoted string elsewhere.

View original on GitHub ↗

3 Comments

hiiqbiz-wq · 3 months ago

Additional case: the guard also fires on git rm (not just Remove-Item), and can extract an empty path

Fresh instance today on Windows 11 / PowerShell tool (pwsh 7), while extracting a subdirectory into its own repo. Notable for two reasons: (1) the command was git rm, not Remove-Item — so the scanner keys on the bare rm token, widening the blast radius to git subcommands; and (2) git rm --cached is non-destructive — it only unstages from the index, it never touches the working tree.

Command (interactive — only unstages two dirs from the git index, deletes no files):

git -C $dir rm -r --cached --quiet uploads _dev

Blocked with:

Remove-Item on system path ''/'' is blocked. This path is protected from removal.

The extracted "path" was empty ('') — the scanner saw the rm subcommand, couldn't parse a target past the -r --cached --quiet flags and the two trailing args, and fell back to an empty// path → blocked.

Same session, the unresolved-variable variant also fired (consistent with the note in the original report):

[System.IO.File]::Delete($idx)   →   Remove-Item on system path ''/'' is blocked. ...

Workarounds that parse cleanly: give git rm an explicit single quoted path after -- (git rm -r --cached -- "relative/path"), or use a literal path with [System.IO.File]::Delete('C:\...\literal'). The trip-wires are the multi-flag/multi-arg form and unresolved-variable targets.

Net: the guard should (a) not key on a bare rm token inside git rm — especially git rm --cached, which deletes nothing — and (b) never fall back to / or '' when it can't confidently extract a target; an unparseable target shouldn't block on a guessed root path.

jjwcdp · 3 months ago

Also reproduces on Windows PowerShell 5.1 (powershell.exe), and — notably — fires on commands that contain no Remove-Item, rm, or git rm at all.

Two blocks today, neither command containing any delete/destructive cmdlet (only process enumeration, or file create/copy):

  1. Read-only, double-quoted regex — a command enumerating processes (Get-CimInstance Win32_Process) and pulling a UUID from each command line with -match "(?:--resume|-r)\s+$uuid":

``
Remove-Item on system path '\s+$uuid' is blocked. This path is protected from removal.
``

  1. File create/copy (New-Item + Copy-Item + Set-Content — no delete), reusing the same \s+ regex literal in single quotes:

``
Remove-Item on system path '\s+' is blocked. This path is protected from removal.
``

So the \s+-as-target extraction (case 2 of the original report) does not require an actual Remove-Item/rm in the command — the regex literal alone trips the "Remove-Item on system path" rule. (The only rm substring present was inside Format-Table, which was also present in a command that passed, so that substring isn't the trigger on its own.)

Intermittency, sharpened: the identical single-quoted literal '(?:--resume|-r)\s+([0-9a-fA-F-]{36})' passed in one read-only command and was blocked in another that additionally did Copy-Item/Set-Content. Consistent with the "depends on command shape" note — here the co-occurring file-write cmdlets seem to matter.

Workaround that avoids it: drop the regex and tokenize with .NET — "$s".Split([char[]]@(' ', [char]9), [System.StringSplitOptions]::RemoveEmptyEntries) — no \s+, no block.

Environment: Claude Code 2.1.210 (PowerShell tool, Windows PowerShell 5.1 / powershell.exe), Windows 11.

Likely the same root cause as #73524.

mattc517 · 1 month ago

Pointer, because this thread is where the missing minimal repro was called out ("minimal synthetic repros ... did not trip it").

Here is a deterministic one on 2.1.229, Windows 11, PowerShell tool. The two commands differ by a single echoed string literal.

Control — runs:

Set-Content -Path "C:\Users\mcamp\AppData\Local\claude-guard-probe.txt" -Value "x"
Remove-Item "C:\Users\mcamp\AppData\Local\claude-guard-probe.txt" -Force
"F-A control ran"

Variant — add one line, denied every time:

Set-Content -Path "C:\Users\mcamp\AppData\Local\claude-guard-probe.txt" -Value "x"
"A:"
Remove-Item "C:\Users\mcamp\AppData\Local\claude-guard-probe.txt" -Force
"F-B variant ran"
Remove-Item on system path '"A:"' is blocked. This path is protected from removal.

Two notes on why this one reproduces where the earlier attempts didn't:

  • A bare drive letter, not a slash-string. The root-path predicate accepts ^[A-Za-z]:\/?$ as well as the / forms discussed above, so "A:" qualifies while /synthesize and friends depend on other branches. It is also language-independent, unlike the Spanish del collision reported on #81378.
  • The shape requirement is real. The deny only happens on the fallback string-splitter, which runs when the AST parse fails — the code is inside an if(!i.valid){...}. Two statements parse fine and pass, which is exactly the intermittency described here. The four-line form above is enough to fail the parse.

The full write-up, with the verbatim 2.1.229 code — the two script-global variables and the if(K&&j!==void 0)return pat(j) line that combines them across unrelated statements — is on #81378: https://github.com/anthropics/claude-code/issues/81378#issuecomment-5330356143

That comment also shows Remove-Item Q:\Windows -Recurse -Force still blocking on 2.1.229, i.e. the statement-scoped inner loop catches genuine attempts and the cross-statement branch can be removed without weakening the guard.

Showing cached comments. Read the full discussion on GitHub ↗