Skip to content

feat(filesystem): add denyReadAlways for denies that beat allowRead#284

Open
smolyn wants to merge 3 commits into
anthropic-experimental:mainfrom
ubc:gs/deny-read-always
Open

feat(filesystem): add denyReadAlways for denies that beat allowRead#284
smolyn wants to merge 3 commits into
anthropic-experimental:mainfrom
ubc:gs/deny-read-always

Conversation

@smolyn

@smolyn smolyn commented May 27, 2026

Copy link
Copy Markdown

Summary

Adds an optional filesystem.denyReadAlways: string[] schema field that emits read-deny rules with priority over allowRead. Designed to let users keep a broad allowRead (e.g. ~/src for cross-project work) while still blocking credential-style files (.env, ~/.aws/credentials, *.pem, …) inside.

Motivation

Today, read restrictions have two layers:

  • denyRead — deny broad regions
  • allowRead — re-allow within denied regions (wins over denyRead)

There is no way to express "deny X even though X is inside an allowed region." denyRead: ["~/src/**/.env*"] is silently overridden when allowRead: ["~/src"] is set, because generateReadRules (macos-sandbox-utils.ts:224-298) emits Seatbelt rules in allow → deny → allow order — last-match-wins makes allowRead the final word.

The write side already has the symmetric capability: denyWrite beats allowWrite via the existing denyWithinAllow channel (sandbox-config.ts:239-254). This PR adds the equivalent for reads.

Behavior

Rule list Priority
denyReadAlways highest — wins over allowRead and denyRead
allowRead (a.k.a. allowWithinDeny) re-allows within denyRead
denyRead (a.k.a. denyOnly) denies regions
(default) allow

Example config:

{
  \"filesystem\": {
    \"denyRead\": [\"/Users\"],
    \"allowRead\": [\"~/src\"],
    \"denyReadAlways\": [
      \"/**/.env*\",
      \"/**/credentials\",
      \"/**/id_rsa*\",
      \"/**/id_ed25519*\"
    ]
  }
}

~/src/myproject/.env → blocked (denyReadAlways).
~/src/myproject/source.ts → allowed (allowRead beats denyRead).
~/Documents/note.md → blocked (denyRead /Users, no override).

Implementation

  • src/sandbox/sandbox-config.ts — add denyReadAlways to FilesystemConfigSchema (optional, additive).
  • src/sandbox/sandbox-schemas.ts — extend internal FsReadRestrictionConfig with denyAlways?: string[]; updated docstring to describe the three-layer model.
  • src/sandbox/sandbox-manager.ts — plumb the field through getFsReadConfig and the customConfig override path, with the same Linux-glob-expansion treatment used for denyRead.
  • src/sandbox/macos-sandbox-utils.ts — third pass in generateReadRules emitting (deny file-read* ...) after the allowWithinDeny loop so denies win. Also pass denyAlways paths to generateMoveBlockingRules so mv/rename cannot bypass.
  • src/sandbox/linux-sandbox-utils.ts — third pass after the allowRead re-bind loop emitting --tmpfs <dir> or --ro-bind /dev/null <file>. Bubblewrap binds are applied in argument order, so later binds shadow earlier ones for the same destination.
  • test/config-validation.test.ts — schema test confirming denyReadAlways validates.

+128 / -5 lines across 6 files. All additive — existing configs unaffected.

Glob anchoring note

Glob patterns are CWD-relative unless they start with / (the existing denyRead has the same behavior). The intended pattern for global credential matching is /**/.env*, which resolves to the regex ^/(.*/)?\.env[^/]*$. On Linux, broad globs like /**/.env* hit the existing "too broad" guard in expandGlobPattern and get skipped with a warning — same limitation as broad denyRead globs today. Linux users should narrow the patterns to e.g. ~/src/**/.env*.

Test plan

  • pnpm exec tsc --noEmit clean
  • test/config-validation.test.ts schema test passes
  • End-to-end on macOS with srt --settings:
    • Created ~/src/srt-test/.env and ~/src/srt-test/regular.txt.
    • With allowRead: [\"~/src\"] only: both readable.
    • Added denyReadAlways: [\"/**/.env*\"]: .env blocked with EPERM, regular.txt still readable. ✓
  • Linux integration verification (not run locally — would appreciate CI confirmation)

Dependency note

This branch stacks on #283 (allowAllDomains) — the PR diff currently includes both features. After #283 merges I'll rebase this branch onto fresh main so the diff shows only the denyReadAlways change.

🤖 Generated with Claude Code

smolyn and others added 3 commits May 26, 2026 17:53
When `network.allowAllDomains: true`, filterNetworkRequest short-circuits
to allow after the deniedDomains check runs, so explicit denies still take
effect. Intended for environments where filesystem restrictions are the
primary boundary and enumerating every reachable domain is impractical
(e.g. opening web docs lookups in an otherwise locked-down sandbox).

The flag is optional and additive: existing configs are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Read restrictions today have only two layers — denyRead and allowRead —
with allowRead unconditionally winning. That makes it impossible to
broadly allow a directory (e.g. ~/src for cross-project work) while
still keeping credential-style files (.env, *.pem, ~/.aws/credentials)
denied within it. The write side already has the symmetric capability
via denyWrite, so reads were the odd one out.

New optional `denyReadAlways: string[]` in FilesystemConfigSchema. On
macOS it emits Seatbelt (deny file-read*) rules AFTER the allowRead
loop so they take precedence (last-match-wins). On Linux it emits
bubblewrap binds (--tmpfs for dirs, --ro-bind /dev/null for files)
after the allowRead re-bind loop for the same effect.

denyReadAlways paths are also passed to generateMoveBlockingRules so
mv/rename cannot bypass the deny by relocating the file.

Glob handling reuses the existing helpers — patterns work the same as
denyRead. Patterns with a leading "/" match globally (e.g.
"/**/.env*"); without it they are CWD-relative, matching existing
denyRead behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant