Bash-tool snapshot-source wrappers accumulate during subagent dispatch, triggering process-cascade OOM

Resolved 💬 3 comments Opened Apr 17, 2026 by tmlgcq Closed May 28, 2026

Bash-tool snapshot-source wrappers accumulate during subagent dispatch, triggering process-cascade OOM

Summary

Under sustained subagent work in superpowers:subagent-driven-development-style skills, the /bin/bash -c 'source <snapshot>.sh ... && eval ...' wrapper processes that Claude Code uses to execute every Bash tool call accumulate faster than they are reaped. The accumulating wrappers each source the same large shell-snapshot file and consume CPU; load average climbs from background (~2) to several hundred over the course of several minutes. On the first incident this drove the system past the point where tmux could respond to input and required a hard reboot.

The issue reproduces inside a single subagent dispatch when that subagent's task runs a moderately-spawny bash-based test harness (one that forks many git/jq/mktemp per test). Today (2026-04-17) I experienced this twice:

  1. ~2 hours ago the original cascade took my machine down and required a hard reboot (tmux OOM-killed, system unresponsive).
  2. After reboot, running a deliberately smaller, more carefully-serialized workload captured a live in-progress reproduction before I intervened: a single Task 8 subagent produced 16+ bash -c 'source snapshot...' wrapper processes all at 5–107% CPU within ~1–2 minutes of dispatch. I killed it via TaskStop + pkill -9 before it escalated.

Related but distinct existing issues

This is not a duplicate of any of the following, but shares the wrapper pattern as a structural amplifier:

Closest relative. Same /bin/bash -c 'source snapshot...' && eval ... && pwd -P >| /tmp/claude-*-cwd wrapper template. Same Fedora 43 / Linux 6.19 platform. Same escalation to OOM territory.
Difference: #37490 is a respawn loop — a single failing command being respawned infinitely by Claude Code's own runtime after the pipe chain breaks. In our case there is no failing command and no respawn loop; wrappers accumulate during legitimate, successful subagent work because the subagent issues many Bash tool calls in rapid succession and the wrappers are not throttled or waited on between invocations.

Same platform class (Linux), same end-state (kernel OOM killer, thousands of bash children).
Difference: #47978 is recursion inside a single wrapper — command ""command_not_found_handlemise()command "" → … The fork bomb is internal to one tool call. In our case each wrapper is a legitimately-issued separate Bash tool invocation with a different eval payload; recursion is not involved. I don't use mise and have no command_not_found_handle in my snapshot.

Same concern category (orphaned bash descendants), different lifecycle.
Difference: #43944 is about run_in_background: true processes (dev servers, builds) surviving past session end — a post-session leak. Our issue is during-session: wrappers pile up mid-subagent and are actively running (non-zombie, consuming CPU), not orphans of completed work.

Why I believe ours is a distinct fourth failure mode of the wrapper pattern:

| Root cause | Example issue | Fork source |
|---|---|---|
| Runtime respawn loop on failure | #37490 | Claude Code respawning after pipe break |
| Recursion inside a single wrapper | #47978 | command_not_found_handle re-entering |
| Session-end reparenting leak | #43944 | Backgrounded dev servers orphaned |
| Concurrent-dispatch accumulation (this report) | n/a | Subagent issuing many Bash calls with no concurrency cap or wait |

The first three all reach the same cascade through the bash -c 'source snapshot...' wrapper; this is the fourth reported way to get there.

Environment

  • Claude Code version: 2.1.113
  • OS: Fedora Linux 43 (Workstation Edition)
  • Kernel: Linux 6.19.12-200.fc43.x86_64
  • Bash: GNU bash 5.3.0(1)-release
  • CPU: 11th Gen Intel Core i7-1165G7 @ 2.80 GHz (4 cores / 8 threads, max 4.7 GHz)
  • RAM: 30 GiB, 8 GiB swap
  • User ulimit -u: 124864 (Fedora default — permissive enough that the storm reaches "load average several hundred" before PID exhaustion)
  • Shell snapshot directory: ~/.claude/shell-snapshots/ — 7 files, 92–117 KB each, 1222–1521 lines per file; the current snapshot (sourced by every wrapper in the storm) is 1521 lines

Reproduction conditions

  1. Claude Code conversation running a subagent-driven workflow (specifically the superpowers:subagent-driven-development skill).
  2. The dispatched subagent's task includes running a bash-based test harness that spawns many subprocesses per test. My harness: ~50 deterministic tests, each forking ~15–20 child processes (mktemp, git init, git config, git add, git commit, git checkout, jq, the hook-under-test), for ~600–800 process spawns in a full run.
  3. The subagent runs the harness, reads the output, and re-runs the harness inline to investigate test failures it wants to diagnose. Each re-run is issued as a fresh Bash tool call; each fresh call gets wrapped in a new bash -c 'source snapshot...' invocation.
  4. A second Claude Code session was running concurrently on a different project (same machine, different repo, no shared hooks), also doing subagent-heavy superpowers-style work. Both sessions contributed to aggregate pressure on the original crash.

Observed behavior (today's live capture)

Between 17:47 and ~17:49 local time today, I dispatched a single Task 8 subagent via the superpowers:subagent-driven-development skill. I became concerned about 1–2 minutes later when process count started climbing, and captured ps -u tmlgcq -o pid,pcpu,pmem,comm,args --sort=-%cpu before killing the runaway:

PID   %CPU  COMMAND  ARGS
40812 107   bash     /bin/bash -c source /home/tmlgcq/.claude/shell-snapshots/snapshot-bash-1776462469922-uqjqdo.sh 2>/dev/null || true && shopt -u extglob 2>/dev/null || true && eval 'HOOK_SCRIPT=... <50-test harness block>' && pwd -P >| /tmp/claude-abbb-cwd
40675  50   bash     /bin/bash -c source /home/tmlgcq/.claude/shell-snapshots/snapshot-bash-1776462469922-uqjqdo.sh ...
40674  31   bash     ... (same)
40673  23   bash     ... (same)
40672  20   bash     ... (same)
40671  15   bash     ... (same)
40670  13   bash     ... (same)
40669  12   bash     ... (same)
40668  11   bash     ... (same)
40667   9   bash     ... (same)
40666   8   bash     ... (same)
40665   8   bash     ... (same)
40664   7   bash     ... (same)
40663   7   bash     ... (same)
40662   6   bash     ... (same)
40661   6   bash     ... (same)

All 16+ wrappers source the same snapshot file and eval the same shell test block. The process count was still climbing when I ran TaskStop followed by pkill -9 -f "snapshot-bash-<id>". The aggregate CPU across these 16 processes exceeded 400% on an 8-thread machine — i.e., fully saturated and queueing.

Observed behavior (original crash, 2026-04-17 earlier today, ~2 hours before the capture above)

Timeline of the first incident, reconstructed from memory and what I saw in tmux before it froze:

  • t=0: Subagent dispatched as part of Hook test-harness work (smaller harness than today's, ~30 tests). Initial system load normal (~2).
  • t+~2 min: Noticed tmux becoming sluggish; checked top and saw several dozen bash -c 'source snapshot...' processes. Load average ~10.
  • t+~3 min: Load climbing rapidly. Tmux command line not responding. Second Claude session in another tmux window was also doing subagent work.
  • t+~5 min: Load average past 600; system essentially unresponsive. Unable to interact with either Claude session or any other terminal. Kernel began OOM-killing; tmux was one of the casualties.
  • Recovery: Hard reboot via the power button. No data loss in the repos (trap-based tmp-dir cleanup in the test harness fired cleanly enough that no orphans were on disk after reboot).

So the escalation on the original crash was on the order of several minutes, not seconds — but once past ~load 100 it becomes self-reinforcing because scheduler latency prevents successful reaping, which compounds the accumulation.

Why this matters

  • The wrapper pattern (/bin/bash -c 'source snapshot...' && eval ... && pwd -P >| /tmp/claude-*-cwd) is inherent to how Claude Code executes Bash tool calls. Any project with a legitimately-spawny test harness runs afoul of it as soon as a subagent is asked to run the harness or the test runner.
  • ulimit -u is not a sufficient safeguard on Linux defaults (124864). The storm reaches "system-unusable" territory long before the PID cap is hit.
  • Sourcing a 1521-line snapshot file on every single Bash tool call is a fixed overhead that compounds badly when a subagent issues many calls in sequence. Shell snapshots have grown steadily on my system: 1222 → 1521 lines over six weeks.
  • Subagents have no visibility into overall system load or their own process-tree size, so there is no natural backoff mechanism at the subagent layer.

Suggested mitigations

In rough order of impact-to-complexity ratio:

  1. Cap concurrent Bash-tool wrapper processes per Claude session. A runtime-level semaphore (e.g., max 8 concurrent bash -c wrappers per Claude PID, configurable) would prevent the storm at its root without limiting subagent usefulness. This is the closest fix to the problem.
  2. Have the Bash tool wait on wrappers before issuing the next one from the same subagent. If the previous wrapper hasn't exited, don't dispatch another — block the subagent until the current one finishes. This serializes tool calls within a subagent while still allowing cross-subagent parallelism.
  3. Per-subagent cgroup with pids.max. Spawn each subagent inside its own cgroup with a hard PID cap (e.g., 256). If the subagent breaches, it dies cleanly rather than taking the system down. This is defense-in-depth for all four of the fork-source categories above, not just this one.
  4. Lighter wrapper for simple commands. Commands that don't need the full shell snapshot (git status, ls, cat, echo) could be invoked directly via execvp rather than through the bash -c 'source ... && eval ...' template. Larger surgery but would halve the fork rate for the common case.
  5. Snapshot hygiene / trim. My snapshots have grown from 1222 to 1521 lines over six weeks. Periodic trim of dead aliases/functions would reduce per-wrapper cost and per-wrapper source time.
  6. Expose concurrency telemetry to subagents. Include current outstanding-Bash-call count in the tool-call context so subagents can self-throttle (or at least surface to users).

Workarounds currently in use

  • Switched the orchestrating session to serial Agent dispatch (one at a time) rather than the skill's default parallel dispatch.
  • Stopped re-running test harnesses inside subagents — captured expected output in the main session and passed it in the prompt. Reduced re-dispatch amplification.
  • Running only one Claude Code session at a time on this machine.

These reduce fanout but don't fix the underlying behavior. A fresh, carefully-serialized session still reproduced the storm today at smaller scale, which is what allowed me to capture the ps output above.

Artifacts available on request

  • Full ps output captured during today's reproduction (above, slightly truncated for width; full version available).
  • Transcript of the subagent that triggered today's reproduction.
  • Shell snapshot file (1521 lines, the one that was being sourced by every wrapper).
  • Hook-under-test source and test harness shape (private repo; can describe the harness structure in detail or share extracts).
  • Kernel dmesg output from the original crash, if I can recover it from journalctl (may not have survived the reboot).

Happy to attach any of these.

View original on GitHub ↗

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