[BUG] Claude Code spawned ~3,000 parallel bash processes in 17 seconds, causing OOM crash and hard reboot.

Status Fixed / completed
Reported on v2.1.150
Maintainer reply None cached
Activity 12 comments · opened May 25, 2026 · closed Jun 1, 2026

Preflight Checklist

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

What's Wrong?

Claude Code spawns thousands of bash subprocesses in parallel within seconds, exhausting all system memory and triggering the kernel OOM killer. This has occurred multiple times.

~3,079 bash processes were spawned between PID 21300 and PID 24379 in approximately 17 seconds (12:38:41–12:38:58 local time). All processes were owned by UID 1000, all carried oom_score_adj:100 (set by Claude Code). The code process itself reached ~1.4 TB virtual memory allocation. Usually this results in a total lock up of the host and requires a full reboot.

What Should Happen?

Bash subprocesses are spawned sequentially or with a reasonable concurrency limit that respects system memory.

Error Messages/Logs

Kernel OOM log evidence (journalctl -b -1 -k --no-pager | grep -A 50 "Out of memory"):
Mai 25 12:38:41 <hostname> kernel: Out of memory: Killed process 5301 (code) total-vm:1463373852kB, anon-rss:200640kB, file-rss:2916kB, shmem-rss:2076kB, UID:1000 pgtables:2096kB oom_score_adj:300
Mai 25 12:38:57 <hostname> kernel: [  21300]  1000 21300    61965     2732     2285      447         0    90112     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21301]  1000 21301    61313     2468     2023      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21303]  1000 21303    61967     2673     2226      447         0    90112     1888           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21304]  1000 21304    61313     2475     2030      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21305]  1000 21305    61967     2705     2258      447         0    90112     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21306]  1000 21306    61314     2444     1999      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21307]  1000 21307    61314     2477     2032      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21308]  1000 21308    62001     2646     2199      447         0    90112     1888           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21309]  1000 21309    61315     2445     2000      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21310]  1000 21310    62001     2710     2263      447         0    90112     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21311]  1000 21311    61315     2513     2036      477         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21312]  1000 21312    61316     2482     2037      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21313]  1000 21313    62002     2654     2207      447         0    90112     1888           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21314]  1000 21314    61316     2485     2040      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21315]  1000 21315    62002     2718     2271      447         0    90112     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21316]  1000 21316    61317     2454     2009      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21317]  1000 21317    61318     2490     2045      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21318]  1000 21318    62003     2687     2240      447         0    90112     1888           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21319]  1000 21319    61319     2458     2013      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21320]  1000 21320    62003     2751     2304      447         0    90112     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21321]  1000 21321    61319     2527     2082      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21322]  1000 21322    62004     2659     2212      447         0    90112     1888           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21323]  1000 21323    61320     2464     2019      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21324]  1000 21324    61320     2500     2055      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21325]  1000 21325    62004     2723     2276      447         0    90112     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21326]  1000 21326    61321     2468     2023      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21327]  1000 21327    62006     2663     2216      447         0    90112     1888           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21328]  1000 21328    61321     2536     2091      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21329]  1000 21329    62006     2727     2280      447         0    90112     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21330]  1000 21330    61322     2504     2059      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21331]  1000 21331    61322     2508     2063      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21332]  1000 21332    62007     2669     2222      447         0    90112     1888           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21333]  1000 21333    61323     2509     2032      477         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21334]  1000 21334    62007     2733     2286      447         0    90112     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21335]  1000 21335    61358     2513     2068      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21336]  1000 21336    61358     2513     2068      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21337]  1000 21337    62008     2673     2226      447         0    90112     1888           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21338]  1000 21338    61359     2521     2076      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21339]  1000 21339    62008     2737     2290      447         0    90112     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21340]  1000 21340    61359     2489     2044      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21341]  1000 21341    61360     2522     2077      445         0    77824     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21342]  1000 21342    62009     2677     2230      447         0    94208     1888           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21343]  1000 21343    61360     2491     2046      445         0    77824     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21344]  1000 21344    62009     2773     2326      447         0    94208     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21345]  1000 21345    61361     2559     2114      445         0    81920     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21346]  1000 21346    62010     2682     2235      447         0    94208     1888           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21347]  1000 21347    61361     2495     2050      445         0    81920     1408           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21348]  1000 21348    61362     2531     2086      445         0    81920     1376           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21349]  1000 21349    62010     2778     2331      447         0    94208     1824           100 bash
Mai 25 12:38:57 <hostname> kernel: [  21350]  1000 21350    61362     2500     2055      445         0    81920     1408           100 bash
--
Mai 25 12:38:58 <hostname> kernel: Out of memory: Killed process 13836 (Web Content) total-vm:2821064kB, anon-rss:7324kB, file-rss:3604kB, shmem-rss:1216kB, UID:1000 pgtables:580kB oom_score_adj:233

Steps to Reproduce

Fresh install of claude code, no changes in any settings.

This happens specifically when claude decides to read files or use the find command.
Having extracted a bash command that previously caused an issue, I can reproduce the issue by entering the prompt:

"run this bash command: find /Path/ -maxdepth 3 -type f \( -name ".tex" -name ".makefile" -o -name "Makefile" \) 2>/dev/null | head -30 && echo "---" && find /Path/-maxdepth 3 -type d | head -20)"

Where /Path/ is a working path and manual execution of the bash command works.

System info

  • OS: Fedora 42
  • Kernel: 6.19.14-101.fc42.x86_64
  • RAM: 32 GB
  • Shell: fish

Claude Model

Not sure / Multiple models

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.150 (Claude Code)

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

VS Code integrated terminal

Additional Information

_No response_

View original on GitHub ↗

11 Comments

Frad70 · 3 months ago

lol

harris1111 · 3 months ago

This is not a repo contains claude-code source code

github-actions[bot] · 3 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/50191
  2. https://github.com/anthropics/claude-code/issues/37490
  3. https://github.com/anthropics/claude-code/issues/56660

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

yurukusa · 3 months ago

The 3,000-bash-in-17-seconds pattern is the OS-level manifestation of what's been documented in the May 2026 sub-agent failure cluster as "scope expansion via uncapped parallel dispatch" — the same primitive that causes the silent-stall and dispatch-fabrication patterns also enables runaway concurrency when the model decides to fan out.
Three signals from your kernel log narrow the cause:

  1. oom_score_adj:100 set by Claude Code on every spawned bash — this is intentional, Claude Code marks its spawned subprocesses so the OOM killer picks them over the parent. But at 3,079 processes, the marker is overwhelmed; the killer hits the parent (code) first because the parent's VSZ is 1.4 TB.
  2. PIDs 21300–24379 contiguous in 17 seconds — that's ~180 spawns/second. No sleep, no wait, no xargs -P N. The parent is calling spawn/exec in a tight loop. The likely call site is the Bash tool with a single command that internally fans out (e.g., find ... -exec bash -c '...' \; or a loop the model generated without bounds).
  3. VSZ 1.4 TB on parent — most of this is unused virtual address space, not RSS. The actual physical-memory pressure comes from the bash subprocess sum (RSS ~2.5 MB × 3,079 ≈ 7.7 GB), which exceeds typical free RAM on most workstations.

Operator-side mitigations (working today):
A. Wrap bash through ulimit + cgroup. The cleanest layer to enforce concurrency is the kernel. Run Claude Code under a systemd-run scope with MemoryMax=4G and TasksMax=200:

systemd-run --user --scope -p MemoryMax=4G -p TasksMax=200 claude

When the limit is hit, the cgroup OOM killer takes only Claude Code's tree, not the entire host. Your hard reboot becomes a graceful claude exit.
B. PreToolUse hook to reject bash fan-out patterns. Add a hook that scans Bash tool calls for fan-out signatures (for , while , find.*-exec, xargs(?! -P [0-9]), parallel) and either blocks them or rewrites them to bounded forms (e.g., inject | xargs -P 8 -n 1). cc-safe-setup ships examples/bash-fanout-bounded-rewriter.sh for this — it converts unbounded loops to bounded xargs -P invocations or refuses the call.
C. PostToolUse hook to count subprocesses. After every Bash tool call, count immediate children of claude. If the count exceeds N (default 50), terminate the session before the next tool call. This is the "circuit breaker" approach — useless after the fact when the host has already rebooted, but the previous two layers should prevent reaching this state.
Why this isn't already default in Claude Code:
The Bash tool surface explicitly trusts the model's concurrency judgment, because a meaningful fraction of legitimate workloads (test parallelism, build steps, batch file processing) require it. A blanket TasksMax in Claude Code itself would break those workflows. The cgroup-based mitigation (A) is the right architectural answer because it lets each operator pick their bound at the OS level rather than the application level.
This is one of the cases I cover in the Sub-Agent Observability Handbook Chapter 5 (scope expansion sub-pattern) — your case is the most extreme production example of the pattern (3,079 spawns vs the previously documented worst case of ~150).
cc-safe-setup hook collection: https://github.com/yurukusa/cc-safe-setup

fdpr-H · 3 months ago

Thanks for the input, approach A definitely provided an effective safeguard against a total system lock up. However, it also essentially rendered the bash tool useless in these situations; this is a response after executing the aforementioned command again: "The system is under resource pressure (fork failures). Try running the command again in a moment, or check if there are too many processes running. You can try ! find ... in the prompt, or wait a bit and retry."

I was not able to locate the script you mentioned in your repo, and initial efforts of my own in using a pre tool hook to try to catch this behaviour has been unsuccessful thus far.

yurukusa · 3 months ago

@fdpr-H — thank you for testing Approach A in practice and reporting back. Your observation is exactly right, and your instinct to look for the PreToolUse hook layer is the correct architectural answer. However, I owe you a correction: when I referenced examples/bash-fanout-bounded-rewriter.sh in my earlier comment, that script did not actually exist in the repo yet. That was a claim-verify gap on my side — the exact failure shape I've spent the last month writing about. I apologize for the misdirection that made you spend time searching.
It exists now. I shipped it today in PR #440, MIT, 172 lines + 47 tests:
Install:

cd ~/.claude/hooks/  # or your hooks directory
curl -O https://raw.githubusercontent.com/yurukusa/cc-safe-setup/main/examples/bash-fanout-bounded-rewriter.sh
chmod +x bash-fanout-bounded-rewriter.sh

Wire up in ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{"type": "command", "command": "~/.claude/hooks/bash-fanout-bounded-rewriter.sh"}]
      }
    ]
  }
}

Detection signatures (7 patterns, ordered most-severe-first):

  1. find ... -exec — the worst case, shell-spawning per result
  2. for ... in $(...) / for ... in * / for ... in $VAR — dynamic-list loops
  3. while read ... / while IFS=... — unbounded per-line loops
  4. parallel without -j N — defaults to one-per-core
  5. make -j without N — defaults to infinite
  6. seq N | while/for/xargs where N > 100 — high-cardinality

xargs without -P deliberately passes through because serial xargs is safe (one process at a time). The hook only refuses when the pattern can spawn N>1 concurrent processes.
On detection: exits 2 with a stderr advisory that names the matched pattern, shows a bounded rewrite of the same command, and provides the override path. The rewrite for your kind of case (find . -exec ...) is the standard find ... -print0 | xargs -0 -P 8 -n 1 <cmd> form — same workload, capped at 8 concurrent processes, sustainable on every workstation tested.
Override knobs (in env):

  • CC_BASH_FANOUT_DISABLE=1 — turn the whole hook off (operator already aware)
  • CC_BASH_FANOUT_OVERRIDE=1 — allow one specific call (clear after the call to re-arm)
  • CC_BASH_FANOUT_PARALLEL_BOUND=N — change the suggested rewrite's N (default 8)

Why this layer instead of cgroup TasksMax alone: Approach A blocks ALL bash including light single-process calls under pressure, which is exactly what you saw. This hook intercepts only the fan-out shape, so ls, git status, simple rm, etc. keep working at full speed. Approach A remains the right backstop (the kernel-level catch-all for misses), but Approach B handles the common case without making bash useless when the OOM killer is hovering.
I've left the original find ... -exec advisory referencing this hook in PR #440's commit message, so the trace is in the record. If you find the detection list misses a pattern from your environment, file a thin issue against yurukusa/cc-safe-setup and I'll add it — the matrix is public and easy to extend.
Also: the case-study deepens the Cluster 1 (Sub-Agent Observability) chapter on scope expansion. Your 3,079-spawn event is now the canonical extreme example I cite when explaining why operator-side concurrency caps matter, alongside the 12-hour silent stall (#61405) and the 120GB recommend-as-authorization case (#61102). Thanks for documenting it so cleanly.

allenfu000 · 3 months ago

Same here on Windows (Claude Code v2.1.156, Opus 4.8): an ultracode workflow's
subagents kept spawning nested local PowerShell processes until they piled up,
maxed out RAM, and crashed my machine along with two other Claude Code windows I
had open.

yurukusa · 3 months ago

@allenfu000 — thanks for the cross-platform data point. Your Windows reproduction confirms that the failure mode is platform-independent at the root, but the trigger differs from @fdpr-H's original case in a way that matters for the mitigation surface.

Two distinct triggers, one root failure

Your reproduction and @fdpr-H's both end in unbounded subprocess spawn → OOM, but they enter the failure through different paths:

  • @fdpr-H's case (Linux, 3,079 bash in 17s) — single Bash tool call internally fanning out (one find ... -exec, one unbounded loop, or one parallel without -j). The fan-out happens inside a tool call the model issued.
  • Your case (Windows, ultracode + nested PowerShell) — an ultracode workflow's subagents spawning PowerShell processes, with the subagents themselves cascading new subagents. The fan-out happens between tool calls, at the orchestration layer, where each subagent independently issues PowerShell calls without a shared budget. (I'm using "ultracode" the way you described it — please correct me if it refers to a specific framework I'm conflating with the general subagent-orchestration pattern.)

This second pattern is the nested-spawn sub-pattern of Cluster 1 (Sub-Agent Observability) — uncapped fan-out at the orchestration layer rather than inside a single tool call — and the operator-side hooks for it are structurally different from the bash-fan-out hook. Three implications:

  1. bash-fanout-bounded-rewriter.sh from PR #440 will not catch your case. It hooks PreToolUse on Bash, scans the command string for fan-out signatures (find -exec, for/while, parallel, xargs without -P). PowerShell tool calls and Task/SendMessage subagent dispatches don't match its scope by design.
  2. The architectural surface for your case is PreToolUse on Task/SendMessage — the dispatcher itself. A hook there can count active subagents per session and refuse to dispatch a new one when the count exceeds N, regardless of whether the subagents go on to spawn PowerShell, bash, or nothing at all. That's the right layer because it gates the fan-out before any subprocess work begins.
  3. The kernel-level backstop (@fdpr-H's Approach A, systemd-run with TasksMax) has no clean Windows analog. Windows' equivalent is Job Objects with JOB_OBJECT_LIMIT_ACTIVE_PROCESS. The PowerShell Start-Job/Start-Process path doesn't compose with Job Objects as cleanly as systemd-run composes with bash, but the Windows Sandbox and WSL2's cgroup v2 both give you the same kind of kernel-enforced ceiling if you're willing to run Claude Code inside one of them.

Operator-side mitigations that work today on Windows

A. Run Claude Code in WSL2 instead of native Windows. Then @fdpr-H's systemd-run --user --scope -p MemoryMax=4G -p TasksMax=200 claude works unchanged. This is the lowest-effort fix if your tooling allows it. WSL2 was the platform where the original bash-fanout-bounded-rewriter.sh was tested, and the hook itself is shell-agnostic at the command-string level (PowerShell tool calls don't trigger it, but bash calls inside WSL still do).

B. Wrap Claude Code in a Windows Job Object on native Windows. Job Objects are the kernel-supported way to set a hard ceiling on a process tree's RAM and active-process count; when a limit is hit, the Job terminates the tree rather than damaging the host. The practical entry points are procgov (open-source, MIT) or the Job Object Win32 API wrapped in PowerShell. I'd direct you to the procgov README for exact flag syntax rather than cite specifics from memory — the tool's documentation is the authoritative source on the supported limit categories (memory, process count, CPU rate). The architectural point is that if you wrap Claude Code in a Job Object with a process-count ceiling, the failing ultracode tree can't reach beyond its own job to take down your other Claude Code windows.

C. Subagent-count PreToolUse hook (not yet shipped, but the right architectural fix). A PowerShell-compatible PreToolUse hook that:

  • Reads the session's transcript on each Task/SendMessage dispatch.
  • Counts the number of subagents currently in-flight (issued tool calls but no tool_result yet).
  • Exits 2 with a stderr advisory if the count exceeds N (default 5).
  • Allows override via CC_NESTED_SPAWN_BUDGET=N env var for legitimate parallel workloads.

I've been treating this as a "design-stage" hook in the Cluster 4 work — your reproduction is the strongest case I've seen for shipping it sooner. Tracking it as PR target; if I can land it in the next 48h, I'll comment back here with the install path. I'm being deliberate about not promising the ship date — the architecture needs verification that the transcript-scan can keep up with high-frequency dispatches without becoming a bottleneck itself. (This is the same self-correction I owe @fdpr-H for the earlier bash-fanout-bounded-rewriter.sh claim that didn't exist yet at the time I referenced it.)

Why this is escalating in the May 2026 surge

Your "two other Claude Code windows" detail is the clue I want to flag for triage. In the original Linux case, the OOM killer's target heuristic (oom_score_adj on bash, parent's massive VSZ) limited collateral damage to the host's other processes. On Windows, the OOM behavior differs: Windows' memory manager will swap, then page-fault, then crash entire processes before reaching a stable state, and the cross-window damage you saw is consistent with virtual memory exhaustion affecting the whole user session before the trigger process itself dies.

This means the Windows failure is potentially worse than the Linux failure for users running multiple Claude Code sessions — a single rogue ultracode workflow can take down concurrent productive sessions. The Cluster 4 surface deserves its own mitigation layer rather than relying on platform-level catches.

I'll add your case to the cluster-tracker as the canonical Windows + ultracode example, alongside @fdpr-H's case as the canonical Linux + single-bash-fanout example, in the next docs commit. The tracker is at yurukusa.github.io/cc-safe-setup/cluster-tracker.html — the nested-spawn pattern lives under Cluster 1 (Sub-Agent Observability) rather than having its own cluster slot. I'll link the docs PR in a follow-up once it lands.

Thanks again for the precision in the report — "two other Claude Code windows" is exactly the kind of detail that makes the cross-platform severity legible.

yurukusa · 3 months ago

@allenfu000 — follow-up on the subagent-count PreToolUse hook commitment from my earlier comment. It shipped today (PR #474, MIT, 158 lines + 26 tests passing).

Install

mkdir -p ~/.claude/hooks
curl -o ~/.claude/hooks/nested-spawn-inflight-guard.sh \
  https://raw.githubusercontent.com/yurukusa/cc-safe-setup/main/examples/nested-spawn-inflight-guard.sh
chmod +x ~/.claude/hooks/nested-spawn-inflight-guard.sh

Wire into ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Task|Agent|SendMessage",
        "hooks": [
          { "type": "command",
            "command": "$HOME/.claude/hooks/nested-spawn-inflight-guard.sh" }
        ]
      }
    ]
  }
}

What it does

  • Hooks PreToolUse on the three dispatcher tool names (Task, Agent, SendMessage).
  • Reads the actual transcript (transcript_path from the hook payload), not a counter file. Scans the trailing 2000 lines (configurable via CC_NESTED_SPAWN_LOOKBACK).
  • Counts tool_use entries whose name is in the dispatcher family. Subtracts any with a matching tool_result. The remaining set is the in-flight ids.
  • If the in-flight count meets or exceeds CC_NESTED_SPAWN_BUDGET (default 5), exits 2 with a stderr advisory naming the count, the budget, the in-flight ids, and the override path. Otherwise exits 0.

Why this design instead of a counter

The cc-safe-setup catalog already had max-concurrent-agents.sh and subagent-budget-guard.sh — both increment a counter on dispatch. The dual error mode (over-counts on rebound, under-counts on burst recovery) is structural to the approach, because there's no clean decrement signal. The transcript-based variant doesn't have that problem because the actual in-flight state is reconstructed from the source of truth (the transcript) on every dispatch. As a bonus, it matches both the modern (Task / SendMessage) and legacy (Agent) tool names; the counter-file variants only match Agent.

Override knobs

For your specific ultracode case (where the workflow legitimately needs more than 5 concurrent subagents):

  • CC_NESTED_SPAWN_BUDGET=N — raise the ceiling globally. Recommended: tune based on your actual observed concurrency cap (start at 5, raise if the hook fires during workloads you trust).
  • CC_NESTED_SPAWN_OVERRIDE=1 — one-shot bypass for a specific dispatch. Clears after the next call.
  • CC_NESTED_SPAWN_DISABLE=1 — kill switch. Set when you're debugging the hook itself.

What it does NOT solve

  • The bash-fanout-bounded-rewriter (PR #440) catches fan-out inside a single Bash tool call. That's @fdpr-H's original 3,079-bash trigger.
  • This hook catches fan-out between tool calls at the orchestration layer. That's your ultracode trigger.
  • A PowerShell-equivalent of bash-fanout-bounded-rewriter to catch fan-out inside a single PowerShell tool call is still missing. It's the natural next ship; the matrix structure is the same, the detection signatures need to be translated to PowerShell idiom (ForEach-Object -Parallel, Start-Job, Invoke-Command against multiple computers, etc.). I'm flagging it as a tracked gap; if you want to file a thin issue against yurukusa/cc-safe-setup describing the PowerShell patterns you observed, that would prioritize the matrix work directly.

The kernel-level backstop still matters

The hook is the application-level catch. The architectural point from my earlier comment stands: wrap Claude Code in a Windows Job Object (via procgov or the Win32 API) with a process-count ceiling, so even if the hook misses or a future bug bypasses it, the failing tree can't reach beyond its own job to take down other Claude Code windows. The hook prevents the common case efficiently without making bash useless under pressure; the Job Object catches the misses.

Thanks again for the precision in the report — the "two other Claude Code windows" detail was what made the cross-platform severity legible, and it's what motivated shipping this hook on the same day rather than rolling it into the next sprint.

yurukusa · 3 months ago

@allenfu000 — short follow-up to round out the Windows-specific surface I owed you context on. Published today (MIT, ~1,735 words):

Claude Code on Windows — Safety Guide for the Issues That Don't Exist on macOS/Linux

It articulates five Windows-specific failure modes from the May 2026 tracker (BSOD with HVCI, the nested-PowerShell cascade case you reported, empty Bash output on Win11, PowerShell unavailable under MINGW64, OAuth paste freeze) and a "WSL2 vs native Windows" decision tree. Section 2 cites your reproduction as the canonical Windows case for the cross-window crash failure mode, alongside @fdpr-H's Linux case.

The cc-safe-setup README now links to the guide from its existing Windows Support section (PR #475, merged today), so other Windows operators landing on the repo can find the guidance without having to dig through issue threads.

I deliberately listed the cc-safe-setup Windows coverage gaps explicitly — PowerShell-specific fan-out detection (the PowerShell equivalent of bash-fanout-bounded-rewriter.sh), Windows Job Object integration to automate the procgov wrap, HVCI compatibility detection at SessionStart. These are tracked as future work; if you hit a PowerShell fan-out pattern that the guide should name, a thin issue against yurukusa/cc-safe-setup would help prioritize.

Closes the loop on the commitment from my earlier comment in this thread. Thanks again for the precision in your original report — the "two other Claude Code windows" detail was the operational signal that made the cross-platform severity worth writing up.

fdpr-H · 3 months ago

I solved it, and must admit, this one is on me.

First step was to collect some more detailed data, I used a simple bash script to save the output of ps into a file.

#!/bin/bash
LOG=/some/path/tmp/claude_procs.log
echo "Monitoring started. Writing to $LOG"
echo "Press Ctrl+C to stop."

while true; do
  echo "=== $(date) ===" >> "$LOG"
  ps auxf >> "$LOG"
  sleep 30
done

I noticed that the processes were not individually spawn by claude but rather all seemed to be child processes of the first one.

Ultimately the root cause was a malformed line in my .bashrc which caused PATH to get overwritten with a useless literal string, so the shell could no longer find any command.

With PATH broken, the Fedora command_not_found_handle function, invoked whenever a command isn't found, runs $(gettext …) in its non-interactive branch. But gettext itself is now unfindable, so the handler recursively triggers itself, and because each call forks a command-substitution subshell, it becomes a fork bomb. (The recursing subshells inherit the original argv, which is why ps shows an infinite chain of identical bash -c -l … processes.)

Showing cached comments. Read the full discussion on GitHub ↗