[BUG] MCP servers not properly terminated when Claude Code exits, causing orphaned processes

Open 💬 39 comments Opened Jun 11, 2025 by noomz
💡 Likely answer: A maintainer (ashwin-ant, collaborator) responded on this thread — see the highlighted reply below.

Environment

  • Platform (select one):
  • [x] Anthropic API
  • [ ] AWS Bedrock
  • [ ] Google Vertex AI
  • [ ] Other: <!-- specify -->
  • Claude CLI version: 1.0.18 (Claude Code)
  • Operating System: macOS Darwin 24.5.0
  • Terminal: Terminal/zsh

## Bug Description
Claude Code does not properly clean up MCP server processes when exiting, leading to orphaned processes that continue consuming system resources indefinitely. These
processes accumulate over multiple sessions and are not cleaned up even when MCP servers are removed from configuration.

## Steps to Reproduce

  1. Configure one or more MCP servers using claude mcp add <name> <command>
  2. Start Claude Code and use it normally (any session that might initialize MCP servers)
  3. Exit Claude Code normally
  4. Check running processes with ps aux | grep mcp
  5. Observe orphaned MCP server processes still running
  6. Optional: Try claude mcp remove <name> - processes still persist

## Expected Behavior
MCP server processes should be gracefully terminated when Claude Code exits. All spawned MCP server processes should be cleaned up, leaving no orphaned processes consuming
system resources.

## Actual Behavior
MCP server processes remain running as orphaned processes after Claude Code exits. These processes:

  • Continue consuming system resources (CPU, memory)
  • Accumulate over time with each Claude Code session
  • Are not cleaned up even when running claude mcp remove <server-name>
  • Persist across system restarts in some cases

## Additional Context

Evidence from testing:

  • Found 40+ orphaned MCP server processes from previous Claude Code sessions
  • Processes dating back several days (some running since Monday/Tuesday)
  • Process types include:
  • Docker containers: docker run -i --rm mcp/time, mcp/github, mcp/memory, etc.
  • Python processes: chroma-mcp, cli-mcp-server
  • Node.js processes: context7-mcp

Example orphaned processes (sanitized):
user 90260 0.0 0.0 411858096 12272 s001 S+ 11:19AM 0:00.56 docker run -i --rm mcp/time
user 88027 0.0 0.0 411869888 12224 ?? S 11:18AM 0:00.53 docker run -i --rm mcp/time
user 54415 0.0 0.0 411896512 4576 ?? S Tue12PM 0:02.82 docker run -i --rm mcp/time
user 90680 0.0 0.0 411871728 10912 ?? S Mon11AM 0:04.81 docker run -i --rm mcp/time
user 54413 0.0 0.0 411889136 4080 ?? S Tue12PM 0:02.74 docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN=*** mcp/github
user 54418 0.0 0.0 411896816 4128 ?? S Tue12PM 0:02.85 docker run --rm -i --mount type=bind,src=/Users/user/Projects/workspace,dst=/projects/workspace mcp/git

Impact:

  • Resource leak with long-running orphaned processes
  • Potential system performance degradation over time
  • User confusion when debugging MCP server issues
  • Accumulating Docker containers consuming disk space

Potential root cause:
The MCP protocol specification includes a graceful shutdown phase, but Claude Code appears to not properly invoke cleanup handlers during application exit to terminate
spawned MCP server child processes.

View original on GitHub ↗

39 Comments

ashwin-ant collaborator · 1 year ago

We have a fix on the way!

ashwin-ant collaborator · 1 year ago

This should be fixed in 1.0.21. Thanks for reporting @noomz!

cbusillo · 1 year ago

I am on version 1.0.27 and seem to have this same issue. I have dozens of node processes. I am on the developer beta of macOS, is that an issue?

CoreyWinkelmannPP · 1 year ago

I can confirm this issue still exists on Claude Code v1.0.31 with a custom MCP server.
Additional context: The same custom MCP server is properly cleaned up when used with GitHub Copilot (process terminates correctly when stopping the server), which indicates that:

My MCP server correctly handles SIGINT termination signals
The issue is specific to Claude Code's MCP process management, not a general MCP server problem

This suggests Claude Code is not sending the proper termination signals to MCP server child processes on exit, while other MCP clients (like GitHub Copilot) handle this correctly.
Environment:

Claude Code version: v1.0.31
MCP Server: Custom server (properly handles SIGINT as verified with GitHub Copilot)
Platform: macOS 15.5

The bug persists across multiple versions (original report was v1.0.18), indicating this hasn't been resolved yet.

mstllc · 1 year ago

Same with v1.0.35. MCP server is a docker container that is persisted until I shut it down and delete it. A new container is instantiated with each call to claude command.

ashwin-ant collaborator · 1 year ago

Thanks, we'll look into this again.

ashwin-ant collaborator · 1 year ago

Hi all, please update to 1.0.38 and let me know if the issue persists. This should be much more robust now.

ashwin-ant collaborator · 1 year ago

Checking in here. Is this working for everyone? @CoreyWinkelmannPP @mstllc @noomz

cbusillo · 1 year ago

@ashwin-ant working for me on macOS 26 and Claude Code 1.0.43. Love CC, keep up the great work!

sbking · 1 year ago

@ashwin-ant This is happening for me on 1.0.51. My MCP server handles SIGINT fine if I launch it directly with npx, but not when I use the same npx command in .mcp.json with claude code. It does work if I configure the mcp server to launch directly from node_modules/.bin/the-bin instead of via npx / pnpm dlx.

As far as I can tell, claude is sending two back-to-back SIGINT signals, then waits 90ms, and then sends two back-to-back SIGTERM signals. For some reason when running through a wrapper like npx it seems like this isn't enough time and my actual MCP server doesn't get the SIGINT signal.

sbking · 1 year ago

Actually, even running the binary directly via the binary in node_modules doesn't work when claude is run in --print mode. I can't get claude to kill my MCP server process at all when run in --print mode.

sbking · 1 year ago

@ashwin-ant I did some digging, it looks like when run in --print mode, claude code isn't sending SIGINT or SIGTERM to my MCP server, it's just closing the stdin stream which leaves the MCP server running if there were any other active handlers

ashwin-ant collaborator · 1 year ago

@sbking nice catch on --print mode! We have a fix on the way. Are you seeing this in interactive mode too still?

Pimzino · 11 months ago

Still seeing this in interactive mode on latest version 1.0.73 (Windows)

jgrichardson · 11 months ago

Still seeing this happening in 1.0.84

neogenz · 11 months ago

Still seeing this happening in 1.0.85

khandy-lively · 8 months ago

Still seeing this on v2.0.29

MarkDeVerno · 7 months ago

We’ve reproduced hangs with claude-agent-sdk-python v0.1.10 when MCP servers are active and when breaking early from the query stream.

Root cause analysis:

  1. Claude CLI spawns MCP server child processes (@azure-devops/mcp, exa-mcp-server, etc.).
  2. Our plugin enabled setting_sources=["user","project","local"], which caused MCP discovery even though servers were “disabled.”

→ Removing setting_sources or passing mcp_servers={} prevents this.

  1. When tasks complete, MCP servers don’t terminate → parent process never exits → stdout never closes → SDK’s query() blocks forever.
  2. If we break early on a ResultMessage, the SDK’s internal cancel scope is exited in the wrong task context → RuntimeError: Attempted to exit cancel scope in a different task.

Evidence:

  • Parent stuck in do_epoll_wait.
  • MCP child processes alive long after task completion.
  • Cancel scope error leaking into unrelated asyncio code.

Workarounds tested:

  • Hard MAX_EXECUTION_TIME watchdog with process‑tree cleanup.
  • Removing setting_sources or overriding with mcp_servers={}.
  • Queue‑based reader that drains the generator to exhaustion while signaling logical completion upstream.

Recommendations:

  • SDK should provide a supported “early close” method that safely terminates the generator and cancel scopes.
  • CLI should not spawn MCP servers unless explicitly configured.
  • CLI should terminate MCP servers on task completion or close stdout once the parent task is done.
github-actions[bot] · 6 months ago

This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

co50 · 6 months ago

Still experiencing this issue on 2.1.9. Many many node.js processes left over that never get term'd.

ericrshields · 4 months ago

Adding another data point: on an EC2 instance running Claude Code over SSH+tmux, I found 14 orphaned mcp-todoist processes accumulated over ~10 days. Each session spawned a new MCP server process (~20MB each), but exiting Claude Code did not terminate them. Only the 2 processes belonging to the currently active session were still attached.

Environment: Ubuntu on EC2, Node v24.11.1, Claude Code accessed via SSH + tmux, MCP server configured via settings.local.json.

The orphans were trivial to clean up manually (pkill), but on a long-running server this accumulation is unbounded and silent.

ChinmayLonkar · 4 months ago

Hello,
The problem persists with Claude Code v2.1.41.
I'm developing MCP tools for embedded firmware development based on ESP-IDF. When the MCP servers spawn a device monitoring process, exit handlers in the server (specifically registered on SIGTERM and SIGINT) terminate the monitoring process, but I noticed that the handlers never get triggered when I exit from Claude code, the process keeps running.

theQuert · 4 months ago

If anyone is still hitting this — I built cc-reaper as a workaround for orphan MCP server / subagent cleanup on macOS.

It adds a Stop hook that kills orphan processes when sessions end, a proc-janitor daemon for crash scenarios, and shell commands (claude-ram / claude-cleanup) for manual intervention. Uses TTY filtering to only target true orphans (PPID=1, no controlling terminal).

git clone https://github.com/anthropics/cc-reaper && cd cc-reaper && bash install.sh

41fred · 4 months ago

Adding another data point: found 11 orphaned mcp-server-cloudflare processes on macOS, 550% combined CPU, oldest running 48+ hours. Cloudflare's MCP server is the worst offender due to its heavier event loop and lack of SIGINT/SIGTERM handlers, but the underlying issue is generic.

We built a periodic monitor as a workaround — a 50-line bash script that runs every 10 minutes via macOS LaunchAgent, detects orphans by checking for PPID=1 (reparented to launchd), and kills them. Different approach from @theQuert's cc-reaper (which uses a Stop hook) — this catches crash scenarios where no hook fires.

Core detection:

ps -eo pid,ppid,%cpu,etime,command | grep 'mcp-server-' | grep -v grep | while read line; do
ppid=$(echo "$line" | awk '{print $2}')
[ "$ppid" -eq 1 ] && kill $(echo "$line" | awk '{print $1}')
done
This is especially critical for daemon setups where Claude Code sessions are spawned on behalf of remote users (e.g., Slack bots) — orphans accumulate from every session with zero visibility.

Environment: macOS Darwin 25.3.0, Claude Code (current), MCP server via npx.

theQuert · 4 months ago

@41fred Great data point — thanks for sharing the Cloudflare MCP findings. Just pushed cc-reaper v0.3.0 incorporating your feedback:

What's new:

  • Broader MCP coverage — Added npx mcp-server-* pattern to catch third-party MCP servers (Cloudflare, GitHub, etc.) across all three layers
  • PPID=1 detectionclaude-cleanup now uses PPID=1 as a fallback alongside TTY filtering, catching crash scenarios where no Stop hook fires
  • CPU metricsclaude-ram now shows CPU% per category + a dedicated Orphans (PPID=1) section for quick visibility
  • Zero-dependency LaunchAgent — New macOS-native daemon alternative (no Homebrew/Rust needed), runs every 10 minutes via launchd, detects orphans by PPID=1 — similar approach to your periodic monitor but with SIGKILL fallback and log rotation

The installer now offers an interactive choice between proc-janitor (feature-rich, grace period + whitelist) and the LaunchAgent (zero-dependency). Both complement the Stop hook layer.

git clone https://github.com/theQuert/cc-reaper && cd cc-reaper && bash install.sh

Repo: https://github.com/theQuert/cc-reaper | CHANGELOG

zachfeldman · 3 months ago

Noticed this issue too, it leads to massive resource usage if you spawn and kill a lot of sessions like I do. The following script finally killed all of the procs and their children (as always, feel pretty bad saying that, but that's the terminology 😆 ), at least for the Heroku MCP. I know it is pretty simple, but figured it might help some folks kill and see if killing worked for different MCP processes:

#!/usr/bin/env bash
set -euo pipefail

echo "Killing heroku-mcp-server..."
pkill -f 'heroku-mcp-server' || true
sleep 1

echo "Killing repl children..."
pkill -f 'npm exec heroku@latest --repl|heroku --repl' || true
sleep 1

echo "Remaining matches:"
pgrep -af 'heroku-mcp-server|npm exec heroku@latest --repl|heroku --repl' || true
41fred · 3 months ago

@zachfeldman @theQuert — released the periodic monitor as a standalone tool: mcp-orphan-monitor

@zachfeldman your pkill approach works for one-off cleanup, but the orphans come back every time a session crashes, force-quits, or the terminal gets closed. this runs every 10 minutes via macOS LaunchAgent and catches them automatically. detects orphans by PPID=1 (parent died, reparented to launchd) so it only kills true orphans, not active MCP servers.

complements cc-reaper too — the Stop hook handles clean exits, this catches crashes and force-quits where no hook fires. different layer, same goal.

git clone https://github.com/41fred/mcp-orphan-monitor.git
cd mcp-orphan-monitor
bash install.sh

50 lines of bash, no dependencies. also includes an optional macOS menu bar app (orphan count, scan-on-demand, kill history). works on Linux with a cron job.

zachfeldman · 3 months ago
@zachfeldman @theQuert — released the periodic monitor as a standalone tool: mcp-orphan-monitor @zachfeldman your pkill approach works for one-off cleanup, but the orphans come back every time a session crashes, force-quits, or the terminal gets closed. this runs every 10 minutes via macOS LaunchAgent and catches them automatically. detects orphans by PPID=1 (parent died, reparented to launchd) so it only kills true orphans, not active MCP servers. complements cc-reaper too — the Stop hook handles clean exits, this catches crashes and force-quits where no hook fires. different layer, same goal. git clone https://github.com/41fred/mcp-orphan-monitor.git cd mcp-orphan-monitor bash install.sh 50 lines of bash, no dependencies. also includes an optional macOS menu bar app (orphan count, scan-on-demand, kill history). works on Linux with a cron job.

Appreciate the work, but I'm on Ubuntu. A cron job might work but it's not super user friendly. I'll consider it if running the script periodically isn't working for me 😄 🙌

kazukinakai · 3 months ago

Dealt with this exact problem — 20+ orphaned npx/uvx processes piling up across sessions.

What fixed it for me was moving all MCP servers behind a single gateway process. Instead of Claude Code spawning individual processes per server, the gateway manages their lifecycle: lazy start on first tool call, idle-kill after 120s of inactivity, and clean shutdown when the gateway container stops.

Since everything runs in Docker, docker compose down guarantees all child processes are gone. No more PPID=1 orphans.

docker compose up -d  # one process to manage
docker compose down   # clean kill of everything

It's a different approach from monitoring/reaping orphans — it prevents them in the first place by having one long-lived process own the MCP lifecycle.

Repo: https://github.com/agiletec-inc/airis-mcp-gateway

zachfeldman · 3 months ago
Dealt with this exact problem — 20+ orphaned npx/uvx processes piling up across sessions. What fixed it for me was moving all MCP servers behind a single gateway process. Instead of Claude Code spawning individual processes per server, the gateway manages their lifecycle: lazy start on first tool call, idle-kill after 120s of inactivity, and clean shutdown when the gateway container stops. Since everything runs in Docker, docker compose down guarantees all child processes are gone. No more PPID=1 orphans. docker compose up -d # one process to manage docker compose down # clean kill of everything It's a different approach from monitoring/reaping orphans — it prevents them in the first place by having one long-lived process own the MCP lifecycle. Repo: https://github.com/agiletec-inc/airis-mcp-gateway

Cool idea! For now, I just disabled the Heroku MCP, which is the one that spawns a million processes. I wasn't really using it much anyway.

COOL-ANNZI · 3 months ago

Windows: Subagent MCP server orphaning

Environment: Windows 11, Claude Code 2.1.96 (VS Code extension + CLI), 5 MCP servers configured (context7, gitlab, redmine, jkit-msg channel, fakechat plugin)

Problem

Each subagent spawned via the Agent tool initializes its own full set of MCP servers. When the subagent completes and its claude.exe process terminates, the MCP process trees (cmd.exenode.exe/bun.exe) are not cleaned up and remain as orphans.

Observed behavior

  • 4 active claude sessions (2 VS Code, 2 CLI) with proper MCP children
  • 3 defunct claude processes (from earlier subagent runs) — still alive but with 0 cmd children, meaning their MCP servers detached and became orphans
  • 27 cmd.exe, 20 node.exe, 6 bun.exe accumulated — majority are orphans from terminated subagents
  • Each subagent spawns ~5 cmd + ~5 node/bun processes (one per MCP server), so a few subagent runs quickly accumulate 60+ zombie processes

Root cause

On Windows, child process trees are not automatically terminated when the parent exits. This requires explicit use of Win32 Job Objects with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. Without this, cmd.exenode.exe/bun.exe chains survive parent (claude.exe) termination.

This is especially impactful for subagents since they are short-lived but each spawns a full MCP server set.

Suggested fix

  • Use Job Objects when spawning MCP server processes on Windows
  • Alternatively, consider not initializing MCP servers for subagents that don't need them (most Explore/coder/reviewer agents never use MCP tools)
ABCreativeDesign · 2 months ago

Windows Desktop variant — window-close vs full-quit + MCP roster staleness

Adding evidence for the Windows Claude Code desktop manifestation. The macOS scenario in this issue (CLI-flavored, processes orphaned to PPID=1) has a desktop-app cousin on Windows that's adjacent enough to belong in this thread.

Scenario observed (Windows 11, Claude Code desktop, 2026-04-29)

User had a Node-based MCP server (custom, FigJam-bridging WebSocket on port 3766) registered in <project>/.mcp.json. Used the MCP server normally. Closed the Claude Code window via the X button (no system-tray Exit). Reopened a new CC window from the Start menu.

Symptoms:

  • Original node.exe MCP server process still alive — port 3766 still bound, original WebSocket connection from the client (Figma plugin) still ESTABLISHED
  • 8 claude.exe processes in tasklist (multiple from prior runs)
  • New CC window's MCP roster did NOT include the previously-registered MCP server. ToolSearch returned "no matching deferred tools" for the server's namespace.
  • .mcp.json and .claude/settings.local.json content unchanged and correct
  • MCP server's external state (port binding, Figma WS connection) was healthy — but CC's session-internal MCP roster never re-acquired it

This is a distinct Windows-desktop UI behavior that compounds the orphaning problem from this issue:

  1. Window-close ≠ app-exit on the desktop app. The user-facing X button leaves the back-end process alive (sticky tray behavior). MCP children spawned by the previous session continue running because nothing told them to stop.
  2. New window opens against the same back-end — but the in-memory MCP roster reflects whatever happened at the original session start, not the current state of spawned children.
  3. Re-spawning fails silently if a port-binding MCP server is involved (the previous server still holds the port; new spawn collides; CC marks it failed without UI surfacing).
  4. User-facing manifestation: the MCP client (Figma plugin in this case) shows "connected" because the server process is alive and reachable; meanwhile the CC main session has no record of the server. Confusing mismatch.

Concrete diagnostic

PS> netstat -an | Select-String ":3766"
  TCP    0.0.0.0:3766           0.0.0.0:0              LISTENING
  TCP    [::1]:3766             [::1]:64354            ESTABLISHED

PS> tasklist | findstr "node.exe claude.exe"
node.exe                     15728 Console        2     93,224 K   ← orphaned MCP server from prior session
node.exe                     14004 Console        2     80,700 K
claude.exe                   33928 Console        2    321,668 K   ← 8 claude.exe processes
claude.exe                   35172 Console        2     36,628 K
claude.exe                   34264 Console        2    135,632 K
claude.exe                    3932 Console        2     66,436 K
claude.exe                    4528 Console        2     84,208 K
claude.exe                   15044 Console        2     86,908 K
claude.exe                   36164 Console        2    660,968 K
claude.exe                   12472 Console        2    117,648 K

ToolSearch from inside the CC session at this moment returned "No matching deferred tools found" for the MCP server's namespace, despite the server process being alive and externally reachable.

Suggested host-side fixes (Windows desktop specifically)

  1. Use Windows Job Objects to tie spawned MCP children to the Claude Code parent's lifetime. With JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, when the parent dies (via crash, force-quit, or normal exit), all MCP children are killed automatically by the OS. This is the Linux-process-group equivalent and the canonical Windows approach.
  2. Distinguish window-close from app-quit explicitly. If the design intent is sticky tray behavior, that's fine — but window-close should EITHER kill MCP children (treating it as a session boundary) OR keep them alive AND ensure the next window opens against the same MCP roster (no spawn duplication, no roster staleness).
  3. Detect duplicate-spawn-on-port-collision and surface to the user. When the new spawn fails because a previous session's process still holds the port, current behavior is silent — the new MCP just doesn't appear. Surfacing this would let users self-repair.
  4. Health-check active MCP children at session start. If an existing process is healthy and reachable on the expected stdio handle, reuse it instead of marking the roster as failed.

Defense in depth — server-side hygiene

Independent of host-side fixes, MCP server authors can self-defend with two small additions to their server lifecycle. Documenting here for ecosystem benefit (and this is the pattern being applied to two custom MCP servers I'm building):

async function shutdown(reason: string) {
  console.error(`[mcp-server] shutdown: ${reason}`);
  // Actively close any held network resources
  wsServer?.clients.forEach(client => client.terminate());
  await Promise.all([
    new Promise<void>(r => wsServer?.close(() => r())),
    new Promise<void>(r => httpServer?.close(() => r())),
  ]);
  process.exit(0);
}

// stdin EOF — canonical "parent dropped me" signal in stdio MCP
process.stdin.on('end', () => shutdown('stdin EOF'));
process.stdin.on('close', () => shutdown('stdin close'));

// Explicit kill signals
['SIGTERM', 'SIGINT', 'SIGHUP'].forEach(sig =>
  process.on(sig as NodeJS.Signals, () => shutdown(sig))
);

if (process.platform === 'win32') {
  process.on('SIGBREAK' as NodeJS.Signals, () => shutdown('SIGBREAK'));
}

This pattern is missing from many published MCP servers (including some popular ones I checked) and contributes substantially to the orphan accumulation observed across this issue's reports. Worth documenting in the MCP SDK / official docs as a recommended pattern alongside whatever host-side fixes land.

Cross-references

Related Windows-platform issues that may share root causes or fix paths with this thread:

  • #48649 (subagents multiply MCP server processes — N×M kernel resource consumption)
  • #54554 (preview_stop leaves orphaned descendant processes)
  • #53134 (MCP servers spawned twice at startup — directMcpHost vs LocalMcpServerManager)

Happy to file the Windows-desktop variant as its own issue if maintainers prefer that over comment-on-#1935 — wanted to consolidate evidence here first since the underlying bug class is the same.

czs007 · 2 months ago

+1 with Linux / systemd-user data point — same root cause, different reaper.

Environment

  • Claude Code 2.1.128
  • Ubuntu 24.04.4 LTS, kernel 6.8
  • 92 GB RAM workstation
  • MCP servers configured: lark-mcp (Lark/Feishu)

Severity
After several weeks of regular use (open / close Claude Code sessions multiple times a day), I accumulated 80 orphaned lark-mcp node processes, each ~1.09 GB RSS:

$ pgrep -f "lark-mcp" | wc -l
80
$ ps -eo pid,ppid,rss,comm | awk '/node/ {print}' | sort -k3 -nr | head -3
3658492    3341 1141204 node   ← lark-mcp, RSS 1.09 GB
3699140    3341 1141204 node
2191627    3341 1141204 node
$ ps -p 3341 -o pid,cmd
3341 /usr/lib/systemd/systemd --user

The orphans are reparented to PPID=3341 (systemd --user) — the Linux equivalent of the macOS PPID=1 reparent in #33947. The user-level systemd doesn't reap them either; they survive across reboots of the controlling tty.

Measured impact (RSS delta after pkill)
While running a heavy parallel C++ build, OOM-killer started taking down cc1plus even at modest -j6 on a 92 GB box. Investigation:

| Step | mem used | swap | sum RSS |
| --------------------------- | ---------- | ------ | ------- |
| Before kill | 56 GB | 30 GB | 55.6 GB |
| pkill -9 -f "lark-mcp" | 24 GB | 14 GB | 21.2 GB |
| Delta | −32 GB | −16 GB | −34 GB |

(Caveat: the −34 GB sum-RSS overstates unique memory because shared libs are double-counted across processes; the −32 GB drop in used is the more reliable figure.)

Notes

  • Single lark-mcp instance @ ~1 GB is normal node/V8 baseline; the bug is the accumulation of 80 of them, not the per-instance size.
  • Linux reparent target is systemd --user (per-user manager), not PID=1. Worth a platform:linux triage note on top of the existing platform:macos reproductions.
  • A transient systemd-user unit with KillMode=control-group would reap these, but Claude Code spawns MCPs as plain forks that escape the parent session's process group.

Cross-ref: same mechanism on macOS in #33947, concurrent-session variant in #45880.

theQuert · 2 months ago

@/tmp/cc-reaper-comment-1935.md

theQuert · 2 months ago

@/tmp/cc-reaper-1935-followup2.md

limaronaldo · 1 month ago

I may have hit a downstream instance of this with claude-mem + chroma-mcp on macOS.

In my case, dozens of chroma-mcp processes accumulated and pointed at the same persistent Chroma data dir (~/.claude-mem/chroma). This caused the HNSW link_lists.bin file to balloon to ~1.67T apparent / 244G allocated, while the actual collection only had ~85K 384-dim embeddings.

Environment:

  • macOS 26.4.1 arm64
  • Claude Code 2.1.76
  • claude-mem 12.1.6
  • Zed 1.3.7 using Claude Code/ACP sessions

I suspect Claude Code/Zed multiple sessions amplified the issue, but the direct corruption path was multiple chroma-mcp writers surviving at the same time.

There is a more specific claude-mem issue because the actionable downstream fix appears to be in its ChromaMcpManager lifecycle/cleanup logic:

https://github.com/thedotmack/claude-mem/issues/2638

wojciecholszewski · 1 month ago

Fresh reproduction in the wild, mid-2026 — adding a concrete data point since this is still happening and the thread is active.

Environment

  • macOS 26.3.1 arm64 (Darwin 25.x)
  • Claude Code 2.1.153
  • Node.js 22.22.1
  • Workflow: multiple parallel Claude Code sessions across tmux panes/windows over several days, each opening different working directories that share the same set of stdio MCP servers in ~/.claude.json.

What I observed

For a single stdio MCP server (@a-bonus/google-docs-mcp@1.4.0, node ... index.js), ps -Ao pid,ppid,pcpu,etime,command | grep showed 59 live instances, all with PPID=1 (reparented to launchd). Ages ranged from ~2 minutes to ~3 days. Each idle instance was burning ~10–19% CPU (busy-loop bug in that particular server, separate problem — see workaround note below), so together they were eating ~420% CPU and ~1.2 GB RSS continuously, on top of being functionally dead.

These were not active children of any running claude process — every parent was init. The cleanup never happened on Ctrl+C / pane close / SIGHUP from the terminal closing / crash. The only thing that scales the leak is how many distinct sessions you open over time.

Why this matters even if the MCP server is "well-behaved"

On any stdio MCP server that happens to busy-loop while idle (and there are several in the wild), every orphaned instance is a permanent CPU+RAM tax until the user notices and runs pkill. A well-behaved server hides the leak but doesn't fix it — ps | wc -l still grows unbounded across sessions.

Workaround pattern other projects are adopting

a-bonus/google-docs-mcp shipped a fix in PR #112 (v1.9.0, 2026-04-26): "fix(stdio): kill orphaned servers via signal handlers + parent-PID watchdog". The MCP server polls its own getppid(); when it sees 1, it self-terminates. This works, but it pushes the lifecycle problem onto every MCP server author individually — which is exactly the pattern the MCP spec was supposed to avoid by making the host process responsible for child lifecycle.

Concrete asks for Claude Code

  1. SIGTERM all child MCP processes on every exit path, not just clean shutdown — Ctrl+C, SIGHUP (terminal close), SIGTERM, uncaught exception, and any other non-zero exit. macOS does not auto-kill children of a dead parent; this has to be explicit.
  2. Process group / setpgid + killpg on exit would catch the case where the MCP server itself spawned its own children (helper processes, headless browsers, etc.) — those grandchildren leak too.
  3. On startup, optionally reap orphaned MCP processes matching the configured command lines whose PPID=1 — many users won't notice until they're hundreds deep.
  4. Closely related and probably the same root cause: #37482 (stdin pipe lost after ~10–20 minutes → orphaned to PID 1). Worth treating them as one fix.

Happy to share a sanitized ps snapshot or run any diagnostic if useful.

reikys · 14 days ago

Still reproducible on v2.1.198 (macOS, Darwin 25.5.0). Some data that may help pinpoint the fix:

After a few weeks of normal use I found 75 orphaned stdio MCP server processes, all reparented to launchd (PPID=1):

$ for pid in $(pgrep -f "my-mcp/server.ts"); do ps -o pid=,ppid= -p "$pid"; done
 2415     1
 3355     1
 ... (73 more)

Two observations:

  1. Claude Code exits without signaling its stdio MCP children. Every one of these servers had SIGINT/SIGTERM handlers installed that call process.exit(0) — if the client had sent SIGTERM on exit, they would have terminated. They didn't, so no signal is being delivered on (at least some) exit paths.
  1. The MCP spec's stdio shutdown sequence says the client SHOULD close the child's stdin, wait for exit, then escalate to SIGTERM and finally SIGKILL. Claude Code appears to stop after closing stdin (or skips even that on abnormal exit), so any server whose event loop is kept alive by timers/sockets lingers forever and keeps consuming resources.

Workaround for server authors until this is fixed — self-detect parent death:

process.stdin.once("end", cleanup);  // spec-correct: exit on stdin EOF
setInterval(() => { if (process.ppid === 1) cleanup(); }, 30_000);  // fallback (macOS/Linux)

Given the number of closed duplicates of this report (#40667, #42881, #60747; #66280 still open for the subagent variant), a SIGTERM-with-timeout follow-up in the client's shutdown path would resolve this whole class of leaks.

omelnickiy · 11 days ago

Another reproduction with a twist: this also happens without the user ever exiting Claude Code — Claude Desktop's local agent mode (CCD) recycles its embedded claude processes mid-session, abandoning the MCP children each time.

Environment

  • Claude Desktop 1.18286.0, embedded Claude Code 2.1.197
  • macOS Darwin 25.5.0 (arm64)
  • MCP servers: stdio, configured at user scope, launched via uv run (Python) and bun (plugin MCP)

What happens

Within a 30-minute window of normal Claude Desktop usage (one chat session plus one app restart), the desktop app launched the embedded claude binary 4 times for the same logical session (first query, a later query cycle, and warm-ups after an app restart — main.log shows [WarmLifecycle:preview] Warming up session ... events matching the spawn times). Each launch spawned the full set of configured stdio MCP servers; each replaced claude process died without terminating its children.

Result: 4 orphaned copies of each MCP server, reparented to launchd:

PID   PPID  STARTED              COMMAND
3608     1  12:59:06             uv --directory .../telegram-mcp run main.py
4174     1  13:03:35             uv --directory .../telegram-mcp run main.py
7751     1  13:25:09             uv --directory .../telegram-mcp run main.py
7908     1  13:29:56             uv --directory .../telegram-mcp run main.py

All orphans had CLAUDE_CODE_ENTRYPOINT=claude-desktop and __CFBundleIdentifier=com.anthropic.claudefordesktop in their environment, confirming the desktop app (not a terminal session) as the launcher.

Two compounding factors

  1. Host side (the bug): CCD replaces claude processes (query cycles, warm-lifecycle, app restart) without killing the children's process group.
  2. Server side (why some linger forever): servers that exit on stdin EOF (e.g. the bun-based telegram plugin server) clean themselves up eventually. Servers that don't (our Python/Telethon server — pending asyncio tasks keep the loop alive past EOF, and when launched via uv run the Python process's own PPID never changes because the intermediate uv process survives) accumulate indefinitely.

Workaround that works

We patched the Python server with a watchdog thread: capture the grandparent PID (uv's parent = the claude host) at startup, poll it with os.kill(pid, 0) every 10s, and os._exit(0) when it disappears (also exit if own PPID becomes 1, and hard-exit after the stdio server returns). Verified: orphans now self-terminate ~10s after the host dies.

A proper fix on the host side would be killing the spawned MCP servers' process group (or at least the direct children and their descendants) whenever a claude process is replaced or torn down — including the desktop warm-lifecycle path, not just interactive CLI exit.