grep -r and cp -r still bypass Read() deny rules on 2.1.259 (changelog fix does not reproduce; Windows/VS Code extension)
What happened
The 2.1.259 changelog entry
grep -r/cp -rover a directory holding a denied file now asks
does not reproduce for me. On 2.1.259, both grep -r and cp -r over a directory that contains a
file matched by a permissions.deny Read() rule complete silently — no prompt, no denial.grep -r prints the denied file's contents; cp -r copies it.
Direct reads of the same file are still correctly denied, so the deny rule itself is active.
Environment
- Claude Code 2.1.259, VS Code extension (
anthropic.claude-code-2.1.259-win32-x64,
bundled resources/native-binary/claude.exe — verified as the running process, not the PATH CLI)
- Windows 11 Pro 26200, Git Bash as the Bash tool shell
permissions.defaultMode: "auto"- Relevant rule in
~/.claude/settings.json:"deny": ["Read(**/.env)"]
Reproduction
mkdir -p /tmp/deny-canary/sub
echo 'CANARY_TOKEN=deny-canary-not-a-real-secret-0001' > /tmp/deny-canary/sub/.env
echo 'CANARY_TOKEN=plain-file-should-be-readable-0002' > /tmp/deny-canary/sub/plain.txt
With Read(**/.env) in permissions.deny, run each of these as a separate Bash tool call
(combining them into one command collapses the permission decision into a single check):
| # | Command | Expected | Actual on 2.1.259 |
|---|---------|----------|-------------------|
| A | cat /tmp/deny-canary/sub/.env | deny | deny |
| B | cd /tmp/deny-canary/sub && cat .env | deny | deny |
| C | grep -r CANARY /tmp/deny-canary | ask (per changelog) | passes — prints .env contents |
| D | grep -f /tmp/deny-canary/sub/.env /tmp/deny-canary/sub/plain.txt | deny | deny |
| E | grep --file=/tmp/deny-canary/sub/.env /tmp/deny-canary/sub/plain.txt | deny | deny |
| F | cp -r /tmp/deny-canary /tmp/cpr-test | ask (per changelog) | passes — .env copied intact |
C returns:
/tmp/deny-canary/sub/.env:CANARY_TOKEN=deny-canary-not-a-real-secret-0001
F leaves a complete copy of the denied file at /tmp/cpr-test/sub/.env (same byte size).
I ran the identical five forms A–E on 2.1.258 beforehand and got exactly the same results, so I
cannot see any behavior change from the 2.1.259 fix in this environment.
Why it matters
A Read() deny rule reads as a containment boundary for secrets, but a single recursive grep
over the parent directory returns the contents anyway. Since cp -r also succeeds, the copied file
can be reached under a name the deny glob no longer matches. (I verified the copy exists and its
size; I did not exercise the rename step.)
Note on attribution
Denial messages differ by form, which is a genuine improvement in 2.1.259: form B now returnscat from '<path>' was blocked by a deny rule., naming the deny rule explicitly. Forms A, D and E
still return the generic Permission to use Bash with command ... has been denied., which is
indistinguishable from an auto-mode classifier denial.
Relation to existing reports
The three open reports I could find on the 2.1.259 deny-rule guard all describe it firing too
often — #91650 (cd to an absolute path), #91683 (cd DIR && grep … under bypassPermissions),
#91681 (rg directory searches). This report is the opposite direction: the guard does not fire at
all on the two command shapes the changelog entry names. Taken together they suggest the guard
matches on command shape rather than on the paths a command will actually read.
(I could not test rg myself — it is not installed in this environment — so I make no claim about
it beyond citing #91681.)