[BUG] WSL2: Bash tool fails with E2BIG because Claude wraps bubblewrap in single /bin/bash -c string exceeding Linux MAX_ARG_STRLEN

Resolved 💬 4 comments Opened Apr 20, 2026 by chenlundeaimi Closed May 27, 2026

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 = 2097152
  • ulimit -s = 8192
  • Process environ sizes 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 underlying claude binary) 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_spawn failure.

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):

  1. Invoke bwrap directly 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.
  2. Pass bwrap configuration via --args-fd / --args <file> / stdin so the sandbox policy does not compete with argv space.
  3. Deduplicate merged deny paths. Observed duplication: Read(//**/foo) and Read(**/foo) pattern pairs in permissions.deny appear to each expand into their own --tmpfs entries.
  4. 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 --setenv entries 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

  1. Use a recent WSL2 kernel on Windows.
  1. 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"
]
}
}
}
``

  1. Start a fresh Claude Code session (VS Code extension or CLI: claude --dangerously-skip-permissions).
  1. Send any message that invokes the Bash tool, e.g. run echo hello.
  1. Observe: the tool call fails immediately with Exit code 126 and [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, so MAX_ARG_STRLEN ≈ 131072 bytes (128 KB)

Hypothesis on argument-length growth

The failing command string consists mostly of three categories:

  1. Proxy --setenv (~25 entries) — auto-injected by Claude Code, not user-configurable. Roughly 4-6 KB.
  2. Required --bind / --ro-bind (6-8 entries) — workspace + Claude's own runtime sockets / debug paths. Not user-adjustable.
  3. --tmpfs per denied path — this is the dominant growth factor. Both permissions.deny Read(...) patterns and sandbox.filesystem.denyRead entries 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: false being 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.

View original on GitHub ↗

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