[BUG] Built-in "dangerous rm" safety check is spelling-dependent: the MSYS /d/… form of a top-level path raises no approval gate

Status Open
Reported on v2.1.293
Maintainer reply None cached
Activity 1 comment · opened Oct 8, 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?

Claude Code's built-in "dangerous rm" safety check treats two spellings of the same on-disk location (per MSYS semantics, /d/x ≡ D:\x) differently: the Windows-form top-level path raises the approval gate; the Git-Bash/MSYS form raises no gate at all. The top-level-directory guard is therefore spelling-dependent — and the MSYS spelling bypasses even the human-approval prompt.

Verbatim predicate (from the bundled JS in claude.exe):

var Kne=/^[A-Za-z]:\/?$/, jne=/^[A-Za-z]:\/[^/]+$/, qne=/^[A-Za-z]:\//, Yne=/^[\\/]{2}(?:wsl\.localhost|wsl\$)[\\/]...
function ese(e){
  if(e==="*"||e.endsWith("/*"))return true;      // wildcard root
  ...; if(h==="/")return true;                   // "/"
  if(Kne.test(h))return true;                    // drive root (D: / D:/)
  ...(home / canonical home / UNC spellings)...
  if(jne.test(h))return true;                    // <- drive + EXACTLY ONE segment => top-level dir
  return false;
}

The trigger is a conjunction (drive-letter form [A-Za-z]: AND a single segment). /d/… matches neither regex (no [A-Za-z]:), so ese returns false and the removal proceeds without any approval gate.

Coverage (measured + derived):
| form | gate raised? | basis |
|---|---|---|
| D:/<top> | yes (approval) | jne |
| D: / D:/ (drive root) | yes (approval) | Kne |
| / | yes | h === "/" |
| home (absolute) | yes | === home |
| /d/<top> (MSYS) | no | neither regex needs [A-Za-z]: |
| D:/a/b/c (deep) | no — except when it is/under the working dir | not critical-path; a separate workspace rule catches cwd/its ancestors |
| relative path | no — same workspace exception | same |
| ~ form | not verified | expansion timing outside the predicate |
| $VAR/x (possibly empty) | yes | a separate sibling rule |

Observed (what was run, not instructions to reproduce): the Windows-form spelling of a top-level directory was held for approval — verbatim: ...Only a person may approve such a removal, and no person did ... What was flagged: Dangerous rm operation detected: 'D:/hql-gateprobe'. The MSYS-form spelling of top-level directories, in the same session, ran to completion with no prompt (rc=0; the targeted directories were removed). ⛔ Do not re-run the MSYS form against any real path — it is ungated.
Note: the two runs used different target names; the equivalence /d/x ≡ D:\x is a property of MSYS that applies here — a same-name A/B was not run.

What Should Happen?

The critical-path test should reach the same verdict for equivalent spellings of the same location.
Suggested fix: normalize at the path-canonicalization layer that feeds this check, before the critical-path test — map the MSYS form (/^\/[a-z]\//) to the drive-letter form. The bundle already contains such a mapping — /^\/[a-z]\// matches 5 sites, 4 of them with the /i flag (e.g. if(O()==="windows"&&n.match(/^\/[a-z]\//i)) → DQ(n), DQ: /d/x → D:\x) — but ese does not call it. The goal is equivalent spellings ⇒ same verdict, not to widen/narrow which directories count as critical.
Blast radius: normalizing at the shared layer would also affect the sibling workspace/home checks (same path input); a narrower option is to canonicalize only for the critical-path test.
Precision note: DQ is a minified identifier in this bundle; its own body matches /^\/([A-Za-z])(\/|$)/ — slightly wider than the quoted regex. Key the fix on the existing canonicalizer at that layer, not on the quoted regex.

Error Messages/Logs

...denied by a built-in Claude Code safety check ... Only a person may approve such a removal, and no person did ... What was flagged: Dangerous rm operation detected: 'D:/hql-gateprobe'

Steps to Reproduce

The check inspects the command string before execution, so the two spellings can be compared without deleting anything: present the same non-existent top-level target once in each spelling and note whether the approval prompt appears — the MSYS spelling produces no prompt.
Deliberately omitted: the exact command lines. The MSYS spelling is ungated, so a pasted line could delete a real directory if the target happens to exist; the shape of the command is not needed to demonstrate the predicate above.

Claude Model

Other (a non-Claude model served through an Anthropic-compatible endpoint). This bug is in client-side bundled code and is model-independent.

Is this a regression?

I don't know

Last Working Version

(not tested on prior versions)

Claude Code Version

2.1.293 (Claude Code)

Platform

Other

Operating System

Windows

Terminal/Shell

Other (Git for Windows Bash — MSYS / MINGW64)

Additional Information

Related but different (found while searching; neither is this bug):

  • #99193 — Windows: sub-agent rm -rf on the 8.3 short-name alias of the home dir wiped ~116 GB.
  • #36339 — [Windows] rm -rf on a pnpm project deleted entire C:\Users via NTFS junction traversal.

The vector reported here differs: the MSYS /d/… spelling of a top-level path is not matched by the critical-path predicate at all.
Environment: Windows 11 · shell = Git for Windows Bash (MSYS) · claude --version = 2.1.293.
Bundle coordinate: bin/claude.exe, size 256,155,808 B; function ese( @ byte 214486728; /^\/[a-z]\// ×5 (4 carry /i).

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗