Windows: "Always allow" writes permission rules with doubled backslashes, so they never match
Summary
On Windows, approving a command with "Always allow" writes a rule into .claude/settings.local.json whose backslashes are escaped one level too many. The stored pattern contains \\ where the real command contains \, so it can never match.
The effect is that "Always allow" silently does nothing for any command containing a Windows-style path. The user is re-prompted for the identical command every subsequent run, and the settings file accumulates dead rules indefinitely.
Environment
- Claude desktop app
1.24012.9, Windows 11 - Reproduces via the PowerShell tool; the same doubling appears on Bash-tool rules containing Windows paths
Reproduction
- In any project, run a command containing a Windows-style relative path:
````
.\.venv\Scripts\python.exe -m mypkg stats
- Approve it with "Always allow"
- Open
.claude/settings.local.json
Stored (raw file text):
"PowerShell(.\\\\.venv\\\\Scripts\\\\python.exe -m mypkg stats)"
which JSON-decodes to the pattern:
PowerShell(.\\.venv\\Scripts\\python.exe -m mypkg stats)
Actual command: .\.venv\Scripts\python.exe -m mypkg stats
Two backslashes in the pattern vs one in the command — no match.
Expected raw file text:
"PowerShell(.\\.venv\\Scripts\\python.exe -m mypkg stats)"
- Run the same command again → prompted again. Repeat forever.
Likely cause
The rule text appears to be regex-escaped before being JSON-serialized. Shell metacharacters are escaped alongside the backslashes — a PowerShell loop was stored as:
PowerShell($ids=1,2,3; foreach\($i in $ids\){ ... })
Note the \( and \). If these patterns are matched as literal prefixes, escaping both backslashes and parentheses is wrong.
Impact
In one project, 77 of 154 rules in settings.local.json were dead this way — including every one of the 44 rules for that project's main CLI. The user had been re-approving the same handful of commands for weeks; each click added another rule that could never fire.
An aggravating factor: rules are stored as the entire literal command, so even a correctly escaped rule only matches that exact invocation — show 230 will not match show 231. Combined with the escaping bug, the file grows monotonically while the prompt rate never drops.
Verified not user error
Across the full local transcript history, the only assistant Write calls targeting settings.local.json wrote an empty allow list. All 154 entries were produced by the approval flow itself.
Workaround
Hand-write the rules in .claude/settings.json with single-escaped backslashes and a wildcard, e.g.:
"PowerShell(.\\.venv\\Scripts\\python.exe -m mypkg *)"
These match correctly. Note that path separators are matched literally, so a rule written with / will not match a command written with \; both spellings need their own entry.