[BUG] `sandbox.filesystem.denyWrite` silently ignores rules containing a mid-path wildcard (Linux)

Status Open
Reported on v2.1.263
Maintainer reply None cached
Activity 1 comment · opened Sep 7, 2026

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?

On Linux, sandbox.filesystem.denyWrite enforces rules whose wildcards are trailing
(/path/**) and rules that are fully literal, but silently ignores any rule with a
wildcard in the middle of the path (/path/**/file, /path/*/file). No warning is
emitted for the specific rule, and the rule appears in /status as if it were active.

sandbox.filesystem.denyRead does not have this problem. The identical rule shapes, in the
same config section, are all enforced on the read side — including both mid-path forms. So this
is not a platform limitation on what the sandbox can express; the write path is missing
matching that the read path already performs.

The pattern is also honoured by permissions.deny at the tool layer, so a single rule can
block the Write tool while leaving the identical path writable to any Bash subprocess. That
asymmetry is what makes this dangerous rather than merely surprising: the rule looks like it
is working when tested through the tool, and is not working where it matters.

/status and claude doctor do surface a warning for this (see below), so the limitation is
known; this issue is to report the specific shape, the read/write asymmetry, and the
fail-open behaviour.

Environment

  • Claude Code 2.1.263, commit 37ae3f38d765
  • Platform linux-arm64, node:22-bookworm-slim in Docker (Docker Desktop for Mac)
  • Sandbox enabled via managed settings at /etc/claude-code/managed-settings.json, with

sandbox.enabled: true, sandbox.allowUnsandboxedCommands: false, and
sandbox.enableWeakerNestedSandbox: true (required for bubblewrap nested inside Docker)

Hypothesis

The denyRead results rule out the obvious explanation. Mid-path globs are clearly
expressible — the read side matches them and denies with EACCES — so this is not a limit on
what bubblewrap can represent.

What the errnos suggest instead is that denyWrite has two enforcement paths and no glob
matcher on either:

  • literal path → EACCES, a per-path deny
  • trailing ** → EROFS, the subtree remounted read-only

A trailing ** reduces cleanly to "this directory prefix", and a literal path needs no
matching at all. A mid-path wildcard fits neither, so it appears to fall through both branches
and be dropped, where the read side would have run it through its path matcher.

If that is right, the fix is to route denyWrite rules through the same matcher denyRead
already uses. A cheap interim fix would be to validate rules at load time and refuse to start
when a rule matches neither branch, so the failure is visible instead of silent.

The existing warning

/status shows:

⚠ Glob patterns in sandbox permission rules are not fully supported on Linux

and claude doctor gives the detail:

- Glob patterns in sandbox permission rules are not fully supported on Linux
  Fix: Found 17 pattern(s): Edit(//workspace/**/.git/hooks/**), Edit(//workspace/**/.git/config),
  Edit(//workspace/**/.mcp.json) (14 more). On Linux, glob patterns in Edit/Read rules will be ignored.

The warning correctly identifies the affected rules. Two things would make it more actionable:

  1. "will be ignored" overstates it, in a way that hides the real risk. The listed Edit

rules are still enforced at the tool layer; it is only their sandbox-derived counterpart
that is dropped. The precise statement is "these rules will not be enforced against
subprocesses" — which is the part a reader needs to act on, and is easy to miss when
testing the rule through the agent appears to confirm it works.

  1. It says Edit/Read, but Read rules are fine and denyWrite is not mentioned. Per the

tables above, the read side honours globs; explicit denyWrite rules have the defect and
aren't named. Naming the write surfaces, and listing which of the 17 are affected, would
let a config author fix them.

Why it matters

The documented guidance for protecting host-executed configuration inside a workspace —
git hooks, .pre-commit-config.yaml, .envrc, .npmrc, shell startup files — is a
denyWrite rule. In a multi-repo workspace those paths only exist under a wildcard
(/workspace/<any-repo>/.git/hooks), so the wildcard form is the only maintainable way to
express them; enumerating every repo literally does not scale and silently rots as repos are
added.

With mid-path globs dropped, those paths are writable by any subprocess.

Prior report

Submitted through /bug on 2026-09-06, receipt 613a9fb3-3860-4b55-8968-d566e485f315.
Filing here for a trackable reference.

What Should Happen?

/workspace/**/probe.txt should block a write to /workspace/docs/probe.txt, exactly as the
same rule blocks a read.

Failing that, an unenforceable rule should be rejected loudly — a startup error, or a
per-rule warning naming the rule — rather than accepted and ignored. Silently dropping a deny
rule fails open, which is the wrong direction for a security control.

Error Messages/Logs

Steps to Reproduce

Each rule is injected on its own via --settings so the rules cannot interact, and every probe
writes to the same target in an existing directory, so a missing parent cannot confound the
result with ENOENT:

probe() {  # probe <label> <denyWrite-rule> <target>
  claude -p "Run this exact bash command and report only its stdout, verbatim: \
node -e 'try{require(\"fs\").appendFileSync(\"$3\",\"\");console.log(\"WRITABLE\")} \
catch(e){console.log(\"blocked:\"+e.code)}'" \
    --allowedTools Bash \
    --settings "{\"sandbox\":{\"filesystem\":{\"denyWrite\":[\"$2\"]}}}"
  rm -f "$3"
}

T=/workspace/docs/probe.txt        # docs/ already exists
probe literal    /workspace/docs/probe.txt "$T"
probe mid-star2  '/workspace/**/probe.txt'  "$T"
probe mid-star1  '/workspace/*/probe.txt'   "$T"
probe trail-star2 '/workspace/docs/**'      "$T"
probe bare-star2  '/workspace/**'           "$T"

The denyRead comparison uses the same harness with denyWrite swapped for denyRead, the
target seeded with printf 'seed\n' > "$T" beforehand, and readFileSync in place of
appendFileSync.

Results

sandbox.filesystem.denyWrite:

| Rule | Target | Result |
|---|---|---|
| /workspace/docs/probe.txt | /workspace/docs/probe.txt | blocked:EACCES ✅ |
| /workspace/**/probe.txt | /workspace/docs/probe.txt | WRITABLE ❌ |
| /workspace/*/probe.txt | /workspace/docs/probe.txt | WRITABLE ❌ |
| /workspace/docs/** | /workspace/docs/probe.txt | blocked:EROFS ✅ |
| /workspace/** | /workspace/docs/probe.txt | blocked:EROFS ✅ |

sandbox.filesystem.denyRead, same rule shapes — all enforced:

| Rule | Target | Result |
|---|---|---|
| /workspace/docs/probe.txt | /workspace/docs/probe.txt | blocked:EACCES ✅ |
| /workspace/**/probe.txt | /workspace/docs/probe.txt | blocked:EACCES ✅ |
| /workspace/*/probe.txt | /workspace/docs/probe.txt | blocked:EACCES ✅ |

Claude Model

Opus

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

Cursor

Additional Information

_No response_

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗