[BUG] MCP servers not properly terminated when Claude Code exits, causing orphaned processes
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
- Configure one or more MCP servers using
claude mcp add <name> <command> - Start Claude Code and use it normally (any session that might initialize MCP servers)
- Exit Claude Code normally
- Check running processes with
ps aux | grep mcp - Observe orphaned MCP server processes still running
- 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.
39 Comments
We have a fix on the way!
This should be fixed in 1.0.21. Thanks for reporting @noomz!
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?
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.
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
claudecommand.Thanks, we'll look into this again.
Hi all, please update to 1.0.38 and let me know if the issue persists. This should be much more robust now.
Checking in here. Is this working for everyone? @CoreyWinkelmannPP @mstllc @noomz
@ashwin-ant working for me on macOS 26 and Claude Code 1.0.43. Love CC, keep up the great work!
@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 fromnode_modules/.bin/the-bininstead of vianpx/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.
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.
@ashwin-ant I did some digging, it looks like when run in
--printmode, 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@sbking nice catch on
--printmode! We have a fix on the way. Are you seeing this in interactive mode too still?Still seeing this in interactive mode on latest version 1.0.73 (Windows)
Still seeing this happening in 1.0.84
Still seeing this happening in 1.0.85
Still seeing this on v2.0.29
We’ve reproduced hangs with
claude-agent-sdk-python v0.1.10when MCP servers are active and when breaking early from the query stream.Root cause analysis:
@azure-devops/mcp,exa-mcp-server, etc.).setting_sources=["user","project","local"], which caused MCP discovery even though servers were “disabled.”→ Removing
setting_sourcesor passingmcp_servers={}prevents this.query()blocks forever.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:
do_epoll_wait.Workarounds tested:
MAX_EXECUTION_TIMEwatchdog with process‑tree cleanup.setting_sourcesor overriding withmcp_servers={}.Recommendations:
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.
Still experiencing this issue on 2.1.9. Many many node.js processes left over that never get term'd.
Adding another data point: on an EC2 instance running Claude Code over SSH+tmux, I found 14 orphaned
mcp-todoistprocesses 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.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.
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.shAdding 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.
@41fred Great data point — thanks for sharing the Cloudflare MCP findings. Just pushed cc-reaper v0.3.0 incorporating your feedback:
What's new:
npx mcp-server-*pattern to catch third-party MCP servers (Cloudflare, GitHub, etc.) across all three layersclaude-cleanupnow uses PPID=1 as a fallback alongside TTY filtering, catching crash scenarios where no Stop hook firesclaude-ramnow shows CPU% per category + a dedicatedOrphans (PPID=1)section for quick visibilitylaunchd, detects orphans by PPID=1 — similar approach to your periodic monitor but with SIGKILL fallback and log rotationThe 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.
Repo: https://github.com/theQuert/cc-reaper | CHANGELOG
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:
@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.
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
cronjob might work but it's not super user friendly. I'll consider it if running the script periodically isn't working for me 😄 🙌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 downguarantees all child processes are gone. No more PPID=1 orphans.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.
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
Agenttool initializes its own full set of MCP servers. When the subagent completes and itsclaude.exeprocess terminates, the MCP process trees (cmd.exe→node.exe/bun.exe) are not cleaned up and remain as orphans.Observed behavior
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.exe→node.exe/bun.exechains 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
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:
node.exeMCP server process still alive — port 3766 still bound, original WebSocket connection from the client (Figma plugin) still ESTABLISHEDclaude.exeprocesses in tasklist (multiple from prior runs).mcp.jsonand.claude/settings.local.jsoncontent unchanged and correctThis is a distinct Windows-desktop UI behavior that compounds the orphaning problem from this issue:
Concrete diagnostic
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)
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.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):
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:
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.
+1 with Linux / systemd-user data point — same root cause, different reaper.
Environment
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-mcpnode processes, each ~1.09 GB RSS:The orphans are reparented to PPID=3341 (
systemd --user) — the Linux equivalent of the macOS PPID=1 reparent in #33947. The user-levelsystemddoesn'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
cc1pluseven at modest-j6on 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
usedis the more reliable figure.)Notes
lark-mcpinstance @ ~1 GB is normal node/V8 baseline; the bug is the accumulation of 80 of them, not the per-instance size.systemd --user(per-user manager), not PID=1. Worth aplatform:linuxtriage note on top of the existingplatform:macosreproductions.KillMode=control-groupwould 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.
@/tmp/cc-reaper-comment-1935.md
@/tmp/cc-reaper-1935-followup2.md
I may have hit a downstream instance of this with
claude-mem+chroma-mcpon macOS.In my case, dozens of
chroma-mcpprocesses accumulated and pointed at the same persistent Chroma data dir (~/.claude-mem/chroma). This caused the HNSWlink_lists.binfile to balloon to ~1.67T apparent / 244G allocated, while the actual collection only had ~85K 384-dim embeddings.Environment:
I suspect Claude Code/Zed multiple sessions amplified the issue, but the direct corruption path was multiple
chroma-mcpwriters surviving at the same time.There is a more specific
claude-memissue because the actionable downstream fix appears to be in its ChromaMcpManager lifecycle/cleanup logic:https://github.com/thedotmack/claude-mem/issues/2638
Fresh reproduction in the wild, mid-2026 — adding a concrete data point since this is still happening and the thread is active.
Environment
~/.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 | grepshowed 59 live instances, all withPPID=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
claudeprocess — 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 -lstill grows unbounded across sessions.Workaround pattern other projects are adopting
a-bonus/google-docs-mcpshipped 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 owngetppid(); when it sees1, 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
PPID=1— many users won't notice until they're hundreds deep.Happy to share a sanitized
pssnapshot or run any diagnostic if useful.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):Two observations:
SIGINT/SIGTERMhandlers installed that callprocess.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.Workaround for server authors until this is fixed — self-detect parent death:
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.
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
claudeprocesses mid-session, abandoning the MCP children each time.Environment
uv run(Python) andbun(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
claudebinary 4 times for the same logical session (first query, a later query cycle, and warm-ups after an app restart —main.logshows[WarmLifecycle:preview] Warming up session ...events matching the spawn times). Each launch spawned the full set of configured stdio MCP servers; each replacedclaudeprocess died without terminating its children.Result: 4 orphaned copies of each MCP server, reparented to launchd:
All orphans had
CLAUDE_CODE_ENTRYPOINT=claude-desktopand__CFBundleIdentifier=com.anthropic.claudefordesktopin their environment, confirming the desktop app (not a terminal session) as the launcher.Two compounding factors
claudeprocesses (query cycles, warm-lifecycle, app restart) without killing the children's process group.uv runthe Python process's own PPID never changes because the intermediateuvprocess survives) accumulate indefinitely.Workaround that works
We patched the Python server with a watchdog thread: capture the grandparent PID (
uv's parent = theclaudehost) at startup, poll it withos.kill(pid, 0)every 10s, andos._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
claudeprocess is replaced or torn down — including the desktop warm-lifecycle path, not just interactive CLI exit.