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

Resolved 💬 1 comment Opened May 6, 2026 by psbanka Closed Jun 4, 2026

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 -u raised 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
-P
is enough to trigger a fork-bomb when Claude invokes it via the
Bash 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:

  1. Save the script as tree.sh and chmod +x.
  2. Have Claude invoke Bash with 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).

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

  1. Spawn a detached background process from Claude via Bash (e.g.

nohup … & disown) — typically a long-running e2e test runner.

  1. Have Claude watch a log file with Monitor running `tail -f $LOG

| grep --line-buffered "<pattern>"`.

  1. When Monitor notifies on the matching event, have Claude call

Bash to read the structured result from a known location, then
TaskStop to kill the monitor.

  1. 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
requested."
— following a 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:

  1. Process accumulation from Claude-launched scripts that descend

into the harness's own process tree (the script reproducer above).

  1. 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: zsh in 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 TaskStop actually 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 (continuous tail -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.

View original on GitHub ↗

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