[BUG] Windows local-agent-mode: agent session processes + MCP fleets not reaped on session end (accumulate across launches)

Status Closed — not planned
Reported on v2.1.170
Maintainer reply None cached
Activity 5 comments · opened Jun 14, 2026 · closed Aug 29, 2026

Summary

On Windows, Claude Desktop's local-agent-mode does not tear down an agent session's process tree when the session ends. Each launched session leaves its claude.exe (embedded Claude Code) and its entire MCP server fleet running. They accumulate one full fleet per launch until manually killed.

This is distinct from the existing reports in two ways:

  1. It is the local-agent-mode orchestration inside the desktop app, not the bare CLI (#33947) or Task subagents (#66280).
  2. The session claude.exe process itself survives — it stays parented to the live Claude Desktop app, it is not orphaned/re-parented (no PPID=1 equivalent). So the orchestrator is failing to end the session process at all, which is upstream of the MCP-orphan problem others have described.

Related: #33947 (macOS, canonical, CLOSED), #66280 (macOS Task subagents, OPEN), #38228 (Windows uv/uvx, closed as dup of #33947).

Environment

  • Embedded Claude Code: 2.1.170 (AppData\Roaming\Claude\claude-code\2.1.170\claude.exe)
  • Host: Claude Desktop app 1.12603.1.0, local-agent-mode
  • OS: Windows 11 Pro 10.0.26200
  • MCP servers (per session): a python3 stdio server, headroom, serena (via uvx → uv → serena.exe → python language servers), plus context7 / playwright / memory (each cmd → npx → node)

Observed behavior

Over ~24h, 7 agent sessions were launched. Each spawned a full MCP fleet; none were reaped on session end. Snapshot before cleanup:

  • 7 × headroom mcp serve
  • 7 × serena instances — their dashboards bound to incrementing ports 24282, 24283, 24284 … 24288 (one per live instance), which is itself a clean fingerprint of the leak
  • the per-session node/python MCP fleets
  • ~21 serena/headroom/uvx processes + dozens of node/python; several GB RAM
  • All 6 stale claude.exe session roots were still alive, each parented to the single live Claude Desktop app (explorer-launched). Only the newest session was actually in use.

Each session's claude.exe is launched by the desktop app with an inline --mcp-config {…} and --plugin-dir …\local-agent-mode-sessions\<uuid>\… (the local-agent-mode marker). The direct children of a live session process:

python3   <stdio mcp server>
cmd  → npx @upstash/context7-mcp
cmd  → npx @playwright/mcp@latest
headroom  mcp serve
uvx  → serena start-mcp-server --project-from-cwd --context claude-code
cmd  → npx @modelcontextprotocol/server-memory

Reproduction

  1. Windows + Claude Desktop with local-agent-mode and several MCP servers configured (include a uvx-launched one such as serena).
  2. Start an agent session, use it, then close / move on. Start a fresh session next time.
  3. Repeat across a day.
  4. Observe the fleet grow one generation per launch:

``powershell
Get-CimInstance Win32_Process -Filter "Name='headroom.exe'" |
ForEach-Object { "$($_.ProcessId) <- parent $($_.ParentProcessId)" }
Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -ge 24282 -and $_.LocalPort -le 24292 }
`
Serena dashboard ports increment (24282, 24283, …) and old
claude.exe` session roots remain alive under the Desktop app PID.

Expected behavior

When a local-agent-mode session ends, the orchestrator should terminate that session's claude.exe and its entire MCP subprocess tree.

Root-cause notes (Windows-specific)

  • Windows has no PR_SET_PDEATHSIG; children are not auto-killed when a parent exits. Reliable teardown needs a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, or explicit recursive PID tracking + tree-kill.
  • The MCP launch chains are 3–4 levels deep (uvx → uv → serena.exe → python; cmd → npx → node). A cleanup that only signals/kills the direct child orphans the deep workers — so per-session job-object containment (or taskkill /T) is required, not just closing stdin on the immediate child.
  • Critically, here even the top-level session claude.exe is never signaled, so this is upstream of the MCP-orphan issue: the orchestrator isn't ending the session process at all.

Workaround

Manually tree-kill stale session roots while keeping the newest: find every claude.exe that is a parent of a headroom/serena process, then taskkill /PID <root> /T /F. This dropped the host from ~21 to 3 of these processes with no impact on the live session.

---
Filed from a Windows Claude Code session after diagnosing the leak on the user's machine.

View original on GitHub ↗

4 Comments

github-actions[bot] · 2 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/61564
  2. https://github.com/anthropics/claude-code/issues/57740
  3. https://github.com/anthropics/claude-code/issues/62107

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

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

🤖 Generated with Claude Code

RobertKodes · 2 months ago

Love the structured output approach and the CLAUDE.md convention for project context. One area that could be interesting: support for monorepo setups where different subdirectories might have different coding conventions or tech stacks. Has this been discussed?

kendrick90 · 2 months ago

This is the same kind of issue as with the updates constantly saying the file is locked because of orphaned grep, find, tail and other child processes that are not killed when main claude.exe is killed. As you said it is windows child process differences.

JohnnyRayWork · 1 month ago

Corroborating this on newer builds, plus a probable root cause and a confirming test.

Still reproduces: Claude Code 2.1.197, Claude Desktop 1.18286.0.0, Windows 11 (26200). Same signature as the report: each finished session's claude.exe --output-format stream-json --input-format stream-json --permission-prompt-tool stdio [--resume <id>] stays resident as a direct child of the single live desktop-app root PID (confirmed with Get-CimInstance Win32_Process -- every hung worker's PPID is the app root; none re-parented or orphaned). Accumulation here is ~2/hr.

Concrete "turn done, process alive" instance: a worker whose transcript .jsonl's last record was a completed assistant message, idle 20+ minutes, with its claude.exe still running and still holding its MCP subtree alive (uvx -> uv -> <server>.exe -> python). Nothing was pending -- it simply never exited.

Probable mechanism (hypothesis): every worker is launched with --input-format stream-json, i.e. it is stdin-driven. If the app keeps the child's stdin pipe open after the turn completes (never sends EOF / never closes the write end), the CLI's stdin reader keeps the Node.js event loop alive and the process blocks forever waiting for input that never comes. That lines up exactly with the observation here that the orchestrator "is failing to end the session process at all" -- the surviving MCP fleet is downstream of that; killing the parent claude.exe takes the whole subtree with it.

Confirming test for whoever picks this up: on a resident-but-finished worker, run Sysinternals handle.exe -p <pid> and check whether stdin (a named pipe) is still open / the process is parked in a stdin read. If so, the fix is app-side: on turn completion, close the child's stdin (or send an explicit session-terminate) and reap the child.

Safe workaround if anyone needs one now: a scheduled reaper that kills only \claude-code\ workers that are provably idle + old. The reliable liveness signal is the worker's transcript .jsonl mtime, not CPU -- a live worker sitting in an API wait shows ~0% CPU while still mid-turn, so CPU-delta alone false-positives and kills live runs; its transcript, however, was just written. Gating on "transcript untouched >= N min AND process age >= M min", protecting the foreground session (walk the parent chain from your own PID) and the app root + its WindowsApps Electron helpers, and killing the worker's full descendant subtree bottom-up, has run cleanly for me.

Showing cached comments. Read the full discussion on GitHub ↗