Claude Code main process leaks ~10 MB/s and triggers a system fork-bomb under specific tool-usage patterns; terminal locks up until the process is killed
Summary
The Claude Code main process exhibits a steady ~10 MB/s memory leak
(observed as ~100 MB every 10 s in Activity Monitor) under specific
tool-usage patterns. Memory grows linearly until macOS memory-pressure
throttling kicks in around 20–30 GB. The leaking workload also drives
the system into a fork-bomb state where zsh in another terminal
reports it cannot fork a new process. Recovery requires manually
killing the specific Claude Code PID and running reset on the
affected terminal.
The bug fires on fresh sessions with zero accumulated context too —
not just long sessions. Conversation length is not the trigger.
Environment
- macOS Darwin 25.4.0, 32 GB RAM
- Claude Code versions observed exhibiting the bug: **2.1.126,
2.1.129**, and at least one earlier version (exact number not
recorded). No version-specific pattern noted.
- Operator typically runs ~8 Claude Code instances concurrently
across separate terminals/projects (each leaks independently).
ulimit -uraised to 8000 (default was hitting the per-user
process limit during fork-bomb states).
Smallest known reproducer
A shell script that recursively walks the process tree using pgrep is enough to trigger a fork-bomb when Claude invokes it via the
-PBash tool with run_in_background semantics. The same script run
from an operator-launched terminal completes quickly without
forking — the difference is that when launched from inside the Claude
harness, the script's bash process is itself a descendant of Claude
Code, so its recursive walk eventually descends into its own subshell
tree.
#!/usr/bin/env bash
# This script fork-bombs the system when invoked via Claude's Bash tool,
# but is harmless when run from an operator-launched terminal.
walk() {
ps -p "$1" -o pid,ppid,command 2>/dev/null | tail -n +2
for child in $(pgrep -P "$1" 2>/dev/null); do
walk "$child"
done
}
for pid in $(pgrep claude); do walk "$pid"; done
To reproduce:
- Save the script as
tree.shandchmod +x. - Have Claude invoke
Bashwith this script as the command, with a
timeout long enough that the tool transitions output to a
background task (this seems to be the trigger that exposes the
process to the harness's task-tracking).
- Within seconds, system process count balloons; zsh in another
terminal reports "cannot fork"; the Claude main process begins
leaking ~10 MB/s.
Long-running-test workflow reproducer (the original repro path)
A full-stack repro that doesn't depend on a custom script. This is
how we originally encountered the bug — running an internal Docker-
based e2e test suite that spawns 15–25 containers per run.
- Spawn a detached background process from Claude via
Bash(e.g.
nohup … & disown) — typically a long-running e2e test runner.
- Have Claude watch a log file with
Monitorrunning `tail -f $LOG
| grep --line-buffered "<pattern>"`.
- When
Monitornotifies on the matching event, have Claude call
Bash to read the structured result from a known location, then
TaskStop to kill the monitor.
- Repeat 1–3 a few times in the same session.
After ~3 iterations, the harness OOMs.
Empty-turn trigger (separate but compounding)
Even without the recursive-script issue, we observed the OOM after
Claude returned an "empty turn" — a response with no user-facing text
and no meaningful tool work, e.g. literal text "No response — following a
requested."Bash run_in_background task launch.
The crash window opens after the tool calls have all returned
success. This suggests at least two distinct failure paths:
- Process accumulation from Claude-launched scripts that descend
into the harness's own process tree (the script reproducer above).
- Something in the empty-turn handling path that doesn't release
memory associated with completed background tasks.
Operator-side observations
- Memory growth is linear — ~100 MB every 10 s, ~10 MB/s
sustained. Suggests a leak per unit time rather than per tool call
or per turn.
- Claude main process climbs to 20–30 GB before macOS memory-pressure
throttling kicks in.
- System enters fork-bomb state:
zshin another terminal reports
"cannot fork". Strong circumstantial evidence the harness is
spawning subprocesses that aren't being reaped.
- Terminal hosting Claude Code becomes completely unresponsive — not
slow, fully frozen.
- Recovery: kill the specific Claude PID (not the parent shell). After
kill, terminal needs reset before it's usable.
What we'd find useful
- Confirmation of whether
TaskStopactually reaps the underlying
process tree, or only marks the task complete in the harness's
registry. We have a strong suspicion it's the latter.
- A way to dump the harness's task-registry and its tracked process
state at arbitrary moments, so we can correlate accumulation with
crashes.
- Memory profile at the moment of OOM — what's the 20–30 GB for?
String accumulation? Buffered streaming output? Leaked Node objects?
Leaked subprocess handles?
Workarounds we've applied
- A wrapper script (link or repo path on request) that detaches our
long-running test runner via nohup … & disown so its own
task-completion event doesn't OOM the harness. Helps but does not
fully resolve.
- A post-run cleanup step that quiesces the test's docker stack
before the EXIT marker is written.
- Recently swapped
Monitor(continuoustail -f | grep) for `Bash
run_in_background with an until grep -q` loop, per Monitor's own
documentation. Too soon to tell whether this resolves the bug.
- A periodic Claude-process counter (snapshot-based, no live
pgrep -P, fork-bomb-safe) that the operator can leave running in
the background to capture the leak rate per tick.
Severity
High. Each crash terminates the operator's Claude session
entirely. Recovery is manual (find the PID, kill it, reset the
terminal, start a new Claude session, lose conversation context).
Costs roughly 5 minutes of operator time per crash, plus loss of
in-flight work. Across a multi-day investigation we've hit it dozens
of times.
We're happy to provide more diagnostic data if you can tell us what
to capture.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗