claude-code - 💡(How to fix) Fix [BUG] tree-kill dependency causes pgrep storm / 100% CPU when reaping Bash subprocess trees on macOS [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#52253Fetched 2026-04-24 06:12:05
View on GitHub
Comments
0
Participants
1
Timeline
6
Reactions
0
Author
Participants
Timeline (top)
labeled ×5subscribed ×1

Error Message

Error Messages/Logs

Root Cause

Root cause

Code Example

case "darwin":
  Bg6(H, K, O, T => F89("pgrep", ["-P", T]), () => U89(K, _, q));
  break;

function Bg6(H, _, q, K, O) {
  var T = K(H), $ = "";
  T.stdout.on("data", Y => { $ += Y.toString("ascii"); });
  T.on("close", function (z) {
    if (delete q[H], z != 0) {
      if (Object.keys(q).length == 0) O();
      return;
    }
    $.match(/\d+/g).forEach(function (Y) {
      Y = parseInt(Y, 10);
      _[H].push(Y); _[Y] = []; q[Y] = 1;
      Bg6(Y, _, q, K, O);   // recursive fan-out, one pgrep per descendant
    });
  });
}

---



---

// darwin / linux: one process listing, build the tree in memory
const out = execSync('ps -axo pid=,ppid=').toString();
const childrenByPpid = new Map();
for (const line of out.split('\n')) {
  const [pid, ppid] = line.trim().split(/\s+/).map(Number);
  if (!pid) continue;
  if (!childrenByPpid.has(ppid)) childrenByPpid.set(ppid, []);
  childrenByPpid.get(ppid).push(pid);
}
const descendants = [];
const stack = [rootPid];
while (stack.length) {
  const p = stack.pop();
  descendants.push(p);
  for (const c of childrenByPpid.get(p) || []) stack.push(c);
}
// then signal in leaf-first order
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

What's Wrong?

Environment

  • Claude Code version: 2.1.118 (also reproduced on 2.1.114, 2.1.94, and many earlier — issue persists 8+ months)
  • OS: macOS (darwin, arm64) — Darwin 25.4.0
  • Shell: zsh
  • Install method: native

Summary

Claude Code's bundled tree-kill implementation produces a fork bomb of pgrep processes whenever it tears down a Bash subprocess tree. CPU pegs at 100% and the machine becomes briefly unresponsive. This has been a recurring cause of crashes for me across many versions over the last 8+ months and has never been addressed.

Root cause

The minified Bg6 function in the bundled binary is the standard npm tree-kill recursive walker. On darwin it discovers descendants by spawning pgrep -P <pid> once per node, recursively:

case "darwin":
  Bg6(H, K, O, T => F89("pgrep", ["-P", T]), () => U89(K, _, q));
  break;

function Bg6(H, _, q, K, O) {
  var T = K(H), $ = "";
  T.stdout.on("data", Y => { $ += Y.toString("ascii"); });
  T.on("close", function (z) {
    if (delete q[H], z != 0) {
      if (Object.keys(q).length == 0) O();
      return;
    }
    $.match(/\d+/g).forEach(function (Y) {
      Y = parseInt(Y, 10);
      _[H].push(Y); _[Y] = []; q[Y] = 1;
      Bg6(Y, _, q, K, O);   // recursive fan-out, one pgrep per descendant
    });
  });
}

For every descendant in the victim's process tree, exactly one pgrep is forked. When the victim is a typical interactive Bash with a fat subtree (zsh + MCP stdio servers + caffeinate + npm exec + node + child shells), one teardown can spawn hundreds of pgreps. When several Bash tool invocations time out or are aborted concurrently — common during subagent batches or parallel tool calls — the bursts overlap and saturate the CPU. macOS pgrep is not cheap (~50–100ms cold), so the cost compounds quickly.

Secondary defect

If pgrep exits 0 with empty stdout (no children, race during teardown), $.match(/\d+/g) returns null and .forEach throws an uncaught TypeError mid-walk. This silently aborts cleanup and leaves orphan descendants behind, which then survive until the next walk and inflate the next storm.

This exhausts the user process limit, and closing the shell leaves claude code as an orphan, which I then need to force quit in activity monitor, because in this state I cannot open another shell.

What Should Happen?

Claude should be able to search for its spawned processes without pgrep cascade reaching the user process limit.

Error Messages/Logs

Steps to Reproduce

Reproduction

Easiest to provoke:

  1. Have at least one MCP stdio server configured (any combination — I have tokensave, read-website-fast, memstate).
  2. Run a Bash tool command that spawns nested processes and exceeds the timeout, e.g. bash -c 'for i in $(seq 1 20); do (sleep 60 &); done; sleep 120'.
  3. Press Esc to abort, or let it time out.
  4. Observe with sudo execsnoop | grep pgrep — you will see a burst of pgrep -P <pid> calls. Repeat a few times, especially with parallel Bash, and CPU pegs.

Claude Model

Not sure / Multiple models

Is this a regression?

No, this never worked

Last Working Version

No response

Claude Code Version

2.1.118

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

iTerm2

Additional Information

Suggested fix

Replace the recursive per-node pgrep walk on darwin with a single ps snapshot, then build the tree in JS. One fork instead of N:

// darwin / linux: one process listing, build the tree in memory
const out = execSync('ps -axo pid=,ppid=').toString();
const childrenByPpid = new Map();
for (const line of out.split('\n')) {
  const [pid, ppid] = line.trim().split(/\s+/).map(Number);
  if (!pid) continue;
  if (!childrenByPpid.has(ppid)) childrenByPpid.set(ppid, []);
  childrenByPpid.get(ppid).push(pid);
}
const descendants = [];
const stack = [rootPid];
while (stack.length) {
  const p = stack.pop();
  descendants.push(p);
  for (const c of childrenByPpid.get(p) || []) stack.push(c);
}
// then signal in leaf-first order

This eliminates the fan-out entirely, removes the recursive Node spawn overhead, and sidesteps the empty-stdout match crash. It is also strictly faster than the upstream tree-kill behavior on Linux as well, so the platform branch can collapse.

If sticking with tree-kill upstream is preferred, please at minimum:

  • Guard $.match(/\d+/g) against null.
  • Cap concurrency of in-flight pgrep invocations (a small semaphore).
  • Debounce/coalesce kills when multiple Bash tools terminate within the same tick.

This has been an intermittent but reliable cause of system-wide pauses for me across many versions. The recursive shell-spawning pattern is an obvious source of runaway behavior and would normally have been flagged in review. Asking that it finally get prioritized.

extent analysis

TL;DR

Replace the recursive pgrep walk on Darwin with a single ps snapshot and build the tree in JavaScript to eliminate the fan-out and reduce overhead.

Guidance

  • Identify the problematic Bg6 function in the bundled binary, which spawns a pgrep process for each descendant in the process tree.
  • Consider replacing the recursive pgrep walk with a single ps snapshot, as suggested in the issue, to build the tree in memory and reduce the number of forked processes.
  • If sticking with the current implementation, guard against null values in $.match(/\d+/g) and consider capping concurrency of in-flight pgrep invocations to prevent overwhelming the system.
  • Verify the fix by running the reproduction steps and monitoring the system's CPU usage and process list to ensure the fan-out is eliminated.

Example

// darwin / linux: one process listing, build the tree in memory
const out = execSync('ps -axo pid,ppid=').toString();
const childrenByPpid = new Map();
for (const line of out.split('\n')) {
  const [pid, ppid] = line.trim().split(/\s+/).map(Number);
  if (!pid) continue;
  if (!childrenByPpid.has(ppid)) childrenByPpid.set(ppid, []);
  childrenByPpid.get(ppid).push(pid);
}
const descendants = [];
const stack = [rootPid];
while (stack.length) {
  const p = stack.pop();
  descendants.push(p);
  for (const c of childrenByPpid.get(p) || []) stack.push(c);
}
// then signal in leaf-first order

Notes

The suggested fix is specific to the Darwin platform, but the issue affects multiple versions of Claude Code. The provided example code snippet is a potential replacement for the problematic Bg6 function.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING