macOS Bash sandbox: seatbelt profile grows one deny entry per registered git worktree → E2BIG kills every sandboxed spawn (profile cached per session)

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Jul 31, 2026

Summary

On macOS, the Bash sandbox (seatbelt) profile adds one filesystem deny entry per registered git worktree, the profile is passed at process spawn, and it is cached for the whole session. In worktree-heavy workflows (multi-agent orchestration with isolation: "worktree" subagents) the profile grows past the OS exec argument limit (ARG_MAX, 1 MiB on macOS) — and from that point every sandboxed Bash spawn in the session fails with E2BIG, including every subagent's (they share the cached profile). Cleanup alone does not recover the session; only a restart rebuilds the profile.

We have hit this three times in two days running a wave-orchestration toolkit that dispatches worktree-isolated Workflow subagents.

Environment

  • Claude Code 2.1.220, macOS (Darwin 25.5.0), VS Code extension + CLI
  • Repo uses Workflow-tool fan-outs with agent({ isolation: 'worktree' }) — typically 3–6 worker worktrees per run, plus reviewer scratch worktrees

Observed behavior

The error surfaced by the Bash tool (paths sanitized):

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 70 vars. The Bash sandbox profile adds 172 filesystem deny paths to every command, 21 of them for registered git worktrees, which grow this list without bound. From another terminal, remove worktrees you no longer need … then restart Claude Code so the profile is rebuilt without them — or relax the Bash sandbox for this session with /sandbox.

Sequence of the three occurrences:

  1. Accumulation across runs (2026-07-30): third Workflow dispatch run of the day; residue worktrees from two prior runs plus a prior session pushed the profile over the limit mid-day. Confirmed subagent scope with a minimal probe agent — identical E2BIG.
  2. Restart with stale registrations (2026-07-31, twice): the IDE window was closed while a Workflow with worktree-isolated agents was mid-run. The relaunched session rebuilt the profile at startup from the stale registrations (killed workflows never clean their worktrees), so the fresh session was born over the limit — every sandboxed spawn dead from the first command.

Key aggravators:

  • The profile is cached per session. git worktree remove + git worktree prune fix the population but the running session keeps spawning with the oversized profile — verified live; only a restart recovers. The error text says this, but it makes the failure a two-step recovery (sweep in another terminal, then restart) where the session itself can't self-heal.
  • The failure is total and initially cryptic: once over the limit, diagnosis commands fail too (every spawn dies), and subagents burn their whole budget on spawns that were guaranteed to fail before the first one ran.
  • Stale/prunable registrations count: worktrees whose directories are already gone still contribute entries until git worktree prune + restart.

Suggestions (any one of these would defuse it)

  1. Collapse per-worktree deny entries into a single deny on the common worktrees root (e.g. .claude/worktrees/) — the entries are siblings under one directory in the common case.
  2. Pass the profile out-of-band (temp file) instead of as a spawn argument, removing the ARG_MAX coupling entirely.
  3. Rebuild the profile on worktree-set change (or at least on git worktree prune), so a session can self-heal after a sweep.
  4. Warn before the cliff: surface a count/size advisory while spawns still work (we added our own advisory at 12 registered worktrees after the first incident — by the time E2BIG fires, the session can no longer even run the cleanup commands sandboxed).

Workarounds we use

  • Worktree-population discipline: sweep at every workflow close + a dispatch-time count advisory.
  • After the cliff: sweep + git worktree prune from unsandboxed commands, then restart the session.
  • Considered: sandbox.filesystem.disabled (user-level) — works but trades away the settings.json write protection.

View original on GitHub ↗

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