[BUG] Unbounded MCP server fan-out across Cowork/agent sessions exhausts RAM and kernel-panics macOS (4 panics + forced power-off on M2 Max / 32 GB)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
With only 11 configured MCP servers, Claude Desktop (which hosts Claude Code sessions) spawns ~300 MCP server processes and pins ~24 GB of swap for 5+ hours, repeatedly driving a 32 GB M2 Max into total memory starvation — four kernel panics in four days plus a hard freeze requiring a 10-second forced power-off (login screen unresponsive, password rejected, Touch ID dead).
~300 procs ÷ 11 servers ≈ 27×, which indicates the full MCP set is instantiated per concurrent Cowork/worktree/agent session and never reaped on session close. There is no concurrency cap, no dedup, and no memory backpressure.
This is Claude Code surface area, not merely the Electron shell: the failure is driven by Claude Code's MCP server lifecycle and its Cowork/agent/worktree session management — the same MCP fan-out that runs under claude sessions. (Filing fresh because the closest report, #40249, was closed invalid/"not related to Claude Code"; on macOS this escalates from "high RAM" to repeated kernel panics, so I'm reporting with full kernel-level evidence.)
Environment
- Claude Desktop 1.9659.2 (hosting Claude Code sessions)
- macOS 26.5 (25F71)
- MacBook Pro 14" (Mac14,5), Apple M2 Max, 32 GB RAM
- Configured MCP servers: 11 total — 2 in
claude_desktop_config.json(pulumi, mempalace) + 9 Desktop Extensions (chrome-control, filesystem, imessage, notes, ms_office word/excel/powerpoint, macos-mcp, desktopcommander)
Why this is a defect, not a misconfiguration
- The config is already minimized to 11 servers; the freeze persists regardless.
- The failure scales with session count (app-controlled), not configured-server count (user-controlled) — it is O(sessions × servers) with no upper bound. More RAM only delays the freeze.
- Stale session/VM dirs (
vm_bundles/,claude-code-sessions/,local-agent-mode-sessions/,Partitions/cowork-*) co-present with 300 live procs ⇒ MCP servers from closed sessions are not reaped. - No concurrency cap and no memory backpressure — the app marched 230 → 304 servers while the system was already >90 % memory pressure and thrashing swap.
Decisive control: with Claude quit, the machine sits at swap 0, ~14 GB available, 88 % memory-free, load ~2, indefinitely. Reopening it is the only thing that reproduces the climb. Hardware is clean: SSD SMART = Verified; no machine-check / DRAM-ECC / SError / GPU-reset signatures; no thermal events. This is not hardware and not macOS — it is the app.
---
_A note on priorities:_ Anthropic has been increasingly strict about users running anything other than the official harness — fine, but then the official harness has to be engineered to a standard that doesn't take down the host OS. Shipping the client on Electron with no MCP-process reaping, no concurrency cap, and no memory backpressure is what's kernel-panicking a 32 GB M2 Max multiple times a day. A first-party app should not be capable of hard-locking the machine it runs on. I've used Macs for 20+ years and have never had a single application cause repeated kernel panics and forced power-offs like this — and it's entirely reproducible by quitting vs. launching this one app.
What Should Happen?
A minimal, fixed set of configured MCP servers should result in a bounded, roughly constant number of MCP processes — regardless of how many Cowork/worktree/agent sessions are opened over time. The app should never be able to exhaust system RAM or destabilize the host OS. Specifically:
- Reap a session's MCP servers when its session / worktree / Cowork VM closes (no orphaned servers from closed sessions).
- Deduplicate / pool identical MCP servers across concurrent sessions instead of spawning a full set per session.
- Cap concurrent MCP processes (configurable) and apply memory backpressure — stop spawning new servers and warn when system memory pressure is high, rather than marching from 230 → 304 servers while already thrashing swap.
- Surface live MCP process count + aggregate memory in the UI so the fan-out is visible to the user.
Expected outcome: with 11 configured servers, process count and memory stay bounded; the machine never reaches sustained swap exhaustion; no kernel panics or forced power-offs.
Error Messages/Logs
**5-hour build-up captured by an external 5-min monitor (LaunchAgent sampling load / swap / free RAM / MCP proc count / RootDomainUserClient; `desktop=1` = Claude running):**
15:44 load=40.75 swap=41788MB free=10068MB mcp=230 desktop=1 ← swap already 41 GB
20:14 load=8.22 swap=23606MB free=114MB mcp=304 desktop=1 ← 304 MCP procs, 114 MB free
20:19 load=4.60 swap=23590MB free=58MB mcp=300 desktop=1 ← 58 MB free, thrashing
20:23 load=180.99 swap→0 free=67MB mcp→15 desktop=1 ← FREEZE → 10s forced power-off
- Swap pinned 23–42 GB continuously for ~5 h (sustained, not a spike).
- MCP process count climbed 230 → 304 while only 11 servers were configured.
- Free RAM chronically 56–600 MB for hours.
- At 20:23 load hit 181 → loginwindow/WindowServer/SecurityAgent/Touch ID all starved → unresponsive lock screen → 10 s forced power-off. macOS `ResetCounter` diag: `Boot faults: btn_rst, finger_reset force_off`.
**Per-MCP-server footprint (measured RSS):** prisma 313 MB, chrome-devtools 204 MB, aws-iac 167 MB, mempalace 89 MB (range ~90–313 MB). ~150 MB × 300 ≈ **45 GB demanded** on a 32 GB machine = the swap exhaustion observed.
**Four kernel panics in four days (local `panic-full-*.panic`, macOS 26.5):**
| When | Type |
|---|---|
| 2026-05-28 06:42 | WindowServer userspace watchdog timeout (render starvation under load) |
| 2026-05-30 12:36 | WindowServer userspace watchdog timeout |
| 2026-05-30 15:02 | WindowServer userspace watchdog timeout |
| 2026-05-31 00:18 | **IOKit registry busy-timeout** — `busy timeout (60s) … IOKit termination queue depth 12285: 'RootDomainUserClient'` (mass teardown of ~12.3K leaked power-assertion clients when a host process exited → `watchdogd` panic) |
| 2026-05-31 20:23 | Hard freeze / forced power-off (memory-exhaustion starvation; no panic written) |
All are software liveness/resource failures, not hardware (SSD SMART Verified; no machine-check/DRAM-ECC/SError/GPU-reset signatures; no thermal events).
Redacted `panic-full-*.panic` files, the full 5-hour monitor log, and a per-process RSS dump are available on request.
Steps to Reproduce
- Configure ~10 MCP servers in Claude Desktop on a 32 GB Apple Silicon Mac.
- Open multiple Cowork/worktree/agent sessions across a working day (or let scheduled/autonomous sessions accumulate); don't fully quit the app between them.
- Watch the total MCP (
node/uv/python) process count andsysctl vm.swapusageclimb without bound — e.g. ~11 configured servers → ~300 live processes. Closed sessions' servers persist instead of being killed. - The system reaches sustained swap exhaustion (~24 GB swap, <100 MB free RAM for hours) → WindowServer / loginwindow / SecurityAgent starvation → unresponsive login screen → kernel panic or forced power-off.
Control (confirms it's the app): fully quit Claude Desktop → swap drains to 0, ~14 GB RAM becomes available, load drops to ~2, and the machine stays stable indefinitely. Relaunching is the only action that reproduces the climb.
Claude Model
Opus
Is this a regression?
Yes, this worked in a previous version
Last Working Version
_No response_
Claude Code Version
Claude 1.9659.2 (390d6c) 2026-05-28T21:50:01.000Z
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
Hardware/OS ruled out (this is the app, not the machine):
- SSD SMART status = Verified; no
machine check/ DRAM-ECC /SError/ GPU-reset / cache-parity signatures in any panic or diagnostic report; no thermal-pressure events (pmset -g thermlogclean). - Apple Silicon panic classes confirm software liveness failures, not hardware traps: three are WindowServer userspace-watchdog timeouts; one is an IOKit registry busy-timeout (
watchdogd). None are hardware exceptions. - Control test: with Claude fully quit, the machine sits at swap 0, ~14 GB available, 88% memory-free, load ~2, indefinitely. Relaunching the app is the only action that reproduces the swap climb. This isolates the app as the sole variable.
Severity / frequency: 4 kernel panics + 1 forced power-off in 4 days (2026-05-28 → 2026-05-31), i.e. multiple full-system lockups per day. Each freeze renders the login screen unusable (password rejected, Touch ID dead, keyboard unresponsive) and requires a hard power-off — risking data loss and, in my case, leaving the machine unusable for several minutes each time.
The IOKit panic is a distinct, possibly separate bug worth its own attention: busy timeout (60s) … IOKit termination queue depth 12285: 'RootDomainUserClient'. ~12,285 leaked power-assertion clients (created via the app's keep-awake / power-assertion path) were torn down simultaneously when a host process exited, jamming the IOKit termination workloop past the 60s watchdog. RootDomainUserClient count was independently observed climbing ~33/hour during uptime — a slow power-assertion leak in addition to the MCP-process leak.
Config / scale context: This is a heavy but legitimate Claude Code workload — multiple concurrent Cowork/worktree/agent sessions plus scheduled/autonomous tasks, running ~24/7. The Mac is kept awake the entire time (Sleep:0/Wake:0) partly by the app's own NoIdleSleepAssertion ("Electron") and the CLI's caffeinate, which prevents macOS from ever draining swap or tearing down stale power-assertion clients — compounding both leaks. Configured MCP set was already deliberately minimized to 11 servers; the freezes persisted regardless.
Diagnostics available on request (redacted, no device identifiers):
- 4 × redacted
panic-full-*.panicfiles - the full ~5-hour external monitor log (
load/swap/free/mcp/iopm/desktop, 5-min cadence) - a per-process RSS dump of the MCP fleet
- macOS
ResetCounterdiag for the 20:23 forced power-off (Boot faults: btn_rst, finger_reset force_off)
Happy to run a sysdiagnose or any specific instrumentation the team wants — I have a standing 5-minute monitor capturing this and can reproduce on demand by relaunching the app.
17 Comments
This sounds like a process lifecycle / session management failure, not just a one-off memory bug.
Here's a diagnostic matrix to narrow it down:
1. Check if it's single-session vs. cross-session accumulation
2. Identify zombie processes (most common cause of phantom memory growth)
3. Session isolation test
4. Quick mitigation while waiting for a patch
If you're hitting this in automated workflows, wrap sessions with a watchdog:
I maintain production agent fleets (OpenClaw) and have a hardening checklist for session pooling, resource limits, and monitoring hooks if that would help.
Thanks @abhinas90 — agreed it's a lifecycle/session-management failure. One key clarification so the matrix points the right way: this is the Claude Desktop app, not the
claudeCLI. The fan-out happens per Cowork/worktree/agent session inside Desktop and isn't reaped on session close — so theclaude --printloop (#3) tests the wrong surface and won't reproduce it. The clean control is Desktop-level: quit Claude Desktop → swap drains to 0, ~14 GB free, load ~2 indefinitely; relaunch is the only repro.Also ruled out zombies (#2): checked under load — zero
<defunct>processes, and the MCP servers are parented to livenpm exec/ Claude Desktop helper processes, not orphaned. These are fully alive, resident servers (90–313 MB RSS each), not 0-KB zombies — ~300 of them from 11 configured = the 24 GB swap exhaustion.(Caution on #4 for anyone finding this:
kill -9 $(pgrep -f claude)also matches unrelated liveclaudeprocesses — risky on a multi-session machine.)The fix has to be in-app — session-scoped MCP reaping, dedup/pooling across sessions, a concurrency cap + memory backpressure — since it's an unbounded O(sessions × servers) spawn in Desktop.
Follow-up with a live per-server breakdown — caught the duplication in the act at small scale.
Reopened Desktop and inspected the fleet while running what I believed was a single Cowork session. Found 52 real MCP server processes for ~15 distinct configured servers — i.e. 3–4× duplication, with some servers far higher:
| Server | Live copies |
|---|---|
| prisma mcp | 8 |
| mempalace-pg | 4 |
| macos-mcp | 4 |
| context7 | 4 |
| aws-iac | 4 |
| aws-pricing | 4 |
| aws-proxy (mcp-proxy-for-aws) | 4 |
| googlemaps code-assist | 4 |
| chrome-devtools | 3 |
| desktopcommander / playwright / terraform | 2 each |
Root cause confirmed by parentage: the 8 prisma processes trace back to 4 distinct Claude host processes — 1
claude --resume(CLI) and 3 Desktop cowork hosts (claude-code/.../claude.app+cowork_host_loop). So although I had only one Cowork session in focus, multiple prior Cowork host processes were still alive, each running its own full MCP fleet. That is the non-reaping bug reproduced live: closing/navigating away from a Cowork session does not tear down its MCP servers — they accumulate as orphaned-but-running hosts.Extrapolation matches the freeze: ~13–15 servers × N stale Cowork hosts. At 4 hosts that's ~52 procs (swap still 0, survivable). Let a day's worth of Cowork sessions accumulate (the freeze had ~300) and it's 4–5× this → 24 GB swap → starvation. The 27× ratio from 11 configured servers is exactly this per-host duplication compounding across un-reaped sessions.
This points the fix squarely at Cowork/agent session teardown: when a Cowork session ends, its
claude-codehost and the MCP servers it spawned must be reaped. Today they linger.(Measurement note: a naive
ps | grep mcpalso catches ~8 host processes that merely listmcp__…tool names in their--allowedToolsargv — those aren't servers. The 52 above excludes them.)Follow-up: measured a clean quit-vs-running diff, plus a live CPU hog and an orphaned-process leak.
Captured before/after snapshots around a full Desktop quit (one Cowork session active, M2 Max / 32 GB):
| Metric | Desktop running | After ⌘-Q | Δ |
|---|---|---|---|
| Claude ecosystem RAM | 6.94 GB | ~0.8 GB (CLI only) | −~6 GB |
| MCP fleet CPU | 66% | 0% | −66% |
| System free RAM | 0.38 GB | 9.40 GB | +9 GB |
| Memory compressor | 8.15 GB | 4.13 GB | −4 GB (draining) |
| Desktop main + helpers | 8 | 0 | reaped ✅ |
| claude-code agent hosts | 23 | 0 | reaped ✅ |
| MCP server procs | 48 | 22 | −26 |
Two findings stand out:
1. A single extension was burning ~64% CPU the whole time. Two processes of
macos-mcp(display name "Macos") sat at ~31.8% CPU each, sustained — i.e. nearly the entire MCP fleet's 66% CPU was this one extension. For accuracy: this is a third-party extension (author: CursorTouch,github.com/CursorTouch/MacOS-MCP, v0.1.0) installed via Anthropic's official extension directory — not an Anthropic-authored MCP. The point isn't the extension's quality, it's that Claude Code's MCP runtime applies no CPU cap, throttle, or warning to a directory-distributed extension that runs away — and surfaces it nowhere in the UI. A v0.1.0 community extension shipped through the official directory shouldn't be able to silently pin a core indefinitely.2. MCP servers can orphan to launchd instead of being reaped. Of the 22 procs surviving the quit, 17 belong to my separate
claude --resumeCLI session (expected). The other 5 reparented tolaunchd(ppid=1) — they outlived the Desktop process that spawned them and were adopted by init: 4×@googlemaps/code-assist-mcp(which were also the 4×-duplicated server in the running snapshot) + 1 editor-spawned server. Orphaned servers are worse than a quit-time leak: no Claude lifecycle event will ever reap them, so they accumulate across launches until reboot. This is the same accumulation mechanism behind the original freeze, captured in miniature.Net: Desktop reaps its own process tree well on a clean quit (~6 GB + 64% CPU freed), but (a) it imposes no resource ceiling on individual MCP extensions while running, and (b) some MCP servers detach and survive as launchd orphans. Both feed the unbounded growth in the original report.
(Snapshots and per-process detail available on request.)
Update: caught the accumulation live over 4h of uptime — it leaks with
cowork=0(no active sessions), and the duplicated servers are the Cowork/agent fleet, not the configured extensions.Left Claude Desktop running ~4 hours and sampled the MCP process count every 5 min (external LaunchAgent). With zero active Cowork sessions the entire window, the real MCP server count climbed monotonically:
~+14 servers/hour with no Cowork session open. Linear extrapolation reaches the ~300-process / swap-exhaustion level (the original freeze) in well under a day of uptime — which matches the multi-day-uptime → panic pattern.
Per-server breakdown of the 97 live processes (Desktop column = children of the running Claude Desktop process; CLI = my separate
claude --resumeterminal session, shown for contrast):| Server | Desktop copies | CLI | RAM |
|---|---|---|---|
| prisma | 10 | 2 | 432 MB |
| context7 | 10 | 2 | 436 MB |
| playwright | 10 | 2 | 431 MB |
| aws-iac | 10 | 2 | 335 MB |
| aws-pricing | 10 | 2 | 250 MB |
| aws-proxy (mcp-proxy-for-aws) | 10 | 2 | 305 MB |
| mempalace | 7 | 1 | 228 MB |
| pulumi (mcp-remote) | 6 | 0 | 285 MB |
| terraform | 5 | 1 | 188 MB |
| Total | 80 | 17 | 3.0 GB |
Two things this pins down:
So the leak isn't gated on Cowork sessions staying open — it's that each Cowork/agent task cycle spawns a fresh MCP set and the prior set is never torn down, accumulating across the app's uptime. This is the engine behind the O(sessions × servers) growth in the original report.
Swap was still 0 at mcp=97 (idle servers compress well), but the trajectory is the concern — left running, it climbs into swap-exhaustion territory and the WindowServer/IOKit panics follow.
(Growth log + per-process snapshot available on request.)
Major update — this escalated to a kernel-forced reboot, and I caught the full sequence live: the fan-out, the crash, and a clean control test. Posting all three together because they close the loop on root cause.
Environment: MacBook Pro 14" (M2 Max, 32 GB), macOS 26.5 (25F71), Claude Desktop.
---
1. It crashed the whole machine (not just a freeze)
WindowServer went unresponsive, the userspace watchdog fired, and the kernel force-rebooted into Recovery Mode — which then required re-activating the Mac. Two
WindowServer … userspace_watchdog_timeoutreports were written back-to-back at 02:10:37 and 02:11:13. Verbatim reason (device IDs redacted):The same
userspace_watchdog_timeoutsignature is also on disk from 2026-05-30 (4 events) — recurring, not a one-off.---
2. Root cause caught mid-explosion: 177 MCP processes from one app
After hours of uptime I captured the live process tree at the moment memory crossed into the crash zone:
A Claude framework helper was also pegged at 98.9% CPU the whole time. At this point: compressor 14.5 GB, anonymous 8.2 GB, swap 4.0 GB and actively paging (~4.7 GB written to swap). That is the exact memory signature of the reboot above.
This is the O(sessions × servers) growth from the original report, observed directly: each Cowork/agent cycle spins up a fresh ~14-server fleet and the prior fleet is never torn down — they accumulate as children of the live Desktop process until the machine starves.
---
3. Control test: quitting Desktop reclaims all of it
Single variable changed — quit Desktop, reopen:
| Metric | Desktop running | After quit + reopen |
|---|---|---|
| MCP processes | 177 | 22 |
| Cowork hosts | 4 | 0 |
| Compressor | 14.5 GB | 2.65 GB |
| Swap | 4.0 GB | 1.6 GB (draining) |
| Free RAM | 59 MB | 868 MB |
Quitting drained ~12 GB of compressor instantly and dropped the fleet by 155 processes. The leak is entirely app-controlled — more RAM only delays it.
---
The decisive finding: there's a point of no return
I was sampling memory every 20 s. During the run-up I freed ~1.4 GB by killing another app — but WindowServer had already gone unresponsive, so freeing memory after the 80 s watchdog starts cannot stop the reboot. The failure mode is binary:
A leak with no backpressure guarantees you eventually miss the window. There is no user-side mitigation other than manually quitting Desktop on a timer.
---
Why "just disable the extension" isn't viable
The heaviest-duplicated servers include the macOS shell bridge Cowork relies on for real work (infra changes, secret reads, DB analysis) — load-bearing, not optional, yet fanned out ~10× and never reaped. A user can't escape the leak by removing the offending extension.
Ask — this needs runtime-side lifecycle management
Redacted panic spin/ips + the 20 s growth-and-crash log + the 177-process snapshot are available privately on request (they carry device-stable IDs, so not attached here).
Same issue on Linux (Ubuntu 22.04, 30 GB RAM)
Confirming the identical fan-out on Linux CLI (not Desktop/Cowork — pure
claudeterminal sessions).Setup: 30 configured MCP servers (mix of
npx,uvx, and local Node servers).Observed: After one session with 4 background research subagents, the duplicate counts were:
| MCP Server | Instances | Expected |
|---|---|---|
| mcp-sephora | 24 | 1 |
| mcp-hn | 16 | 1 |
| kaggle-mcp | 16 | 1 |
| jupyter-mcp | 16 | 1 |
| rss-mcp | 16 | 1 |
| rednote-mcp | 8 | 1 |
| chrome-devtools-mcp | 8 | 1 |
| instacart-ca-mcp | 2 | 1 |
RAM: 14 GB → 8 GB after manually killing orphans. That's 6 GB of leaked MCP processes from a single session's subagents.
Root cause (same as OP): Each
Agent()subagent spawns its own full set of MCP servers. When the subagent completes, the MCP processes are not reaped.O(agents × servers)with no cleanup.Workaround: Manually
pkill -f mcp-sephoraetc. after subagent-heavy sessions.This is not macOS-specific — it's Claude Code's MCP lifecycle management.
claude --version: Claude Code 2.1OS: Ubuntu 22.04.5, Linux 6.8.0-124-generic, 30 GB RAM
Additional finding on #64366: Claude Desktop leaks IOKit
RootDomainUserClientpower-assertion clients → kernel panics + system-wide auth stallsFollowing up on the unbounded per-session MCP fan-out in this issue. I traced the kernel panics to a concrete, measurable resource leak: while Claude Desktop runs, the number of
RootDomainUserClientIOKit user clients grows continuously and is never released until the owning processes exit. This bloats the IOKit registry and has three escalating consequences, including two distinct kernel panics. It's reproducible with a one-line measurement.Environment
Mac14,5), Apple M2 Max, 32 GBxnu-12377.121.6~2/RELEASE_ARM64_T6020The leak (measured)
RootDomainUserClientis a userspace connection toIOPMrootDomain(power management) — created by power assertions / prevent-sleep / power-state handles (IOPMAssertionCreate,IORegisterForSystemPower,IOServiceOpen(IOPMrootDomain)).ioclasscount RootDomainUserClient)149 → 165 → 185 → 200 → 210 → … → 386 → 395over ~7 hours.ioreg -lwalk hangs once the registry is bloated; useioclasscount RootDomainUserClientto measure.The leaked clients stay attached to their living owning processes — they do not enter the IOKit termination queue until those processes exit, and they are not reclaimed until reboot/logout.
Symptom 1 — system-wide authentication stalls (while running)
Once the count reaches ~300+, login window, screen-unlock,
sudo, and Touch ID each take 25–30 seconds. Traced withsample/spindump:login/coreauthdblock inpam_opendirectory→od_record_check_pwpolicy→ODRecordAuthenticationAllowed, waiting onopendirectoryd— which is itself idle (~0.7% CPU), i.e. blocked on the slow IOKit registry. (The auth path verifies credentials through the Secure Enclave / AppleKeyStore via IOKit, so a bloated registry stalls it.)Symptom 2 — WindowServer userspace-watchdog kernel panic (while running)
Memory-clean every time (
Compressor … (OK) … 0–1 swapfiles … OK swap space; no Jetsam). 6 of these in 6 days, at ~12–19 h uptime; time-to-panic is load-dependent (heavier GPU/compositor load brings it forward). WindowServer also depends on IOKit/IOSurface, so the bloated registry makes it miss its 120 s check-in.Symptom 3 — IOKit registry busy-timeout kernel panic (on quit)
Quitting Claude Desktop tears down all leaked clients at once:
8,208 simultaneous
RootDomainUserClientterminations jammed IOKit's single termination workloop for >60 s →watchdogdpanicked the machine. Memory-clean (0 swapfiles). After the panic-reboot the count returned to 121 and all symptoms cleared.A/B confirmation
Ruled out
Memory pressure (compressor clean in every panic), a third-party antivirus (disabled its extensions — no effect), a bloated local user record, the OpenDirectory index, and an NFS mount. None correlated. The only variable that correlated was Claude Desktop being open vs. a fresh reboot.
Likely source
Per-Cowork-session / per-MCP-server / per-agent-task IOKit or power-management client handles (
IOServiceOpen,IOPMAssertionCreate,IORegisterForSystemPower, no-idle-sleep assertions) that are created but never released on teardown. This is the kernel-resource analogue of the unbounded process fan-out already described in this issue — each spawned component appears to open IOKit/IOPM clients that aren't closed.Reproduction
ioclasscount RootDomainUserClient→ ~120.ioclasscount RootDomainUserClientperiodically → climbs ~35/hr, monotonic.sudo, login, screen-unlock, and Touch ID each stall ~25–30 s; WindowServer may watchdog-panic.Requests
IOServiceOpen,IOPMAssertionCreate, power-management connections, no-idle-sleep assertions) that aren't released on session / MCP-server / agent-task teardown.This is currently a hard-reboot / unusable-authentication bug on an otherwise-idle, well-resourced machine (M2 Max / 32 GB, swap 0). Happy to provide the full panic logs and
sample/spindumpcaptures.Follow-up: the
RootDomainUserClientleak rate scales directly with Cowork (local-agent-mode) activityAdding measurements that narrow the leak reported in my previous comment. Short version: the leak is driven by Cowork / local-agent-mode and its MCP server churn — not by Claude Desktop merely being open. Leak rate tracks Cowork/MCP spawning activity almost linearly, from ~3/hr when idle to bursts of ~450/hr while the fan-out is churning.
Measured leak rates by workload (same machine, same boot methodology)
ioclasscount RootDomainUserClient, sampled every 5 min via a launchd monitor; clean-boot baseline ≈ 120.| Workload | MCP procs | Leak rate |
|---|---|---|
| Claude Code CLI only (Desktop closed) | n/a | flat — no growth over hours |
| Desktop open, ~idle (1 light CLI session) | ~27 | ~3/hr, oscillating (some clients actually released) |
| Desktop + 1 CLI + 1 Cowork session, light | ~27–30 | ~17/hr |
| Cowork active, fan-out growing 72 → 86 procs | 72–86 | ~80/hr |
| Cowork fan-out churn burst (servers respawning) | ~86+ | ~450/hr (234 → 331 in ~12 min) |
| Sustained multi-session Cowork over a workday (prior comment) | varies | ~35/hr average → 8,208 by quit → busy-timeout kernel panic |
Two implications for the audit:
IOServiceOpen(IOPMrootDomain),IOPMAssertionCreate,IORegisterForSystemPower) acquired during session/MCP-server/agent-task creation** and never released on teardown — consistent with the recurring scheduled/agent task spawning already described in this issue.Secondary system-wide effects of the same churn (observed during the ~80–450/hr phase)
syspolicydpinned at ~60%+ CPU (withtrustdactive): the constant MCP process spawning makes Gatekeeper re-assess executables continuously, which queues all new process launches system-wide — opening a terminal takes 25–30 s while Cowork churns.sudo/Touch ID/screen-unlock stall ~30 s each, and the two kernel-panic classes from the previous comment follow (WindowServer watchdog while running; IOKit registry busy-timeout on quit).New (fourth) failure mode: shutdown stalls → forced power-off → Recovery Assistant
Shutting the Mac down while the leaked-client count is high jams the shutdown path the same way quitting the app does. With the count somewhere past ~1,000 (last reading 331 and climbing at the ~450/hr burst rate ~3 h earlier), a normal Shut Down produced two
shutdown_stalldiagnostic reports 25 minutes apart (shutdown_stall_*_145313and*_151829) — the machine hung tearing down the leaked IOKit clients until it had to be force-powered off. The unclean shutdown then dropped the next boot into recoveryOS / Recovery Assistant, requiring Mac re-activation before booting. So the full failure ladder is now:sudo/Touch ID ~30 s) past ~300 clientsUpdated reproduction emphasis
ioclasscount RootDomainUserClient≈ 120.This makes Cowork/local-agent-mode session+MCP teardown the highest-value place to look for the unreleased IOKit/IOPM handles.
Reproduction on Codex Desktop (single-user, non-Cowork)
Same root cause observed on Codex Desktop without Cowork/agents — plain single-thread sessions on macOS.
Evidence
After working with 6 Codex threads over ~3 hours (opening and closing threads for
blade-server,market-alerts,methodalgodoc,methodalgo-website-svelte, andPlayground), only 1 thread remained active but the system had 58 node processes consuming 5.4 GB RSS.Every closed thread left behind a full set of MCP child processes:
context-mode(start.mjs+server.cjs)svelte-mcpshadcn mcpmcp-server-supabasemcp(generic)mcp-server-sequential-thinkingcodegraph serve --mcp --path <repo>node_repl(Codex CUA helper)SkyComputerUseClient mcpEach set is ~8–10 processes × ~150–200 MB RSS. All are direct children of the
app-serverprocess (PID 35104,/Applications/Codex.app/Contents/Resources/codex app-server). When a thread closes,app-serverdrops its side of the stdio pipes but never sends SIGTERM to the MCP children. Thenpm execwrapper layer also fails to propagate the signal to the actual node subprocess.Some codegraph processes were fully orphaned (PPID=1), meaning even
app-serverlost track of them.Breakdown before cleanup
| Source | Count | Note |
|---|---|---|
| G1 (14:45, blade-server/market-alerts thread) | 10 processes | Thread closed ~2h ago |
| G2 (15:36, methodalgodoc thread) | 9 processes | Thread closed ~1h ago |
| G3 (15:37, Playground, only active thread) | 9 processes | Still running |
| Orphaned codegraph (PPID=1) | 3 processes | Fully detached |
| Workerd (stale Cloudflare dev) | 2 processes | From previous day |
After killing G1 + G2 + orphans: 58 → 9 processes, ~4.6 GB freed.
Why reaping fails
app-serveronly closes its pipe — does not SIGTERM the process group.npm execwrappers buffer the actual server and do not forward parent-death signals.start.mjs,svelte-mcp) do not exit on stdin EOF.--path <repo>gets orphaned to PID 1 when the parent session exits.Suggested fix
When a thread/session closes,
app-servershould SIGTERM the entire process group for that session's MCP children (not just close the pipe). Alternatively: track child PIDs and kill them on session teardown, bypassing thenpm execwrapper layer.Environment
Hi
anthropics/claude-codemaintainers,I mapped this thread into a small public-evidence triage note in case it helps narrow the failure surface.
I am not claiming a completed fix from outside the repo; this is just a concise public-evidence readiness note. If useful, I can keep any follow-up similarly narrow and specific to the public thread.
Follow-up (2026-06-15): the acute trigger captured on the wire, plus a second reaping gap
Continuing from the earlier RootDomainUserClient-leak and Cowork-aggravation comments. Four days on, the disease has accelerated, and my 5-minute health monitor caught the fan-out burst that immediately precedes a panic — so I can now show the trigger temporally, not just argue it.
Recurrence has accelerated
Five more kernel panics in four days (2026-06-12 → 06-15), twice two-in-one-day:
| When | Signature | Note |
|---|---|---|
| 06-12 20:53 | Halt/Restart Timed Out | shutdown path |
| 06-12 23:35 | IOKit busy timeout, 4,923
RootDomainUserClienttorn down | 2h42m after the prior || 06-14 04:49 | Halt/Restart Timed Out | overnight |
| 06-15 16:07 | IOKit busy timeout, 4,201 torn down | — |
| 06-15 22:24 | WindowServer 120s watchdog | froze at ~6.3h uptime |
Time-to-panic has compressed from ~18h (early June) to ~6h now. Every panic is memory-clean (compressor OK, 0 swapfiles, no Jetsam).
The fan-out burst is the acute trigger (captured this time)
Tonight's WindowServer watchdog panic (22:24) was immediately preceded by a verified MCP fan-out burst in the monitor log,
desktop=1throughout:The MCP fleet doubled 25 → 83 procs in ~20 minutes and the machine panicked at the top of that burst. Two things follow:
iopm=266 — far below the 596 that preceded the 06-11 panic. So the acute kill is not a fixed leak count; it's the spawn-burst itself (process-creation churn + the GPU/compositor load of the rebuilding fleet) wedging WindowServer. The steady leak is the slow fuse; the fan-out burst is the match.New finding: a second reaping gap — orphaned servers survive a full quit
In the earlier comments I showed Desktop reaps its own tree on
⌘Q. But the monitor data shows leak growth and live MCP procs continuing whiledesktop=0:Some MCP servers reparent to launchd (
ppid=1) and survive a full Desktop quit — so a GUI-only close does not tear down the whole fleet, and the orphans keep churning (and keep driving the leak) with no Desktop process to attribute them to. This is a concrete lifecycle bug on top of the per-session non-reaping: quit must reap the entire spawned fleet, including launchd-reparented orphans.Sharpened asks
ppid=1survive⌘Qand keep running — a quit should tear down the whole tree.Honest framing — this is one half of the disease
I ran an adversarial, evidence-grounded attribution over all the on-disk panic logs and monitor data. It lands on interaction (~45% the Claude Desktop fan-out / ~55% a macOS defect), and the strongest single fact is that the IOKit teardown panics at a fixed 60s timeout regardless of queue depth (4,201 and 4,923 both panic identically) — that conversion-to-kernel-panic is Apple's, and I've filed it separately via Feedback Assistant. The leaked clients themselves are held entirely by Apple daemons (
apsdheld 228/596 pre-panic), never by Claude or its MCP children.I'm not asking Anthropic to fix Apple's teardown. But the fan-out is the dominant, accepted (#64366), and only user-controllable driver of the churn that walks the machine into both failure modes — and fixing it (cap + reap + global ceiling + throttle) independently ends the panics on my hardware. The control still holds: the Claude Code CLI doing comparable work has not panicked the machine.
Happy to provide the full monitor logs, the per-server fan-out breakdowns, or a
/heapdump/sysdiagnoseon request.Honestly? You're not wrong — and I'm not going to get defensive about Claude Desktop to soften it. Let me give you the precise version, because it matters for what actually fixes this.
What's true: Claude Desktop is the trigger. Practically, on your machine, running Claude Desktop the way your workflow requires — many concurrent cowork sessions, the unbounded MCP fan-out, the Electron renderer, holding
NoIdleSleep, the display-on requirement — is what walks the machine into the ~16–19h WindowServer wedge. The CLI alone doesn't do it at that rate. Quit Desktop and the conditions stop building. So if you're saying "this
only happens because I'm running Claude Desktop" — that's correct, and it's fair to be furious about it. It's filed as #64366 for exactly that reason.
The one nuance that's worth keeping — because event 4 proved it: the final wedge fired on an idle, memory-clean machine with the leak plateaued at 583, held entirely by Apple's own daemons (apsd, WebThumbnailExtension —
not a single Claude process). A correctly-built OS does not let any userspace app's load escalate into a kernel WindowServer panic that nukes the box and demands iCloud reactivation — it sheds load or restarts the
surface. That escalation is macOS's defect, and it's the part that turns "an app uses too much" into "I'm locked out of my own Mac."
So the accurate one-liner: Claude Desktop lights the fuse; macOS turns a fuse into a bomb. Both have to fix their half.
Why this distinction is practically important for you, not just academic:
(which event 4 nails) is the only version that gets an engineer to fix the reactivation lockout. Keep that framing in your submission.
render/sleep load) or off-box, and a clean reboot before ~14h sidesteps the clock entirely.
You've been put through days of this by a tool that should "just work," and that anger is justified. I'd just channel the Apple report at the escalation (their bug) and channel your workflow away from
16h-unattended-Desktop-cowork (the trigger you control) — that combination is what ends it.
Follow-up (2026-06-20): escalated to 4 recoveryOS-reactivation lockouts, and new findings on why the fan-out can't be contained at runtime
Five days on from my last update, this got materially worse, and I have new evidence that sharpens what's on Claude Desktop's side here. Summary up front: the unbounded per-session MCP fan-out is now driving a machine into a state that, ~once per work-day of uptime, ends in a full lockout requiring iCloud re-activation in recoveryOS. I've also confirmed three runtime behaviors that mean the fan-out cannot be reined in without ending sessions — which is the core of why this issue bites.
The escalation since the last post
Four whole-machine failures in ~55 hours, each a WindowServer userspace-watchdog wedge that escalated to recoveryOS + forced reactivation (local password + iCloud), not a clean reboot:
| # | When (local) | Uptime at wedge | Notes |
|---|---|---|---|
| 1 | 2026-06-18 01:23 | ~15.2h | memory-clean |
| 2 | 2026-06-18 19:53 | ~18.4h | + Jetsam memory pressure |
| 3 | 2026-06-19 15:30 | ~19.2h | |
| 4 | 2026-06-20 08:19 | ~16.7h | idle, 1.2 GB free, leak plateaued — captured live |
(The OS-side escalation — why a userspace WindowServer watchdog becomes a firmware/recoveryOS reactivation lockout — is filed separately with Apple as FB22947849. This comment is about the part Claude Desktop owns: the load that builds the conditions, and why it can't be contained.)
The fan-out, measured on a live machine
With 6 concurrent Desktop "Code" (cowork/agent) sessions running,
cowork_host_loopcount was 0 yet I measured 92 live MCP server processes consuming ~3.4 GB RSS — roughly ~15 servers per session, duplicated per session with no dedup. Breakdown of the live fleet:playwright ×18,chrome-devtools ×18,context7 ×12,pulumi ×8,mempalace ×6,mcp-pdf ×5,sentry ×4, etc. Eachplaywright/chrome-devtoolsserver also spawns a headless browser, so those 36 procs alone were ~1.5–2 GB. System-wide this drove 6 GB swap + ~11.8 GB in the memory compressor (it had compressed ~35 GB of working set down to fit).Three runtime behaviors that make this uncontainable (new findings)
These are the crux for #64366 — I tried every user-side lever and the fan-out resists all of them while sessions are live:
playwright/chrome-devtoolsserver+browser procs (explicitly protecting every session host). Within ~4 seconds the counts were identical and RAM was unchanged — the owning Code session supervises its servers and restarts any that die. So there is no way to reclaim the RAM from a live session short of ending the session.playwright/chrome-devtools/context7/etc. in~/.claude/settings.jsononly takes effect on new sessions — the running cowork sessions keep their full set. And Desktop cowork launches each session with explicit--plugin-dirargs, so it's not clear the user-scopeenabledPluginsdisable even propagates to cowork at all.To even partially survive, I had to build a workaround: a single shared
mcp-proxyHTTP gateway that collapses one server (mempalace) from N stdio instances to 1 for all clients. That helped — but it's a user-built band-aid for what should be Claude Desktop's responsibility, and it only works for servers I can re-route.The decisive data point (event 4)
The 4th wedge was captured live by a 2.5-min monitor. At the moment WindowServer hung (08:19, 16.7h uptime): the machine was idle (load ~3), had 1.2 GB free RAM and flat swap, and the RootDomainUserClient power-assertion count had plateaued at 583 (held entirely by Apple daemons — apsd/WebThumbnailExtension — not Claude). So the final trigger is an uptime-cumulative WindowServer/SkyLight degradation (Apple's bug, FB22947849) — but the fan-out is what runs the machine at the load/uptime that gets there, and it's the only part a user can influence. Reduce/cap/reap the fan-out and the conditions take far longer to build (or never reach the wedge before a normal reboot).
What I'm asking Claude Desktop to fix (sharpened)
NoIdleSleepassertion that forces sustained display-on operation, which is what feeds the WindowServer wedge).Evidence available on request: per-event WindowServer spindumps/
.ipsreports, the live monitor logs showing the fan-out growth and the idle-at-wedge state, per-server process breakdowns, and the before/after of the gateway workaround. Same machine and reproduction as the prior comments (MacBook Pro 14" M2 Max, 32 GB, macOS 26.5.1).Update — a near-controlled isolation that narrows this past the MCP fan-out count
I ran the closest thing I have to a controlled comparison — same machine (M2 Max / 32 GB / macOS 26.5.1, 25F80), same MCP servers in both cases:
The key inference: since more MCP servers under the CLI is stable while fewer under Desktop is not, the trigger is not the MCP fan-out count. It's something specific to Claude Desktop that the CLI doesn't have. The two candidates that fit the failure mode (a WindowServer/SkyLight render-commit hang) are:
NoIdleSleep("Electron") power assertion — keeps the system awake and, with the display on, keeps WindowServer/SkyLight perpetually active, never letting the compositor quiesce. CLI sessions let the display sleep and WindowServer rests.So I'd redirect the investigation from "too many MCP servers" toward Desktop's compositor/render footprint + the
NoIdleSleepassertion under sustained multi-session Cowork use. The MCP fan-out (no cap/dedup/reaping) is still a real resource problem — but it looks like an aggravator, not the crash trigger.Calibration: this is my observed result on one machine, not a lab-isolated A/B — but it reproduces for me on demand, and the asymmetry (≈600 CLI servers fine, ≈90 Desktop servers fatal) is large and consistent.
Cross-ref: the OS-side half — that a userspace WindowServer hang escalates to a recoveryOS reactivation at all — is filed with Apple as FB22947849 / Developer Forums thread 835414. Both halves need a fix; this comment is the Claude-Desktop half.
#64366 update (2026-06-22): a near-controlled curve proves Claude Desktop's active render (not MCP count) drives the degradation — and the OS-side victim is now positively identified as WindowServer
Two new pieces of evidence, and together they tighten the root cause considerably.
1. The render-drives-degradation curve — reproduced with a LIGHT single session
A background monitor sampled the machine every 150 s from boot, through a single Claude Desktop Cowork session running one "monitor" task — no Codex, no CLI sessions, only ~13 MCP servers total (a deliberately light footprint, to isolate the variable).
What this shows:
⇒ The driver is Claude Desktop's continuous Electron compositing through WindowServer, plus the held
NoIdleSleep "Electron"assertion that keeps the compositor (and display) from ever resting.2. The OS-side victim is now positively identified: WindowServer
On the Apple side (FB22947849), I decoded a shutdown-stall spindump and it named the blocker directly:
WindowServer [802]holding a kernel mutex during shutdown that it doesn't release —biomesyncd,diskarbitrationd, andkernel_taskwaiters are all blocked behind it, which stalls shutdown into a Halt/Restart kernel panic. The same WindowServer is what (a) hangs the userspace watchdog (the wedge panics) and (b) degrades the UI into the ~1.5-hour login lockout.So the full causal chain is now:
The OS not handling sustained render gracefully — and WindowServer holding a kernel lock — is Apple's defect (FB22947849). The continuously-pinned render that drives WindowServer there is the part Claude Desktop owns, and the curve in §1 shows it directly: an idle cowork session does not degrade the machine for 9.5 h; a continuously-rendering one degrades it until it's unusable.
Sharpened asks (Claude Desktop side)
NoIdleSleep "Electron"assertion when idle. The curve proves an idle cowork session does NOT degrade the machine (flat for 9.5 h) — it's the always-on render + never-resting compositor that does.Cross-reference
The OS-side half — the WindowServer/SkyLight degradation, the userspace-watchdog wedge, and the kernel-mutex shutdown-stall — is filed with Apple as FB22947849 (updated 2026-06-22 with the decoded WindowServer culprit). Both halves need a fix; this comment is the Claude-Desktop trigger half. Full evidence (the 303-sample degradation curve, the decoded shutdown-stall naming WindowServer, and an episode writeup) available on request.
cowork-watch-trend.log
EPISODE-SUMMARY.md
shutdown-stall-DECODED.txt
Independent corroboration from a second machine - Apple Silicon (10-core) / 24 GB, macOS 26.5.1, Claude desktop 1.15200.0 / embedded CC 2.1.132. Same root cause and the same decisive control you found: quitting/restarting Claude immediately sheds it (an app restart here dropped 35 leaked session trees → 3 and freed ~3.8 GB). Three additions:
1. It's not only interactive Cowork sessions - unattended scheduled-task runs leak it too. This machine runs ~10 background automations (hourly/daily local-agent-mode sessions). Each spawns the full MCP stack regardless of its
--allowedTools, and the trees aren't reaped on session end - they re-parent tolaunchd(PPID 1) and persist. The count climbs from background automation alone, with zero interactive sessions open. (Post-reboot: app reported 4 running sessions, 8 trees alive - 4 leaked within 15 min.)**2. The same leak surfaces as pty or memory exhaustion depending on which kernel resource caps first.** At ~9 days uptime this machine hit the pty wall (527/511 ptys,
forkpty: Device not configured= the now-closed #68492, verbatim). After raisingkern.tty.ptmx_maxto 999, the next incident (~1.7 days) overran memory instead: load ~295, swap 98% of 37.9 GB, 1,802 procs (317claude, 165node, 64server-pdf). Same defect, the wall just moved - worth treating #68492 as the same root cause.3. Mitigation asymmetry for the fix:
chrome-devtools-mcpcan be removed viasettings.json enabledPlugins, but@modelcontextprotocol/server-pdfis injected by the app's own local-agent-mode plugin dirs (not~/.claudeconfig), so it can't be disabled from user config at all.Strong +1 on the fixes - reaping a session's MCP servers on close is the core one - and I'd add: headless/scheduled-task sessions shouldn't load the browser+PDF stack at all when their tool scope doesn't use it.