anthropics / anthropics/claude-code

[BUG] No single syntax to express "these are the only files/directories Claude may read" — and every available workaround is worse

Aperta
#92,615 1 commento 0 reazioni 0 assegnatari Vedi su GitHub
area:permissions area:sandbox enhancement
Lingua principale
Python
Stelle
145k
Fork
23.1k
Metriche di merge delle PR
Metriche PR in attesa

Descrizione

### Preflight Checklist

- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) 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?

## 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:
```json
{ "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.:
```json
{ "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

```shell
## Error Messages/Logs

(none — this is a missing expressive capability, not a crash or error)
```

### Steps to Reproduce

## Steps to Reproduce

1. Try to express "only `app/` is readable" the natural way — broad `deny` + narrow `allow` (shown above). Confirm it fails: `app/` itself becomes unreadable, because `deny` always wins regardless of specificity.
2. Switch to the only working approach: enumerate every non-`app` top-level directory/file in `deny`. Confirm this takes on the order of 100 lines for a standard Rails project layout.
3. Try to add one exception (e.g. allow `config/routes.rb` while keeping the rest of `config/` denied). Confirm this requires deleting the `config/**` deny entry and re-enumerating every other file in `config/` individually — not adding a single line.
4. Add a new top-level directory to the project that isn't in the `deny` list yet (e.g. `uploads/`). Confirm it's readable by default, with no warning that the restriction no longer covers the whole project.
5. With the full `deny` enumeration from step 2 in place, run an ordinary command like `bin/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.

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Direzione di ricerca

Start with the permissions configuration and the PreToolUse hook behavior described in the report; reproduce the broad deny/narrow allow case and the bin/rails runner bypass. Done means a documented readable-root boundary denies unlisted and newly added paths, supports exceptions without re-enumeration, and has tests covering command-based reads.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
json, python, shell
Ambito
cli, security, tooling
Tipo di issue
Funzionalità
Difficoltà
5/5
Tempo stimato
Più di una settimana
Stato di attività
Attiva
Chiarezza
Abbastanza chiara
Idoneità per principianti
30/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.