[BUG] WSL2: Bash tool fails with E2BIG because Claude wraps bubblewrap in single /bin/bash -c string exceeding Linux MAX_ARG_STRLEN
Preflight Checklist
- [x] I have searched existing issues — closest related but distinct: #4488, #17727, #35986
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code
What's Wrong?
On WSL2, every Bash tool call fails immediately with Exit code 126 and "Request interrupted by user for tool use" (no actual user interruption occurs) when sandbox.enabled: true is set with a non-trivial filesystem.denyRead list and/or permissions.deny Read(...) patterns.
Root cause (confirmed via strace): Claude Code constructs a bubblewrap (bwrap) sandbox wrapper command and passes it as a single -c argument to /bin/bash. This single argument exceeds Linux's MAX_ARG_STRLEN = PAGE_SIZE × 32 = 128 KB limit (defined in include/uapi/linux/binfmts.h), causing posix_spawn to return E2BIG.
Important: This is NOT the ARG_MAX (2 MB) limit. The kernel enforces a separate per-argument size cap via MAX_ARG_STRLEN.
Baseline checks (all normal — ruling out environmental causes):
getconf ARG_MAX= 2097152ulimit -s= 8192- Process
environsizes are in the single-digit KB range - Bare
bash -c 'echo hello'works node -e "child_process.spawn('bash', ['-c', 'echo hello'])"works- Same Claude binary, smaller workspace / smaller deny list → works
Independent verification that narrowed this to the harness/bwrap layer:
- Both the VS Code extension and the standalone CLI (
claude --dangerously-skip-permissions, same underlyingclaudebinary) reproduce the failure identically. - VS Code window reload, full VS Code restart (process kill + relaunch), and switching permission mode across
plan/default/acceptEdits/ bypass all fail to resolve it. - User-side PreToolUse Bash hooks were independently verified as NOT the cause: invoking them directly with the documented stdin JSON payload returns exit 0.
- No permission prompt appears in the VS Code UI during failure — the tool call is terminated before any permission layer is reached, consistent with a low-level
posix_spawnfailure.
What Should Happen?
Bash tool calls should execute regardless of how many sandbox deny/allow rules the user has configured. The bwrap invocation should not hit kernel argument length limits.
Suggested fixes (in order of preference):
- Invoke
bwrapdirectly with an argv array instead of wrapping it in/bin/bash -c "<string>". The current design forces the entire bwrap configuration into a single argument. - Pass bwrap configuration via
--args-fd/--args <file>/ stdin so the sandbox policy does not compete with argv space. - Deduplicate merged deny paths. Observed duplication:
Read(//**/foo)andRead(**/foo)pattern pairs inpermissions.denyappear to each expand into their own--tmpfsentries. - Reconsider the unconditional injection of proxy environment variables (
HTTP_PROXY,HTTPS_PROXY,ALL_PROXY,FTP_PROXY,GIT_SSH_COMMAND,DOCKER_*,CLOUDSDK_PROXY_*,GRPC_PROXY, etc.) — these contribute ~25--setenventries per Bash call regardless of user setting.
Error Messages/Logs
Shape of the failing execve (sanitized; proxy env vars and internal mounts summarized):
execve("/bin/bash", ["/bin/bash", "-c",
"bwrap --new-session --die-with-parent --unshare-net
--bind <claude_http_sock> <claude_http_sock>
--bind <claude_socks_sock> <claude_socks_sock>
--setenv SANDBOX_RUNTIME 1
--setenv TMPDIR <tmpdir>
[~25 proxy-related --setenv entries omitted: HTTP_PROXY, HTTPS_PROXY,
ALL_PROXY, FTP_PROXY, RSYNC_PROXY, DOCKER_HTTP_PROXY, DOCKER_HTTPS_PROXY,
CLOUDSDK_PROXY_*, GRPC_PROXY, GIT_SSH_COMMAND,
CLAUDE_CODE_HOST_HTTP_PROXY_PORT, CLAUDE_CODE_HOST_SOCKS_PROXY_PORT, ...]
--ro-bind / /
[several --bind entries for /tmp/claude-*, .npm/_logs, .claude/debug, workspace]
[many --tmpfs / --ro-bind entries, one per denied path in
sandbox.filesystem.denyRead + permissions.deny Read(...), which is the
dominant contributor to the combined argument length]
<user workload>"
], 0x... /* ~58 vars */
) = -1 E2BIG (Argument list too long)
Captured with:
strace -f -e trace=execve -s 2000 -o /tmp/claude-trace.txt -- \
claude --dangerously-skip-permissions
# → accept warning → send "echo hello" → observe failure → Ctrl+D
grep "execve.*bash" /tmp/claude-trace.txt
grep "E2BIG" /tmp/claude-trace.txt
Steps to Reproduce
- Use a recent WSL2 kernel on Windows.
- In
~/.claude/settings.json, enable sandbox with a realistic deny configuration. Minimal reproducer uses roughly this shape — the total rule count across both sections matters more than specific paths:
``json``
{
"permissions": {
"deny": [
"Read(//**/.env)", "Read(**/.env)",
"Read(//**/.env.local)", "Read(**/.env.local)",
"Read(//**/*.pem)", "Read(**/*.pem)",
"Read(//**/*.key)", "Read(**/*.key)",
"Read(//**/secrets/**)", "Read(**/secrets/**)"
// ... total ~30 patterns typical of a conservative production config
]
},
"sandbox": {
"enabled": true,
"filesystem": {
"denyRead": [
"**/.env", "**/.env.*",
"**/secrets/**", "**/*.pem", "**/*.key"
// ... plus typical host-level path patterns
],
"allowRead": [
"**/.env.example", "**/.git/config"
]
}
}
}
- Start a fresh Claude Code session (VS Code extension or CLI:
claude --dangerously-skip-permissions).
- Send any message that invokes the Bash tool, e.g.
run echo hello.
- Observe: the tool call fails immediately with
Exit code 126and[Request interrupted by user for tool use].
Minimal workaround confirmed: collapsing sandbox.filesystem.denyRead to a single broad entry (e.g. ["~/"]) with narrow allowRead carve-outs for the binaries/caches/paths actually needed reduces bwrap argv below the 128 KB threshold and restores Bash tool functionality.
Claude Model
Opus
Is this a regression?
I don't know
Claude Code Version
2.1.114 (Claude Code)
Platform
Anthropic API
Operating System
Other Linux
Terminal/Shell
WSL (Windows Subsystem for Linux)
Additional Information
Environment (minimal)
- Recent Microsoft WSL2 kernel (6.x series) on Ubuntu-based distro
- bubblewrap via standard distro package
getconf PAGE_SIZE=4096, soMAX_ARG_STRLEN ≈ 131072 bytes (128 KB)
Hypothesis on argument-length growth
The failing command string consists mostly of three categories:
- Proxy
--setenv(~25 entries) — auto-injected by Claude Code, not user-configurable. Roughly 4-6 KB. - Required
--bind/--ro-bind(6-8 entries) — workspace + Claude's own runtime sockets / debug paths. Not user-adjustable. --tmpfsper denied path — this is the dominant growth factor. Bothpermissions.deny Read(...)patterns andsandbox.filesystem.denyReadentries expand here, apparently without deduplication.
Users who enable a conservative sandbox with explicit denylists routinely have enough deny patterns (30+ patterns across permissions.deny and sandbox.filesystem.denyRead) to exceed the 128 KB threshold.
Related issues
- #4488 — E2BIG in CI environments
- #17727 — Linux sandbox: bwrap invocation bug (mounts non-existent paths)
- #35986 —
sandbox.enabled: falsebeing ignored in 2.1.79
Other tools for comparison
- Cursor uses Landlock + seccomp directly (no bubblewrap wrapper command) — structurally immune to this failure mode. See Cursor's agent sandboxing blog post.
- OpenAI Codex uses a bubblewrap backend and reports structurally similar issues: openai/codex#14976, openai/codex#16018.
Severity
Medium-High. Users with realistic permissions.deny / sandbox.filesystem.denyRead configurations hit this on every Bash tool call, which blocks any shell-dependent workflow. The user-side workaround (replacing granular deny patterns with a single broad deny plus narrow allowRead carve-outs) resolves the symptom but reduces isolation granularity.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗