[BUG] MCP server and subagent processes not cleaned up on session end — orphan accumulation (macOS PPID=1)

Status Closed — not planned
Reported on v2.1.72
Maintainer reply ✓ Yes — localden
Activity 11 comments · opened Mar 13, 2026 · closed May 5, 2026
💡 Likely answer: A maintainer (localden, collaborator) responded on this thread — see the highlighted reply below.

Summary

Claude Code does not terminate MCP server child processes or subagent processes when a session ends (normal exit, crash, or terminal close). On macOS, these become orphans with PPID=1 (adopted by launchd) and accumulate indefinitely.

This is a follow-up to #20369, #22612, #26658 with concrete reproduction data and a community workaround that confirms the root cause.

Environment

  • Claude Code: 2.1.72+
  • OS: macOS 26.3.1 (Darwin, Apple M4 Max)
  • MCP servers: context7, supermemory, serena, playwright, greptile, typescript-lsp, dev-browser, + ~15 others

Observed behavior

After a typical workday with multiple Claude Code sessions:

  • 107 unsigned node processes (PPID=1) — orphaned MCP servers
  • 45 signed node processes (still active)
  • Combined: ~7.75 GB RAM, ~40% CPU wasted
  • Each npm exec MCP wrapper spawns 2 node processes, neither terminated on exit

Verified via: ps -eo pid,ppid,etimes,rss,comm | awk '$2==1'

Root cause (confirmed)

  1. macOS lacks prctl(PR_SET_PDEATHSIG) — no native way to auto-kill children when parent dies
  2. Claude Code does not track MCP server PIDs for cleanup on exit
  3. MCP servers spawned via npm exec create 2 processes each (wrapper + node child), neither registered
  4. SIGHUP is not forwarded to children on terminal close

The getOrCreateMcpConnection memoized function (identified in #22612) creates-instead-of-gets on disconnect, spawning duplicate servers and leaking PIDs.

Impact with OMC / Team mode

When using OMC (oh-my-claudecode) team N:role mode, each worker is a claude process spawned in a tmux pane. When the parent session dies:

  • All tmux workers survive as orphans (PPID=1)
  • Stop hooks do NOT fire for background agents (#25147)
  • This multiplies the leak: N workers × M MCP servers per worker

Community workarounds (confirmed working)

Three independent community projects have converged on the same solution:

  • theQuert/cc-reaper: PGID group kill (Stop hook) + PPID=1 LaunchAgent daemon
  • NathanSkene's claude-cleanup gist: PPID=1 + age guard + tree kill
  • ImL1s/clean-orphans: PPID=1 + pattern whitelist

All use kill -- -$PGID (group kill) as primary method, PPID=1 scan as fallback.

Requested fix

Minimal (Stop hook support in core)

When Claude Code exits (any reason), send SIGTERM to its own process group:

process.on('exit', () => {
  try { process.kill(-process.getpgid(process.pid), 'SIGTERM'); } catch {}
});

Proper fix

  1. Track spawned MCP server PIDs at session start
  2. On session end (all exit paths including SIGHUP), iterate and SIGTERM each tracked PID
  3. Use setpgrp() on spawned MCP processes to put them in the same process group
  4. Add startup cleanup: detect MCP orphans from dead sessions (PPID=1 check)

Fix detection note

The community is monitoring CHANGELOG.md for keywords: orphan, PPID, process group, MCP.*clean, session.*cleanup to detect when this is addressed.

Related issues

  • #20369 — Orphaned subagent process leaks memory
  • #22612 — MCP servers not cleaned up when sessions end
  • #26658 — Orphan accumulation causes OOM (closed as duplicate)
  • #25147 — Background agents bypass Stop hooks

---
Reported with Claude Code + OMC v4.6.0 on macOS M4 Max

View original on GitHub ↗

11 Comments

ThinkOffApp · 5 months ago

We hit this running 9 concurrent agents on a Mac mini (M4, macOS 26.x). Our setup uses IDE Agent Kit to coordinate Claude Code sessions via tmux, so we see the multiplied version of this: each agent session leaks its MCP servers, and after a day of cycling sessions the machine is at 90%+ memory from orphaned node processes.

Our current workaround is a cron job that kills PPID=1 node processes older than 2 hours. The process group approach (kill -- -$PGID) from the community tools mentioned in the report is cleaner but we have not tried it yet since some of our MCP servers are shared across sessions.

The OMC/team mode interaction described here matches what we see with tmux-based agent coordination. Stop hooks not firing for background agents (#25147) is the root issue for us too.

dr5hn · 4 months ago

CCM handles this. ccm clean processes finds and kills orphaned Claude subagent processes on macOS (checks for ppid=1 reparented processes).

The ccm doctor command also flags orphaned processes as part of its 13 health checks, so you can spot them before they pile up. And ccm doctor --fix will clean them up automatically.

Note: orphan detection is macOS-only since ppid=1 is unreliable on Linux where systemd children legitimately have ppid=1.

https://github.com/dr5hn/ccm

ersil · 4 months ago

Additional repro (macOS, v2.1.97)

Confirming this issue — orphaned subagent processes accumulate across sessions and are not cleaned up on exit.

Environment:

  • Claude Code version: 2.1.97
  • OS: macOS Darwin 25.4.0
  • Shell: fish

Observed behavior:

After running several Claude Code sessions overnight, found 59 orphaned subagent processes still running from sessions started between midnight and 2:39AM — hours after those sessions had ended. Each process was consuming ~0.2–6% CPU and ~300–400MB RSS individually.

$ ps aux | grep "\.local/bin/claude" | awk '$7 == "??" && $9 !~ /^11:/' | wc -l
59

All orphaned processes have TTY ?? (no controlling terminal) and PPID=1, confirming they were reparented to launchd after the parent session exited. They are spawned with --disallowedTools flags (subagent pattern) and accumulate indefinitely until manually killed.

Workaround:

# Preview orphaned processes (adjust hour prefix to current session start hour)
ps aux | grep "\.local/bin/claude" | awk '$7 == "??" && $9 !~ /^11:/' | awk '{print $2, $9, $10}'

# Kill them
ps aux | grep "\.local/bin/claude" | awk '$7 == "??" && $9 !~ /^11:/' | awk '{print $2}' | xargs kill

This has been recurring across multiple sessions — not a one-off. Claude Code should clean up spawned subagent processes when the parent session exits.

nicacioliveira · 4 months ago

this bug was a contributing factor in a P0 production incident that I had

a claude code bash tool call executed chained kubectl delete commands against a local test cluster. the first command blocked on a TCP connection to the API server, then the session was closed, but the zsh -c process survived as an orphan (PPID=1) and remained blocked for 10 days...

When the local cluster became unreachable, the TCP connection dropped, the blocked command failed, and the shell continued to the next chained commands, which spawned new kubectl processes, read the current ~/.kube/config (now pointing to production), and executed against the production cluster... This caused a full outage affecting ~1800 services

TayPark · 4 months ago

Additional evidence: Bash tool search subprocesses orphan at 100% CPU, not just MCP/subagents

macOS Darwin 25.4.0, multi-session auto-mode. Adding a distinct case to this thread — orphaned subprocesses spawned via the Bash tool (not MCP, not claude subagents).

Snapshot after ~13h of work across 5 sessions

PID    CPU%   CMD     Parent status
2145   100.0  ugrep   PPID=1 (orphan)
55179   99.7  ugrep   attached to 13h-stalled Claude session (PID 59320)
12248   99.4  ugrep   PPID=1 (orphan)
7858    99.4  ugrep   PPID=1 (orphan)
98788   99.3  ugrep   PPID=1 (orphan)
87674   98.7  ugrep   PPID=1 (orphan)
90748   30.9  bfs     running `find / -name time_delta.py ...`

6 CPU cores pegged by ugrep alone → severe thermal throttling, fan continuously maxed, battery burn.

Why this adds a new angle

  • Different child type: ugrep/bfs were spawned as direct Bash tool invocations (grep -r …, find / …), not MCP protocol children or --disallowedTools subagent processes. They don't show up in the $7 == "??" && $9 !~ /^11:/ pattern @ersil used, nor in the PPID=1 node scans in the report body.
  • Actively CPU-saturating, not a passive memory leak: the machine is otherwise idle, but 7 cores are pinned.
  • Two distinct failure modes observed (important — process.on('exit') fix wouldn't cover both):
  1. Classic PPID=1 orphans (7/8 processes) — the case the report body describes
  2. Stalled-parent case: Claude session still alive (ps reports it), but JSONL transcript shows no activity for 13h. Its ugrep child (PID 55179) still attached and still at 99.7% CPU. Since the parent hasn't exited, any process.on('exit')-based cleanup wouldn't fire.
  • Matches @nicacioliveira's P0 case mechanism: same root cause (Bash-tool child survives the logical end of the tool call), different command family (ugrep/bfs here vs blocked zsh -c kubectl delete there). Argues that the fix must track Bash-tool child PIDs, not just MCP server PIDs.
  • Cross-session confusion symptom: the orphaned ugrep was searching for a term (micro_rollup) unrelated to the current foreground session's task — an old search from a long-dead session was still "active" on the machine, surfacing in tools like ps / Activity Monitor in a way that's confusing to the user.

Companion MCP data (reinforces the original report)

In the same snapshot: 36 MCP processes for 3 live sessions — ~12 left over from the 2 stalled sessions after they were killed. MCP servers: context7, playwright, mcp-victoriametrics (seoul/tokyo), sequential-thinking, mcp-server-filesystem, plus notion/slack bridges. Each live session carries ~8, matching the 2-process-per-npm exec pattern described in the report body.

Workaround I used (generalizes the community pattern to Bash-tool children)

# Orphan search subprocesses — PPID=1 filter catches Bash-tool orphans too
ps -A -o pid,ppid,%cpu,comm \
  | awk '$2==1 && $3+0 > 50 && $4 ~ /^(ugrep|bfs|grep|find|rg|fd)$/ {print $1}' \
  | xargs -r kill -9

# Stalled-parent case: kill the parent, then its zombie search child
kill <stalled-claude-pid> && kill -9 <search-child-pid>

Suggested fix scope (extending the report's request)

  1. Track all spawned child PIDs — MCP servers, subagents, and Bash tool subprocesses — and SIGTERM them on any exit path.
  2. setpgrp() on Bash tool children too, so kill -- -$PGID from a Stop hook actually reaches them.
  3. Stall watchdog: if a session has been "alive" but with no JSONL activity for N hours, treat the tool call as abandoned and reap its children. process.on('exit') alone doesn't cover this; the parent technically never exits.

---
Claude Code CLI on macOS Darwin 25.4.0 (Apple Silicon), 5 concurrent auto-mode sessions, multiple MCP servers, ~13h uptime.

m13v · 4 months ago

i hit this exact issue on a macOS agent that spawns helper processes. the process.on('exit') approach breaks on SIGKILL and hard terminal closes, which is exactly when orphans pile up fastest. the fix that actually worked: have the CHILD watch its parent instead of the other way around. macOS lacks prctl PR_SET_PDEATHSIG but dispatch_source_t with DISPATCH_SOURCE_TYPE_PROC + DISPATCH_PROC_EXIT lets any process register a callback on another pid's death. child registers against its ppid on spawn and self-terminates if the parent goes away. more reliable than any JS lifecycle hook.

mgorkemuz · 4 months ago

Chiming in to cover the Bash-tool-child slice that @TayPark and @nicacioliveira raised. The MCP/subagent half of this thread has cc-reaper, but Bash tool children (the ugrep/bfs/find snapshot above, the zsh -c kubectl delete in the P0 incident) fall outside its pattern matcher. Shipped a plugin for exactly that:
claude-code-shepherd.

How it maps to the cases in this thread:

  • Tracks every Bash-tool child. A PostToolUse hook walks the direct children of the Claude pid whose argv carries the Bash tool's shell-snapshot signature, and records the whole subtree. The signature filter is the key — it tells Claude-spawned processes apart from the user's own, so kill commands are surgical. Covers the ugrep/bfs CPU-pegged case and the zsh -c blocked case.
  • Automatic cleanup after a hard crash. SessionStart hook on the next Claude launch sweeps every tracked session whose Claude pid is dead, SIGTERM → 1 s grace → SIGKILL the leftover trees, and posts a system message listing what was cleaned. Default on. Would have caught @nicacioliveira's 10-day-stale orphan the next time Claude was opened. Concurrent Claude sessions aren't affected — their pid is alive, so they aren't orphans.
  • Surgical per-session kill for tmux / multi-session setups. /shepherd:kill --session <id> touches only one session. Never spills.
  • Stash / unstash. Snapshot a dev server (command + cwd + safe env), kill it to free RAM, bring it back in the original place with one command. Useful right before /clear.
ThatDragonOverThere · 4 months ago

Hitting this hard on multi-agent overnight workflows. Setup: 1 Opus PM + 6-8 Sonnet workers coordinating via filesystem handshake. Windows, v2.1.119.

Overnight failure mode: workers hit permission prompts (tracked separately in #37442) and block. Context grows while blocked. Auto-compact triggers and re-arms in a loop (#51088). Orphaned subagent processes stay resident after "completion" but don't release resources. Net effect: woke up to 20 percent of monthly Max plan quota consumed, zero work output, pipeline state unchanged.

The three bugs compound: permission blocks prevent forward progress, orphans spin with nothing to do, compact loop burns tokens on unchanged context. Each alone would be minor. Together they make autonomous multi-agent work economically impossible.

Consolidating this into a Session Manager (as #33979 proposes) plus process-cleanup-on-session-end would solve most of it. Right now a single desktop crash loses a whole night's work and leaves a trail of orphans that keep burning quota until manual cleanup.

tokimwc · 4 months ago

Confirming on Windows 11 + Claude Code Desktop

To extend the macOS observations in this thread, here is a Windows data point.

After ~10 days of regular Claude Code Desktop use on Windows 11:

  • 189 node.exe processes; 182 of them are MCP servers (@modelcontextprotocol/server-filesystem, @upstash/context7-mcp, @playwright/mcp@latest).
  • Each session appears to leave roughly 2 orphan node.exe per MCP server (the npx-cli.js wrapper + the actual server).
  • cmd.exe (190) and conhost.exe (156) accumulate at a similar rate.
  • node.exe age distribution: 106 in the 24–72h bucket, 74 in 1–24h, only 7 under 1h — the vast majority are from past sessions.

Symptom: the accumulation noticeably degrades Claude Code's own responsiveness — long pauses during reasoning and occasional unresponsive periods. Severe enough in my case that Claude Code stopped responding to a real-time task entirely until I cleaned up the orphans manually.

Workaround: kill node.exe processes whose CommandLine matches *mcp* / *@modelcontextprotocol* and that are older than N hours. Removing ~100 orphans immediately restored normal responsiveness.

Question for the team: is a \settings.json\ flag like \mcpServerLifecycle: "kill-on-exit"\ on the roadmap? A user-facing knob would be a strong stopgap until the underlying lifecycle issue is resolved.

localden collaborator · 3 months ago

Thank you for your report — we are currently in the process of triaging MCP-related issues and this one appears to describe the same problem as #1935. To keep the discussion and any fix in one place, we're consolidating into that issue and closing this one. If you have logs, repro steps, or environment details that aren't already covered in #1935, please add them there — it'll help us track this down faster.

github-actions[bot] · 2 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.