[BUG] No single syntax to express "these are the only files/directories Claude may read" — and every available workaround is worse
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?
<!--
This maps to the "Bug Report" issue form at:
https://github.com/anthropics/claude-code/issues/new?template=bug_report.yml
Copy each section below into the matching field on that form.
Suggested title: No single syntax to express "these are the only files/directories Claude may read" — and every available workaround is worse
-->
What's Wrong?
There is no single expression in permissions that says "these are the only readable files/directories." You can only express the inverse — list everything that is not allowed — and every practical consequence of that makes it worse, not just inconvenient:
1. Only the inverse can be expressed. The natural way anyone would first try to write "only app/ is readable" is a broad deny plus a narrow allow:
{ "permissions": { "deny": ["Read(/**)"], "allow": ["Read(/app/**)"] } }
This does not work. Rules are evaluated in a fixed deny > ask > allow order that specificity never changes, so the broad deny also blocks the narrower allow path — app/ itself becomes unreadable. The only way to achieve "only app/ is readable" is to manually write deny for every other top-level directory and file in the project. For a normal Rails project that's roughly 100 lines to express what should be a single line.
2. Because that enumeration alone isn't trusted to hold, you also need auxiliary scripts. The standard advice is that permissions.deny can be bypassed (e.g. via Bash), so on top of the ~100-line enumeration we also had to write a custom PreToolUse hook script that re-checks the resolved path on every file-tool call, plus dependency checks and fail-closed error handling once that hook became load-bearing. Expressing one access boundary now requires a config file and a maintained shell script, for something that should be a declarative one-liner.
3. Adding a single exception later requires rewriting large chunks of existing rules. Because there's no positive whitelist, you can't "add one more allowed path." To allow one specific file inside an already-denied directory (e.g. allow config/routes.rb while still denying the rest of config/), you have to delete the directory-wide deny "Read(/config/**)" and replace it with an enumerated list of every other file in config/ you still want denied. A one-line exception requires rewriting an entire section of the config.
4. Newly added files/directories are ambiguous by construction. Since the whole mechanism is an inverse, hand-maintained list, anything added to the project after the list was written — a new top-level directory, a new root-level file — is readable by default. Nothing flags this. The list doesn't degrade gracefully; it silently stops covering the project as the project evolves.
5. After all of that — the deny enumeration, the auxiliary hook script, the rewrites for every exception, and remaining vigilant about new files — it still cannot achieve reliable allow/deny. Any ordinary command that opens a file directly (e.g. bin/rails runner "puts File.read('config/database.yml')", ruby -e "...") bypasses the entire thing, because Bash deny rules only match literal command strings, not what file a command will actually touch. There is no version of this that ends in an actual guarantee, no matter how much of the above you do.
What Should Happen?
What Should Happen?
There should be one syntax that directly expresses the allowed set, e.g.:
{ "permissions": { "readableRoots": ["app/**"] } }
...where anything not matching a listed root is denied by default, including things added later, and adding an exception is adding one entry, not rewriting a section. Ideally this would also apply to Bash (a real path-based sandbox), since a string-pattern deny can never fully guarantee "no command can read outside this root."
Error Messages/Logs
## Error Messages/Logs
(none — this is a missing expressive capability, not a crash or error)
Steps to Reproduce
Steps to Reproduce
- Try to express "only
app/is readable" the natural way — broaddeny+ narrowallow(shown above). Confirm it fails:app/itself becomes unreadable, becausedenyalways wins regardless of specificity. - Switch to the only working approach: enumerate every non-
apptop-level directory/file indeny. Confirm this takes on the order of 100 lines for a standard Rails project layout. - Try to add one exception (e.g. allow
config/routes.rbwhile keeping the rest ofconfig/denied). Confirm this requires deleting theconfig/**deny entry and re-enumerating every other file inconfig/individually — not adding a single line. - Add a new top-level directory to the project that isn't in the
denylist yet (e.g.uploads/). Confirm it's readable by default, with no warning that the restriction no longer covers the whole project. - With the full
denyenumeration from step 2 in place, run an ordinary command likebin/rails runner "puts File.read('config/database.yml')". Confirm it succeeds, bypassing the entire configuration.
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.263
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Terminal.app (macOS)
Additional Information
Regression
No, this never worked
Claude Model
Not sure / Multiple models
Claude Code Version
(please fill in via claude --version)
Platform
(please select — Anthropic API / AWS Bedrock / Google Vertex AI / Other)
Operating System
Other Linux
Terminal/Shell
(please fill in)
Additional Information
Other things checked and ruled out as ways to express this today: permissions.additionalDirectories (only adds to the readable scope, can't narrow it), permissions.defaultMode: "dontAsk" (still auto-runs "recognized read-only Bash commands," so it doesn't express a whitelist either, and its effect on Read/Grep/Glob isn't documented), and permissions.blockReadsOutsideWorkingDirectories (only fences reads outside the whole project, not one subtree of it from another). None of these provide a single construct for "this subtree only," and none of them remove the need for the enumeration, the auxiliary hook, the per-exception rewrites, or the gap against newly added files described above.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗