[BUG] Sandbox deny rules with mid-path globs cause E2BIG on Linux — per-file bwrap expansion instead of per-directory overlay

Resolved 💬 4 comments Opened Apr 10, 2026 by nbadmbira Closed May 23, 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?

With sandbox.enabled: true in my org's managed-settings.json, consider this simple Claude Code session:

! cat .claude/settings.json
  ⎿  {
       "permissions": {
         "deny": [
             "Read(foo/*/data/cache/**)"
         ]
     }}

❯ use Bash to echo "hello world"

● Bash(echo "hello world")
  ⎿  Interrupted · What should Claude do instead?

● The command was interrupted. What would you like me to do instead?

Bash simply croacks. If I check Claude's debug log:

2026-04-10T23:35:41.677Z [DEBUG] Shell exec error: E2BIG: argument list too long, posix_spawn '/usr/bin/zsh'
2026-04-10T23:35:41.679Z [DEBUG] Bash tool error (131ms): Shell command failed

But when I delete that single deny rule, Bash works fine!

Summary (everything below is from Claude, but has been verified and is accurate!)

On Linux, when permissions.deny contains Read(...) rules with mid-path glob wildcards (e.g., Read(foo/*/data/cache/**)), the sandbox appears to expand the glob against the actual filesystem and generate a separate bubblewrap bind-mount argument for every matching file. In a project with thousands of generated/cached files under the matched path, the resulting bwrap argument list exceeds Linux's ARG_MAX, and posix_spawn fails with E2BIG.

This does not occur on macOS.

Environment

  • Claude Code v2.1.101
  • Linux (RHEL-family), not containerized
  • ARG_MAX: 2,097,152 (2MB)
  • MAX_ARG_STRLEN: 131,072 (128KB) — PAGE_SIZE * 32
  • ulimit -s: 8192
  • Shell snapshot size: ~116KB (from --debug log)
  • Shell environment size: ~3.7KB
  • Sandbox enabled via managed-settings.json (sandbox.enabled: true, sandbox.failIfUnavailable: true)
  • Bubblewrap + socat

Steps to reproduce

  1. On Linux with sandbox enabled, have a project containing a directory tree with thousands of files (e.g., App cache: foo/*/data/cache/ — mine has ~2,700 entries across two subdirectories)
  2. Add a single deny rule to .claude/settings.json:

``json
{ "permissions": { "deny": ["Read(foo/*/data/cache/**)"] } }
``

  1. Start Claude from the project directory
  2. Run any Bash command (e.g., echo hello)

Result: Every Bash command fails silently. The model sees "Interrupted" and hallucinates explanations. Only claude --debug reveals the real error:

Shell snapshot created successfully (118918 bytes)
Spawning shell without login (-l flag skipped)
Shell exec error: E2BIG: argument list too long, posix_spawn '/usr/bin/zsh'
Bash tool error (509ms): Shell command failed

Diagnosis

I isolated the cause to a single permissions.deny rule:

"Read(foo/*/data/cache/**)"

Key observations:

  • Only Read(...) deny rules trigger it. The following rules with identical or broader wildcards do NOT cause E2BIG, even when present simultaneously:
  • Edit(**/data/cache/**) — no problem
  • Write(**/data/cache/**) — no problem
  • Grep(foo/*/data/cache/**) — no problem

This suggests only Read (and possibly Edit) deny rules are translated into OS-level sandbox filesystem restrictions. Write, Grep, and Bash deny rules appear to operate only at the tool permission level.

  • Rooted patterns are fine. Read(build/**) works (only 8 files in build/). The issue is specifically mid-path wildcards that fan out across directories with many files.
  • Patterns starting with ** are fine. Edit(**/data/cache/**) doesn't trigger it, suggesting the expansion logic skips patterns with no concrete prefix before the first glob character.
  • Explicit paths fix it. Replacing the wildcard with the actual directory names works. See "Workaround" below.
  • The problem scales with file count. find foo/*/data/cache/ | wc -l returns 2,704. At ~100+ bytes per bwrap argument, that's ~270KB of additional argv — enough to push past OS limits when combined with the ~116KB shell snapshot.
  • Only occurs on Linux. The identical settings work fine on macOS, which uses sandbox-exec profiles instead of bubblewrap bind-mounts.
  • Only occurs in project directories. Running from ~ (no project-level deny rules) works fine. The managed-settings deny rules alone don't trigger it.
  • getLinuxGlobPatternWarnings() — the /sandbox UI displays warnings about glob patterns not working on Linux, so this limitation is partially known. But the glob expansion still proceeds and causes the crash.

Workaround

Replace mid-path wildcards with explicit directory names:

- "Read(foo/*/data/cache/**)",
+ "Read(foo/bar/data/cache/**)",
+ "Read(foo/baz/data/cache/**)"

Suggested fix

When building bubblewrap arguments from denyRead glob patterns, resolve to the shallowest matching directories and emit one --tmpfs overlay per directory, rather than expanding ** into per-file bind-mounts. For foo/*/data/cache/**, the correct bwrap output should be two directory overlays — not 2,704 individual file entries.

What Should Happen?

Claude's ability to use Bash should not be impaired by this single trivial deny rule.

Error Messages/Logs

Documented above

Steps to Reproduce

Documented above

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.101 (Claude Code)

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

iTerm2

Additional Information

_No response_

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗