PowerShell command blocked citing a path unrelated to the command; here-string text content is scanned as code

Status Open
Maintainer reply None cached
Activity 2 comments · opened Jul 26, 2026

Summary

When the built-in PowerShell safety check cannot statically parse a command (multi-line / compound scripts), it falls back to a string-splitting scanner. In that fallback path, "the first token that looks like a protected system path" and "whether a delete cmdlet appears anywhere in the script" are tracked as two independent variables spanning the entire multi-statement script. They are then combined at the end to produce a deny message.

Consequences:

  1. The path shown in the error message can be completely unrelated to what the command actually deletes.
  2. A command that deletes nothing at all can be blocked, if its text content happens to contain a delete-cmdlet name plus a token that normalizes to /.
  3. Because the check is fail-closed and blocks the whole invocation, unrelated operations earlier in the same command (e.g. file writes) also never execute.

Observed behaviour

Case A — wrong path in message

A multi-line PowerShell command that (a) read a file, (b) did a string replace, (c) wrote it back via [System.IO.File]::WriteAllText, (d) printed byte counts, (e) ran a Select-String probe, and (f) as its last statement deleted a lock file at a normal user path, was blocked with:

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

The command contained no reference to /. The actual delete target was an ordinary file under a data directory. Verified that nothing executed (file byte count unchanged), confirming the block happens before execution.

Case B — text content triggers it, with no delete operation present

A later command performed a pure write — no delete of any kind — inserting a block of prose into a Markdown file using a single-quoted here-string (@'...'@). The prose was documentation about Case A, so it contained the cmdlet name as literal text, plus a quoted '/' inside a sentence. Result:

Remove-Item on system path ''/'」=**CLI' is blocked. This path is protected from removal.

The string in the message ('/'」=**CLI) is a fragment of the prose being written — the surrounding text is Chinese, hence the CJK characters. It is not a path, not an argument, and not part of any command. This demonstrates that here-string content is scanned as if it were code.

This case is the more actionable of the two: it means writing documentation that merely mentions a delete cmdlet can be blocked, even when the command performs no deletion at all.

Expected behaviour

  • The path reported in the deny message should be the path the delete cmdlet actually targets.
  • A command with no delete operation should not be blocked by a delete-path rule.
  • String literals / here-string contents should not be scanned as command tokens; or if the scanner cannot distinguish them, it should report "cannot statically verify — needs manual approval" rather than asserting a specific protected-path claim.

Root cause

The fallback scanner splits the script on [;|\n\r{}()&]+ (note: parentheses are split points), then per token:

  • sets a "candidate protected path" variable on the first token that passes the protected-path predicate, guarded by an is-undefined check so it is never overwritten;
  • sets a boolean when any token normalizes to a delete cmdlet (aliases are normalized correctly — rm, del, rd, rmdir, ri, erase all map through);
  • after both loops, if the boolean is set and the candidate path is defined, it denies using that candidate — even though the two were discovered in unrelated statements.

The protected-path predicate returns true for a bare /: after backslash-to-slash normalization, path.win32.isAbsolute('/') is true and the normalized value equals /.

Caveat on this section: this was derived by extracting strings from the shipped bundle, so identifier names are not meaningful and may change between releases. The mechanism is the claim being made, and it is independently corroborated by Case B, which is reproducible from observable behaviour alone without any inspection of the bundle.

Suggested fix

Scope the candidate-path search to the same statement in which the delete cmdlet was matched, instead of using a script-global variable. If no path token is found within that statement, return "cannot statically verify — requires manual approval" rather than borrowing a token from elsewhere.

This preserves the protection: a genuine Remove-Item C:\Windows still has its path token in the same statement and is still caught. It only removes the cross-statement false positives.

Additionally, string literals and here-string bodies should be excluded from token scanning in the fallback path.

Notes

  • [System.IO.File]::Delete(...) is not covered by this check at all (member invocation rather than a command); that is how the original operation was eventually completed. Raising this as context rather than as a security report: the check reads as a guardrail against accidental high-risk cmdlet use, not a security boundary. It may be worth being explicit about that in the docs so users do not over-rely on it.
  • Environment: Windows 11, Claude Code CLI installed globally via npm (%APPDATA%\npm\node_modules\@anthropic-ai\claude-code), PowerShell 7.

Workarounds (for others hitting this)

  1. Keep delete operations as their own single-line invocation, without nested parentheses, separate from read/write/verify logic.
  2. When writing text that mentions delete cmdlet names, write it with a file-editing tool rather than passing it through a PowerShell here-string.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗