Claude Desktop local-agent (Cowork) sessions leak claude-code node processes (never exit at session end) — GBs of RAM + MCP port exhaustion

Status Open
Reported on v2.1.181
Maintainer reply None cached
Activity 4 comments · opened Jun 25, 2026

Summary

On macOS, local Claude Code sessions launched by the Claude Desktop app (the "Cowork" / local-agent mode, where the Desktop app spawns claude-code/<version>/claude.app/Contents/MacOS/claude node processes) are not reaped when the session ends. The node process keeps running indefinitely after the conversation is over, and it keeps its child MCP-server processes alive with it. Over hours/days these accumulate into dozens of multi-hundred-MB processes, and the surviving MCP children keep holding the OS resources they own — notably TCP ports — which eventually breaks new sessions.

Environment

  • Claude Desktop (macOS) launching local Claude Code sessions and scheduled tasks
  • Claude Code version 2.1.181
  • Per-session process lineage: /Applications/Claude.app/Contents/MacOS/Claude…/Contents/Helpers/disclaimer ……/claude-code/<ver>/claude.app/Contents/MacOS/claude (the node) → MCP-server children

Observed behavior

  • Every local session (and every scheduled-task run) spawns a claude-code node process under the Desktop app.
  • When the session/conversation ends, that node process does not exit. SessionEnd hooks do fire (so the session logically ends), but the process keeps running.
  • Lingering nodes accumulate without bound until the Desktop app is restarted. Observed dozens at once (50+), including processes from sessions started 1–3 days earlier still running. In a fresh snapshot after a restart: ~5 nodes from a single morning's scheduled-task burst, each ~230–380 MB RSS (~1.2 GB total); the count climbs as more sessions run.
  • The lingering node keeps its child MCP servers alive too. Concretely, a stdio MCP server it spawned (in our case the Google Workspace MCP, workspace-mcp) keeps running and keeps holding the local TCP port it bound (we saw 17 workspace-mcp processes alive in one snapshot).
  • The per-session registry files at ~/.claude/sessions/<node_pid>.json are also not cleaned up — they persist after the session ends and carry no heartbeat (mtime is frozen at session start), so they can't be used to tell a live session from a leaked one.

Impact

  1. Unbounded memory leak: dozens of multi-hundred-MB node processes (>1 GB quickly) plus their MCP-server children, until the Desktop app is restarted.
  2. Concrete downstream failure (how we found it): each leaked node's workspace-mcp child keeps holding its OAuth-callback TCP port. workspace-mcp binds within a small fixed port range per account (e.g. 8001–8005). Once leaked instances fill the range, a new session's Google Workspace MCP can't bind and exits with No available port in range → the connector shows "Failed to connect." This presents as an intermittent, confusing auth-looking failure that is actually port exhaustion caused by the process leak.
  3. The only remediation is quitting and reopening the Desktop app.

Steps to reproduce

  1. On macOS, use Claude Desktop's local-agent / Cowork mode (and/or scheduled tasks) to run several Claude Code sessions over time. Have each connect at least one stdio MCP server that binds a local socket (e.g. a Google Workspace MCP).
  2. End each session normally.
  3. ps -ax | grep 'claude-code/.*MacOS/claude' — node processes from ended sessions remain running indefinitely; their MCP-server children remain too. The count grows with each session and never decreases until the Desktop app restarts.

Expected behavior

When a local session ends, its claude-code node process and the MCP-server children it spawned should be terminated, and its ~/.claude/sessions/<pid>.json cleaned up.

Notes / suggested investigation

  • Node process not exiting at session teardown (an open libuv handle / unref'd timer / lingering socket keeping the event loop alive?).
  • MCP child processes not being killed when their owning session ends (and, relatedly, stdio MCP servers that don't exit on stdin EOF can outlive even a dead parent).
  • The session-registry file lacking an end marker / heartbeat leaves external cleanup tooling unable to distinguish active vs ended sessions.

Severity: Medium-high — silent multi-GB memory growth on long-running Desktop installs, plus intermittent MCP failures that look like auth bugs.

View original on GitHub ↗

4 Comments

jacksonhux · 2 months ago

Corroborating this on a newer version: it still reproduces on Claude Code 2.1.187 (this issue was filed on 2.1.181), macOS, sessions launched by Claude Desktop's scheduled tasks.

Setup: one hourly scheduled task. Every fire leaves exactly one …/claude-code/<ver>/claude.app/Contents/MacOS/claude --output-format stream-json agent process that never exits. Clean one-per-fire fingerprint over 8 hours (paths/user redacted):

started 13:06  elapsed 6h58m  cpu 2m15s
started 14:06  elapsed 5h58m  cpu 1m56s
started 15:06  elapsed 4h58m  cpu 1m38s
started 16:06  elapsed 3h58m  cpu 1m18s
started 17:06  elapsed 2h58m  cpu 1m01s
started 18:06  elapsed 1h58m  cpu 0m41s
started 19:06  elapsed 0h58m  cpu 0m22s
(the 20:06 fire then added an 8th; none have exited)

Each process is ~390 MB RSS and accumulates only ~20 s of CPU per hour — i.e. it is idle/sleeping after its work finished, not still working. The task's actual workload is plain Python subprocesses that exit cleanly on every run; only the wrapping claude agent session lingers.

Prior severe impact on the same machine, before I started watching for it: these accumulated to 214 sessions → load ~96 on a 10-core M4, and the box overheated. The only thing that cleared them was quitting/reopening Claude Desktop — which is consistent with "restart reaps the children" and with the OP's findings.

Detection note that may help others: count the agent sessions with

ps -axo command | grep "MacOS/claude --output-format" | grep -v "Helpers/disclaimer"

A naive grep -i claude only counts the stable Claude Desktop Electron app tree (~11–13 processes) and is blind to these leaked agent sessions.

The leaked sessions had MCP tools enabled (computer-use plus a couple of stdio MCP servers), which fits the "an MCP child keeps the event loop alive / doesn't exit on stdin EOF" hypothesis raised in the issue.

blwfish · 1 month ago

Corroborating this from a third, independent trigger path — not Cowork, not scheduled tasks. Plain cmd-click / "Open in → New Window" local-agent-mode sessions (the mechanism in #63016) leak identically, so this isn't Cowork-specific code — it's shared local-agent-mode session-teardown machinery. area:cowork undersells the scope.

Setup: macOS, Claude Code 2.1.219, Claude Desktop. No Cowork feature touched, no scheduled tasks. Sessions opened by cmd-clicking to spawn parallel windows over the course of a day (2026-07-28 into 2026-07-29), each with a user-level MCP server config (~/.claude.json) providing several stdio MCP servers, including one that shells out to a local socket-based bridge process (freecad-mcp, a third-party FreeCAD control server).

Evidence this isn't an error/crash-path artifact: I picked a session at random and read its full transcript. It ran a normal diagnostic task, reached a complete, correct resolution, and ended with an ordinary final assistant message — no exception, no hang, no crash on Claude's side:

session first message:  2026-07-29 06:39:52 EDT
session last message:   2026-07-29 07:19:30 EDT  ("Noted and saved.")

Cross-referencing against ps and the per-session registry file at ~/.claude/sessions/<pid>.json (which records sessionId, cwd, procStart, giving an authoritative PID↔session mapping rather than a timestamp guess), the session's own claude node process spawned its MCP bridge child 1 second before the session's first message — and as of this writing, hours after the conversation's clean natural end, that bridge process (and its parent claude node) are both still running, still in a normal idle sleep state, no crash, no zombie/defunct state. A second session ("Weekly FC review and version build") shows the identical pattern: bridge spawned 1 second before the session's first message, still alive long after the session's last message.

(Note: I couldn't find any field — in the process command line or in the ~/.claude/sessions/<pid>.json registry, which is identical in shape for every session regardless of how its window was opened — that records how a given session's window was created. So I can't independently verify from logs alone that these were specifically cmd-click-spawned rather than some other multi-window mechanism; that's from the reporter's own recollection of their workflow that day, not something the logs confirm one way or the other.)

Confirms the OP's isRunning: false-but-alive finding, with the socket-holder identified: list_sessions (the app's own session-tracking) reports both of these as isRunning: false. lsof shows the file descriptor on the other end of each leaked claude process's stdin/stdout socket is still held open by /Applications/Claude.app/Contents/MacOS/Claude — the main Electron process itself, not some intermediate helper. So this isn't a case of a child process holding a stray fd open; the app's own top-level process never released its connection to the session when it updated the session list to say the session had ended.

Scale, for reference: across roughly one day of intermittent multi-window usage, I found 7 leaked claude node processes (confirmed against ~/.claude/sessions/<pid>.json registry files and cross-checked as isRunning: false via the session-list API — one additional process I initially miscounted turned out to be a still-live, legitimately-running session, not a leak), each with its own leaked MCP bridge child, none reaped. All 7 mount the same shared local-agent-mode plugin directory.

Downstream effect worth flagging for anyone chasing a similar report: because MCP server discovery/config in my case is user-level, every one of these 8 leaked sessions independently holds its own connection-capable bridge process to the same external tool (a single FreeCAD instance, in my case globally discoverable via a ~/.cache registry). A live, current session's tool calls against that same external resource can then contend with however many leaked sessions' bridges are still sitting around from days prior — producing confusing "operation in flight, never returns" symptoms in an otherwise-unrelated live session, with no obvious link back to the real cause (leaked sessions from unrelated past conversations) unless you go looking for it via ps/lsof the way this thread did. If anyone else is debugging a mysterious hang against a shared local tool/service and lands here from search: check for leaked sessions before assuming the tool itself is at fault.

Happy to provide the full ps/lsof output or list_sessions JSON if useful for triage.

Possibly worth consolidating: #69854 and #77940 both look like the same underlying defect (session-teardown not reaping the node process / not releasing its connection) surfacing through yet more trigger paths — scheduled tasks and background "bg" sessions respectively. #80885 also matches the isRunning: false-but-alive symptom directly. Between the four of us we've now got four independent trigger paths (Cowork, scheduled tasks, background sessions, cmd-click) all hitting what reads like one shared teardown bug in local-agent-mode session lifecycle management — might be worth relabeling this one beyond area:cowork and treating it as the canonical thread, if that's useful for triage.

Distinguishing this from the older, broader MCP-cleanup issues: #1935 and #79567 also describe MCP server processes surviving session end, and given #1935's age (filed June 2025) and comment count it's an easy target to fold new reports into by default. But both of those describe plain CLI session exit (exit, closing a terminal tab) — a different code path than what's going on here. This report and #69854/#77940/#80885 are specifically about the Desktop app's interactive/local-agent-mode session lifecycle: the app's own bookkeeping (isRunning) flips to false while the app's own top-level Electron process (not just some MCP child) still holds a live, connected IPC socket to the "ended" session. That's a different failure than "the CLI didn't SIGTERM its MCP children on exit" — here the CLI process itself never even got the signal to exit, because the thing that was supposed to close its connection didn't. Worth keeping this thread separate from #1935/#79567 even if they turn out to share a root cause, since the repro and the actual defective code path aren't the same.

blwfish · 1 month ago

Follow-up on my comment above, on why this went from presumably-present-but-unnoticed to hitting three times in one hour for me: recording the reproduction conditions I was able to pin down, in case they're useful to anyone trying to reproduce this deliberately.

A leaked session is silent by itself — idle at ~0% CPU, no crash, nothing to notice — until enough of them accumulate and one happens to collide with something live on a resource it's still holding open (in my case, a shared local MCP server instance). So severity/visibility should scale with two things: how long since my last full app quit (nothing gets reaped until then), and how many session-spawn events happened in that window.

Two things I was able to check directly, not just estimate:

  • The Desktop app itself (the top-level Electron process, not individual sessions) had been continuously running for 34+ hours at the time of the incident — confirmed against the actual PID via ps, not guessed.
  • I checked whether that stretch was actually unusual for me by looking at how long each Claude Code CLI version stayed current in my own session history before the next one showed up. It wasn't the longest such stretch I've had in the past two months — several were comparably long or longer. So "I was forced to restart for a new version" doesn't by itself explain why this particular stretch is the one that produced visible symptoms when others apparently didn't.

The piece I can't independently verify from any log I have access to, though I can verify a closely related fact that sharpens it: running multiple concurrent sessions is not new behavior for me -- my own conversation history shows real overlapping multi-session activity going back months, well before this incident, so the total amount of parallelism isn't the variable that changed. What I believe changed, based on my own recollection of how I've been working rather than anything logged: in the 2-3 days before this incident I shifted a lot of that routine parallelism from separate Desktop app processes (launched fresh from separate shells) over to local-agent-mode/cmd-click spawning within a single already-running app instance. No field I could find anywhere (session registry, conversation metadata, raw transcript JSON) records how a given session's window was opened, so which mechanism produced any given past session isn't something I can confirm from logs -- this part is my own firsthand read on a recent change in my usage pattern, not measured. Flagging that distinction explicitly rather than presenting it as harder evidence than it is. But it matters mechanistically: separate app processes are independent process trees that get torn down cleanly by the OS when each one quits, while cmd-click sessions all share the one long-lived Electron process this bug lives in. If that shift is real, it would concentrate months of routine parallelism onto the one code path that doesn't reap its children, which is a more precise candidate trigger than "more parallel sessions" would be. Combined with the two confirmed points above, it's a coherent hypothesis for why this surfaced now, rather than being a mystery stacked on top of an already-known bug.

danstern1807 · 25 days ago

Corroborating this on Windows, and on much newer versions. The issue is labeled platform:macos, but the same leak reproduces on Windows 10 — and I have a diagnostic detail that may point at what holds the process open.

Environment

  • Windows 10 Pro 19045, Claude Desktop app (session registry reports entrypoint: claude-desktop)
  • Claude Code 2.1.219 and 2.1.221 (the app's bundled CLI). This issue was filed on 2.1.181 and corroborated on 2.1.187, so the confirmed range is now ~40 releases wide.
  • Trigger path: Desktop app scheduled tasks — one task on cron 5-55/10 * * * * (every 10 minutes).

Observed

Same as the original report: every scheduled-task run spawns a claude.exe child of the Electron host, and that child never exits after the run completes. It accumulated to 49 processes holding 21 GB of commit charge out of a 40 GB commit limit, leaving 156 MB free — at which point the machine could no longer allocate anything:

  • the Electron app crashed
  • claude --version died with a Bun stack overflow
  • PowerShell's ConvertFrom-Json threw OutOfMemoryException on an 8 KB file

Roughly 470–550 MB commit per leaked process, ~6 runs/hour ≈ 4 GB/hour. Restarting the Desktop app is what clears it, matching the report.

Possible holder: the process's own HTTPS keep-alive pool

A leaked process whose run has demonstrably finished (transcript frozen, final report written) still holds established connections to the API:

PID 29904 (v2.1.221)   commit 469 MB   threads 31 (all in Wait)   CPU 0.5%
TCP: 9 x Established -> 160.79.104.10:443      (api.anthropic.com)
                      + 2607:6bc0::10:443

Sampled 170 seconds apart: transcript growth 0 lines, CPU +0.92 s, sockets still Established.

The control group is what makes this sharp. On one day, 58 of 91 scheduled runs died early on API Error: Unable to connect to API (ENOTFOUND) during two network outages. Every one of those exited cleanly. The 33 runs that did real work all leaked. So: work => leak, no work => clean exit.

The early-failing runs never resolved DNS, so they never established the API connection pool and never spawned MCP children. That suggests the parent's own undrained agent keep-alive connections are sufficient on their own to hold the process open — i.e. a mechanism independent of (and additional to) the MCP-child-holding-a-port mechanism described in the original report.

One correction, offered constructively

The report states that the ~/.claude/sessions/<pid>.json files "carry no heartbeat (mtime is frozen at session start), so they can't be used to tell a live session from a leaked one." They can, if you combine them with the transcript:

  1. sessions/<PID>.json gives an authoritative pid -> sessionId map, plus startedAt — validate that against the live process start time to guard against PID reuse.
  2. The session's own transcript is <sessionId>.jsonl. Its head contains a <scheduled-task name=\"...\"> marker for scheduled runs, which distinguishes them from interactive sessions. (Note the quotes are JSON-escaped inside the JSONL, so a naive name="..." regex misses them.)
  3. That transcript's last-write time is the missing heartbeat — a finished run stops writing within about a minute, while a live one keeps appending.

Gating on all of it together — scheduled-task marker, transcript quiet >= 10 min, process age >= 30 min, and a live CPU sample near zero — reaps leaked runs reliably without ever touching a live interactive session.

One warning for anyone else building a workaround: matching processes to transcripts by creation time is not safe. I tried that first, and a dry run showed it mapping my live interactive session to a scheduled run's transcript that had been created 5 seconds earlier — it would have killed the active conversation. The sessions/<PID>.json map removes the guesswork.