claude-code - 💡(How to fix) Fix [BUG] WSL2: Bash tool fails with E2BIG because Claude wraps bubblewrap in single /bin/bash -c string exceeding Linux MAX_ARG_STRLEN

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

Error Messages/Logs

Root Cause

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.

Fix Action

Fix / Workaround

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.

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.

Code Example

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)

---

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

---

{
      "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"
          ]
        }
      }
    }
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues — closest related but distinct: #4488, #17727, #35986
  • This is a single bug report
  • 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.

  2. 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:

    {
      "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"
          ]
        }
      }
    }
  3. Start a fresh Claude Code session (VS Code extension or CLI: claude --dangerously-skip-permissions).

  4. Send any message that invokes the Bash tool, e.g. run echo hello.

  5. 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.

extent analysis

TL;DR

The most likely fix is to invoke bwrap directly with an argv array instead of wrapping it in /bin/bash -c "<string>" to avoid exceeding the Linux MAX_ARG_STRLEN limit.

Guidance

  • Identify and reduce the number of --tmpfs entries generated by permissions.deny and sandbox.filesystem.denyRead to minimize argument length growth.
  • Consider deduplicating merged deny paths to reduce the number of --tmpfs entries.
  • Review the unconditional injection of proxy environment variables and consider making them configurable to reduce the number of --setenv entries.
  • Verify that the bwrap command is properly formatted and that the arguments are not exceeding the MAX_ARG_STRLEN limit.

Example

No code snippet is provided as the issue is related to the bwrap command invocation and argument length limits.

Notes

The issue is specific to the WSL2 environment and the bwrap command invocation. The provided workaround of collapsing sandbox.filesystem.denyRead to a single broad entry with narrow allowRead carve-outs can restore Bash tool functionality but reduces isolation granularity.

Recommendation

Apply workaround by invoking bwrap directly with an argv array or passing bwrap configuration via --args-fd / --args <file> / stdin to avoid exceeding the MAX_ARG_STRLEN limit. This approach is preferred as it addresses the root cause of the issue and allows for more granular deny patterns.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix [BUG] WSL2: Bash tool fails with E2BIG because Claude wraps bubblewrap in single /bin/bash -c string exceeding Linux MAX_ARG_STRLEN