permissions.deny on an absolute credential path silently matches nothing (single leading / is project-relative) — no warning
Summary
A permissions.deny rule written with a single leading / — the form almost everyone will reach for when guarding an absolute credential path — is interpreted as project-relative and therefore matches nothing. Claude Code emits no warning. The user is left with a rule that reads as protection and provides none.
This is documented behaviour ("a leading / is project-relative; // is absolute"), so this is not a bug report about resolution. It is a report that the failure is silent on exactly the paths where silence is most dangerous.
Reproduction
.claude/settings.json in a project at /Users/me/src/myproject:
{
"permissions": {
"deny": [
"Read(/Users/me/.ssh/**)",
"Read(/Users/me/.aws/credentials)"
]
}
}
Then, in a session launched from that project, ask Claude to read ~/.ssh/config.
Expected: denied.
Actual: allowed — the file's contents are returned. The rule resolved to /Users/me/src/myproject/Users/me/.ssh/**, which matches nothing.
Verified on Claude Code v2.1.209 (macOS). The correct forms are Read(//Users/me/.ssh/**) or Read(~/.ssh/**).
Why this is worth a warning
- The failure mode is silent and inverted. A misconfigured allow rule fails closed (you get denied and notice). A misconfigured deny rule fails open — and you only find out when something reads the credential you thought was guarded.
- The paths people write these rules for are precisely
~/.ssh,~/.aws,~/.gnupg,.env— the highest-consequence files on the machine. - A leading
/reads as "absolute" to essentially every developer. The gitignore convention is correct and defensible, but it is the opposite of the intuition users bring to an absolute filesystem path, and nothing in the product corrects them. sandbox.credentialsdoes not cover this. It guards the Bash sandbox, not the Read tool — so a project can have{"path": "~/.ssh", "mode": "deny"}undersandbox.credentialsand apermissions.denyRead rule, and the Read tool still returns the file. Both guards can appear present while neither is effective.
Suggested fixes (any one would close it)
- Warn on a suspicious deny path. If a
Read/Edit/Writerule's path begins with a single/and looks like an absolute filesystem path (e.g. starts with/Users/,/home/,/etc/,/var/), emit a startup warning: "this is project-relative; did you mean//…or~/…?" - Warn on a rule that can never match (resolves to a path outside/below the project that does not exist).
- Surface effective rules. A
claude doctor//permissionsview showing each rule's resolved path would make this self-diagnosing. - At minimum, call the hazard out explicitly in the credential-guarding examples in the permissions docs.
Impact
Anyone hardening a project against credential reads by the obvious method has a non-functioning guard and no signal that anything is wrong. In our case this was found only because we ran an explicit enforcement probe; the rule had been in place, believed effective, for some time.
Related
- #51211 is an open report of
permissions.denyRead rules silently not enforcing, and its original repro (Read(/Users/.../Downloads/**)) has the same single-leading-/shape as this report — but that thread's discussion attributes failures to a different mechanism (broadallowwildcard beating a specificdeny). This issue documents the leading-/-resolves-project-relative root cause specifically, which that thread doesn't cover.