[BUG] Bash tool fails with spawn E2BIG — sandbox profile size scales with working-tree file count, gated by git-repo detection

Status Open
Reported on v2.1.210
Maintainer reply None cached
Activity 1 comment · opened Jul 16, 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?

Every Bash tool invocation fails immediately with spawn E2BIG (even a no-op like true) whenever cwd is inside a git repository with a non-trivial working tree. The failing argument is the compiled sandbox filesystem policy, not the command — the error reports "187 filesystem deny paths" and a ~1-1.1MB compiled profile, enough to exceed the OS argument-length limit.

Through extensive testing across six different locations (full comparison table in Steps to Reproduce), the failure appears gated on two conditions together: (1) cwd is inside a git repository, and (2) that repository's working tree is non-trivially sized. A non-git directory never fails regardless of size (tested up to 377,000+ files). A git repository containing a single file works fine. Every git repository we tested with 148+ working-tree files failed, regardless of the repository's purpose, .claude/ project configuration content, or size beyond that point (tested from 148 up to ~40,000 files, all failing identically).

We were not able to identify the exact internal mechanism — we ruled out several specific, plausible hypotheses directly (see Steps to Reproduce), including git-internals/reflog file count, .claude/commands presence, per-project Claude Code session history, and secret-filename pattern matching against the working tree. The empirical pattern itself is robust and reproducible across independent process launches; the specific code path responsible needs someone with source access to identify.

What Should Happen?

Bash commands should execute normally regardless of working-tree size, as long as cwd is a git repository with normal project content. The compiled sandbox filesystem policy should stay well under the OS argument-length limit (ARG_MAX) regardless of how many files exist in the project.

Error Messages/Logs

Repo A (interactive session):
Could not start /bin/zsh: the command line plus environment exceed the OS exec argument limit (E2BIG). At spawn: command line 1.1MB across 3 args (largest single arg 1.1MB); environment 4KB across 70 vars (largest: PATH at 1015 bytes). The Bash sandbox profile adds 187 filesystem deny paths to every command.

Repo B (scheduled batch-agent run, separate process launch):
Could not start /bin/zsh: the command line plus environment exceed the OS exec argument limit (E2BIG). At spawn: command line 1MB across 3 args (largest single arg 1MB); environment 3.7KB across 67 vars (largest: PATH at 903 bytes). The Bash sandbox profile adds 187 filesystem deny paths to every command.

Repo C (fresh session):
Could not start /bin/zsh: the command line plus environment exceed the OS exec argument limit (E2BIG). At spawn: command line 1MB across 3 args (largest single arg 1MB); environment 2.6KB across 52 vars (largest: PATH at 641 bytes). The Bash sandbox profile adds 187 filesystem deny paths to every command.

Repo D (fresh session):
Could not start /bin/zsh: the command line plus environment exceed the OS exec argument limit (E2BIG). At spawn: command line 1.2MB across 3 args (largest single arg 1.2MB); environment 2.8KB across 54 vars (largest: PATH at 693 bytes). The Bash sandbox profile adds 190 filesystem deny paths to every command, 3 of them for registered git worktrees, which grow this list without bound.

All four texts above were captured from genuinely independent process launches. Note Repo D's message explicitly breaks out 3 of its 190 deny paths as attributable to registered git worktrees — the first direct, product-surfaced confirmation that worktrees contribute to this count (see Steps to Reproduce and Additional Information for how this reconciles with our own worktree-count testing, which found no effect in the two repos we checked that each had only one worktree).

Steps to Reproduce

  1. Use Claude Code (tested on both v2.1.210 and v2.1.211 — both fail identically) on macOS, sandbox mode enabled, inside an enterprise deployment with a centrally-managed permission/deny policy applied on top of personal settings (see Additional Information for scoping).
  2. Test the Bash tool across locations with varying working-tree file counts. We tested six:

| Location | Git repo? | Working-tree files (excl. .git) | Result |
|---|---|---|---|
| Non-git parent directory | no | 377,261 | works |
| Repo A (production config repo) | yes | 1,224 | fails |
| Repo B (small monitoring/automation repo) | yes | 148 | fails |
| Repo C (infra project with a large dependency tree) | yes | 29,389 | fails |
| Repo D (deployment manifests repo) | yes | 39,904 | fails |
| Freshly-initialized minimal repo (1 empty commit, no remote) | yes | 1 | works |

  1. This let us directly rule out (or, in one case, confirm as a minor factor) several specific hypotheses:
  • Command content: a bare true fails identically to complex commands.
  • Git ref/branch count: git pack-refs --all --prune (consolidating hundreds of loose refs into one packed file) had no effect — it only changes ref storage format, not count.
  • Worktree count: confirmed via git worktree list in two of the four repos — each has exactly ONE worktree (the main one), no extras, yet both still fail, showing worktree count is not the dominant driver. However, Repo D's own error message directly confirmed 3 of its 190 deny paths are attributable to "registered git worktrees" — so worktrees ARE a real, product-confirmed contributor, just a small one (3 entries) relative to the ~1MB+ overshoot, and not present at all in the other three failing repos.
  • Git-internals/reflog file count: one repo had 612 files under .git, ~545 of them stale reflog files (.git/logs/refs/**, one per branch ever locally checked out — never removed by git pack-refs, and not removed by a plain git gc either, since default reflog expiry only covers entries older than ~90 days). We force-expired all reflogs (git reflog expire --expire=now --all) then deleted the resulting empty files (find .git/logs -type f -empty -delete), cutting .git's file count from 612 to 56 (91% reduction). Bash still failed identically afterward, tested from a genuinely fresh process — ruling this out as the dominant driver.
  • .claude/commands/.claude/skills presence: one failing repo's .claude/ directory contained only settings.json + settings.local.json — no commands, skills, or hooks — and it still failed.
  • Per-project Claude Code session history: one failing repo had exactly 1 prior Claude Code session in ~/.claude/projects/<project>/ (fewer than a working non-git test case had) and still failed.
  • Secret-filename pattern matching: the org's centrally-managed policy includes several **/-anywhere glob deny rules matching filenames like .env, credentials.json, secrets.yaml, *.pem, *.key, etc. We hypothesized these might be expanded against the working tree, scaling with the number of matches. Directly countertested by searching for matching filenames in every location above: the non-git directory that works fine has the most matches (23) of anywhere tested; three of the four failing repos have zero matches. After later identifying the specific policy change responsible (see Additional Information), we extended this test: comparing file counts matching the OLD pattern set (3 broad substring-match patterns) against the NEW pattern set introduced by that change (12 narrow explicit-suffix patterns) across all six locations found the NEW patterns match zero files everywhere — working and failing locations alike — and the OLD pattern's matches also show no correlation with pass/fail (its highest count, 51, is in a location that works fine). This conclusively rules out literal pattern matching, old or new, as the mechanism.
  1. What survives every counter-test as the dominant factor: total working-tree file count, but only when cwd is inside a git repository. Outside a git repo, file count appears irrelevant (377,261 files, no failure). Inside one, every repo we tested with 148+ files failed; the single-file repo did not. Registered git worktrees are a separate, real, but minor additive contributor (confirmed directly in one repo's own error message), and the count of distinct recursive **/-anywhere patterns in the active policy appears to be a further contributor independent of literal matches (see Additional Information).

Claude Model

Sonnet (default)

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.210 (this session) and 2.1.211 (the latest available at time of testing) — confirmed via a genuine version upgrade and fresh-session retest, both fail identically

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

VS Code integrated terminal

Additional Information

  • Root cause identified via source control history: we traced the org's centrally-managed policy repository and found the exact change responsible for the timing. The evening before our first confirmed failure, a policy update replaced a small number of broad substring-match patterns for secret-like filenames (3 patterns matching any filename containing "secret" anywhere, e.g. equivalent to **/*secret*.json/yaml/yml) with a much larger set of narrow, explicit-suffix patterns (12 patterns, e.g. **/secret-values.yaml, **/values-secret.yaml, **/*.secret.json, **/*.secrets.yaml, etc.) — applied to both Read and Edit permission rules, for a net of +18 distinct recursive **/-anywhere patterns from this one change alone. This was intended to reduce false-positive blocking of legitimate development files (a reasonable security goal). It was bundled in the same release with an unrelated cleanup that removed ~137 obsolete rules using a permission verb Claude Code has since deprecated, so the release's net rule count went down even as this specific pattern category went up substantially — which is why an engineer who reviewed the release's diff initially pushed back on it being the cause. A long-running Claude Code process that had been active continuously since two days prior kept working normally well into the following afternoon — after this update landed — while every process started fresh afterward failed immediately (consistent with the process-level caching behavior described below).
  • We tested whether the new patterns' file matches explain the increased failures: across all six locations in our comparison table, the new 12-pattern set matches zero files everywhere, working and failing locations alike, and the old 3-pattern set's matches show no correlation with pass/fail either (its highest count is in a location that works fine). This rules out literal pattern matching entirely and points instead to the sheer count of distinct recursive **/-anywhere patterns as the relevant variable — i.e., something in the sandbox construction appears to process the working tree once per distinct recursive pattern at a cost independent of whether that processing finds any matches, so simply having more such patterns increases the chance of exceeding the argument limit once inside a git repository with a non-trivial working tree.
  • Scoping: this reproduces inside an enterprise deployment where Claude Code's managed/remote-settings mechanism (a standard product feature for centrally-managed permission policy) pre-populates several hundred permission entries organization-wide, layered under personal/project settings. That combined baseline is unusually large compared to a bare install, and is likely why this particular deployment hits the limit at working-tree sizes (148+ files) that a lighter-baseline install might not.
  • Partial self-diagnosis already exists for one contributor: Repo D's error message broke out its deny-path count explicitly ("187 → 190, 3 of them for registered git worktrees, which grow this list without bound") and suggested a concrete remediation (git worktree remove/git worktree prune for stale checkouts, then restart Claude Code; or relax the sandbox for the session via /sandbox). This is useful and appreciated — but the same kind of diagnostic breakdown was never surfaced for the much larger contributors we identified (working-tree file count, recursive pattern count) in any of the four failing repos. Extending this same self-diagnosis to cover all contributors would directly address the "silent failure past the limit" problem.
  • Version upgrade tested directly: we upgraded from v2.1.181 to the latest available release (v2.1.211 at time of testing, via claude upgrade) and confirmed via a genuinely fresh process launch that the bug reproduces identically on the latest version — ruling out "stale/outdated CLI" as an explanation independent of the root cause above. (Note: this environment had two separate Claude Code installations at different versions on the same machine; claude upgrade only updates the currently-active one, so any reproduction attempt should confirm claude --version explicitly inside the session being tested.)
  • Process-level caching observation: separately, the compiled sandbox profile appears fixed once a Claude Code process starts, and does not get recomputed for the life of that process — including for sub-agents spawned within it. Applying a fix to an already-running session — whether a version upgrade or a git-level change (the reflog cleanup above) — had no effect on that session's failures; only a genuinely new top-level process launch reflected either change.
  • Independently, a colleague at the same organization traced an earlier instance of this general error class to a personal Read(<home directory>/**)-style recursive allow-glob in their own settings.json — removing it resolved their case. Multiple engineers at the same organization have hit variants of spawn E2BIG over roughly a 6-week span across different Claude Code versions, suggesting the underlying architectural issue (something in the sandbox profile compiler scaling poorly with the number of recursive patterns it needs to represent) recurs in more than one form.
  • Related but distinct prior report: #3836 (same spawn E2BIG error class, but a same-day v1.0.54 regression on Linux, fixed by rolling back to v1.0.53 — a bad release, not this issue).
  • Possibly the same general class of bug as #74081 (and its unresolved predecessor #46461), both Linux/WSL2-specific: those describe the sandbox's bubblewrap backend expanding a recursive Read(dir/**) deny glob into one bind argument per matched file, overflowing Linux's per-argument limit. Our trigger differs in the specifics (gated by git-repo detection, scaling with total working-tree file count and recursive-pattern count rather than confirmed matches against any specific glob), but the general shape — some processing proportional to a large set of filesystem paths, overflowing an OS argument limit — looks like the same family of problem, just via a different code path and on a different platform.
  • Suggested direction: based on the above, the sandbox likely evaluates each distinct recursive **/-anywhere pattern (whether from org policy, personal settings, or elsewhere) by processing the working tree once per pattern, at a cost that scales with tree size regardless of match outcome — rather than compiling all such patterns into a single combined tree walk, or into true OS-level recursive rules that don't require per-pattern enumeration. This would explain both the pattern-count sensitivity we traced to a specific policy change, and the working-tree-size sensitivity from our six-location comparison, without requiring any literal matches. Two independent, low-risk mitigations seem promising: (1) compiling the set of recursive **/-anywhere patterns into a single combined tree walk (or true recursive OS-level rules) instead of one pass per pattern, and (2) passing the generated path list out-of-band rather than as literal argv (as suggested in #74081) so pattern/file count stops being coupled to the OS argument limit at all. We'd also suggest extending the self-diagnosis already present for worktrees to cover all contributors, with a full breakdown of which source — org policy, personal settings, worktrees, working-tree size — is responsible for how much, rather than surfacing detail for only one of them.

View original on GitHub ↗

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