[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)

Open 💬 17 comments Opened Jun 1, 2026 by ygbr

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

  1. The config is already minimized to 11 servers; the freeze persists regardless.
  2. 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.
  3. 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.
  4. 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

  1. Configure ~10 MCP servers in Claude Desktop on a 32 GB Apple Silicon Mac.
  2. Open multiple Cowork/worktree/agent sessions across a working day (or let scheduled/autonomous sessions accumulate); don't fully quit the app between them.
  3. Watch the total MCP (node / uv / python) process count and sysctl vm.swapusage climb without bound — e.g. ~11 configured servers → ~300 live processes. Closed sessions' servers persist instead of being killed.
  4. 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 thermlog clean).
  • 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-*.panic files
  • 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 ResetCounter diag 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.

View original on GitHub ↗

17 Comments

abhinas90 · 1 month ago

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

ps aux | grep claude | awk '{print $2, $4, $6}' >> /tmp/claude-mem.log
# Run your workload, then check if RSS grows linearly per task or exponentially over sessions

2. Identify zombie processes (most common cause of phantom memory growth)

ps aux | grep defunct
# If you see [claude] <defunct>, the parent isn't reaping children
# Known issue in some v1.x betas — check your CLI version

3. Session isolation test

for i in $(seq 1 5); do
  claude --print "hello world" && sleep 2
done
ps aux | grep claude
# If memory after 5 is significantly > after 1, it's accumulation

4. Quick mitigation while waiting for a patch
If you're hitting this in automated workflows, wrap sessions with a watchdog:

timeout 300 claude ... || kill -9 $(pgrep -f claude)

I maintain production agent fleets (OpenClaw) and have a hardening checklist for session pooling, resource limits, and monitoring hooks if that would help.

ygbr · 1 month ago

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 claude CLI. The fan-out happens per Cowork/worktree/agent session inside Desktop and isn't reaped on session close — so the claude --print loop (#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 live npm 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 live claude processes — 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.

ygbr · 1 month ago

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-code host and the MCP servers it spawned must be reaped. Today they linger.

(Measurement note: a naive ps | grep mcp also catches ~8 host processes that merely list mcp__… tool names in their --allowedTools argv — those aren't servers. The 52 above excludes them.)

ygbr · 1 month ago

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 --resume CLI session (expected). The other 5 reparented to launchd (ppid=1) — they outlived the Desktop process that spawned them and were adopted by init: @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.)

ygbr · 1 month ago

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:

10:18  mcp=50  cowork=2   ← 2 Cowork sessions active
11:24  mcp=21  cowork=0   ← sessions closed (partial reap)
12:04  mcp=55  cowork=0
13:04  mcp=69  cowork=0
14:29  mcp=83  cowork=0
15:04  mcp=97  cowork=0    ← +14/hr, no active sessions

~+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 --resume terminal 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:

  1. The duplicated fleet is the Cowork/agent-session MCP set, not the configured Desktop extensions. My enabled Desktop extensions are filesystem / notes / chrome-control / iMessage / Office×3 (+ pulumi, mempalace in config). The servers piling up ~10× each are prisma / context7 / playwright / aws-* / terraform — i.e. the set a Cowork/agent run spins up. Each finished run leaves its full set behind; ~10 runs → ~10 copies of each.
  1. They are children of the live Desktop process, not launchd orphans — so no lifecycle event reaps them while Desktop runs. Confirmed via control: a full ⌘-Q drops all 80 at once (swap → 0, ~6 GB freed); relaunch is the only thing that reproduces the climb.

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.)

ygbr · 1 month ago

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_timeout reports were written back-to-back at 02:10:37 and 02:11:13. Verbatim reason (device IDs redacted):

OS:     macOS 26.5 (25F71), arm64e, 32 GB, 12 CPUs
Reason: (1 monitored services unresponsive): WindowServer failed to send ping —
        watchdog handling code likely hung 80 seconds since last successful checkin
Command: WindowServer (PID 416, suspended / zombie)

The same userspace_watchdog_timeout signature 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:

  • Claude ecosystem: 7.7 GB RSS across 226 processes
  • 177 live MCP server processes, spawned by 4 un-reaped Cowork hosts each running a full fleet
  • Per-server duplication (should be ~1 each per active session):
chrome-devtools  27     mcp-proxy-for-aws 18     macos-mcp  10
mempalace        24     context7          18     terraform   9
prisma           22     aws-pricing       18     pulumi      4
playwright       18     aws-iac           18

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:

  • Caught before the watchdog window (twice the same night): killing the Desktop tree from a still-alive terminal recovered cleanly — no reboot.
  • Missed the window once: kernel rebooted the box.

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

  • Reap a Cowork/agent session's MCP set when the session ends (today they persist as children of the Desktop process and only die on a full quit).
  • Cap / dedup concurrent MCP fleets instead of spawning a fresh full set per task cycle.
  • Memory backpressure — stop spawning before WindowServer starves into a watchdog reboot.

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).

ithiria894 · 1 month ago

Same issue on Linux (Ubuntu 22.04, 30 GB RAM)

Confirming the identical fan-out on Linux CLI (not Desktop/Cowork — pure claude terminal 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-sephora etc. after subagent-heavy sessions.

This is not macOS-specific — it's Claude Code's MCP lifecycle management.

claude --version: Claude Code 2.1
OS: Ubuntu 22.04.5, Linux 6.8.0-124-generic, 30 GB RAM

ygbr · 1 month ago

Additional finding on #64366: Claude Desktop leaks IOKit RootDomainUserClient power-assertion clients → kernel panics + system-wide auth stalls

Following 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 RootDomainUserClient IOKit 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

  • MacBook Pro 14" (Mac14,5), Apple M2 Max, 32 GB
  • macOS 26.5.1 (25F80), kernel xnu-12377.121.6~2/RELEASE_ARM64_T6020
  • Claude Desktop 1.11187.4
  • Heavy use of Cowork / local-agent-mode with multiple MCP servers (the fan-out described in this issue)

The leak (measured)

RootDomainUserClient is a userspace connection to IOPMrootDomain (power management) — created by power assertions / prevent-sleep / power-state handles (IOPMAssertionCreate, IORegisterForSystemPower, IOServiceOpen(IOPMrootDomain)).

  • Clean baseline right after reboot: ~120 (ioclasscount RootDomainUserClient)
  • With Claude Desktop open it climbs monotonically at ~35/hour. Example trace from one boot (5-min samples, app open the whole time): 149 → 165 → 185 → 200 → 210 → … → 386 → 395 over ~7 hours.
  • By the time the app was quit it had reached 8,208.
  • Note: a full ioreg -l walk hangs once the registry is bloated; use ioclasscount RootDomainUserClient to 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 with sample/spindump: login/coreauthd block in pam_opendirectoryod_record_check_pwpolicyODRecordAuthenticationAllowed, waiting on opendirectoryd — 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)

panic: userspace watchdog timeout: no successful checkins from WindowServer in 120 seconds

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:

panic: busy timeout[0], (60s): multiple entries holding the registry busy,
IOKit termination queue depth 8208: 'RootDomainUserClient' (1,2001), 'RootDomainUserClient' (1,2001), … @IOService.cpp:5986
Panicked task: pid 363: watchdogd

8,208 simultaneous RootDomainUserClient terminations jammed IOKit's single termination workloop for >60 s → watchdogd panicked the machine. Memory-clean (0 swapfiles). After the panic-reboot the count returned to 121 and all symptoms cleared.

A/B confirmation

  • Reboot → count ~120 → login/sudo/Touch ID/terminals all instant.
  • Open Claude.app → count climbs ~35/hr → at ~300+, auth + WindowServer degrade.
  • Quit Claude.app → IOKit busy-timeout panic as the clients flush.
  • The Claude Code CLI does NOT exhibit this (ran for hours leak-free). It is specific to Claude Desktop (Cowork/local-agent-mode + the MCP host).

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

  1. Reboot. Run ioclasscount RootDomainUserClient → ~120.
  2. Open Claude Desktop; use Cowork / let the MCP servers spawn.
  3. Re-run ioclasscount RootDomainUserClient periodically → climbs ~35/hr, monotonic.
  4. Past ~300: sudo, login, screen-unlock, and Touch ID each stall ~25–30 s; WindowServer may watchdog-panic.
  5. Quit Claude Desktop → IOKit registry busy-timeout panic as the leaked clients flush.

Requests

  1. Audit Claude Desktop and the Cowork/local-agent-mode + MCP host for IOKit / IOPM client handles (IOServiceOpen, IOPMAssertionCreate, power-management connections, no-idle-sleep assertions) that aren't released on session / MCP-server / agent-task teardown.
  2. Bound and release these per-session resources (same root as the process fan-out in this issue).
  3. Interim mitigation: a setting to disable Cowork/agent background activity, and/or release idle IOKit clients, so the registry doesn't grow unbounded over a session.

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/spindump captures.

ygbr · 1 month ago

Follow-up: the RootDomainUserClient leak rate scales directly with Cowork (local-agent-mode) activity

Adding 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:

  1. **The rate correlates with MCP-server / session spawning activity, not just the steady-state count. Quiet periods with the same processes alive leak ~3/hr; bursts where the fan-out spawns/respawns servers leak two orders of magnitude faster. This points at IOKit/IOPM client handles (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.
  2. Closing the Cowork session(s) sharply slows the leak even with Desktop still open; the CLI never exhibits it. So the suspect surface is specifically the Cowork/local-agent-mode session lifecycle + MCP host, not the Electron shell.

Secondary system-wide effects of the same churn (observed during the ~80–450/hr phase)

  • syspolicyd pinned at ~60%+ CPU (with trustd active): 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.
  • Swap engaged (~3 GB) after ~47 h of accumulated multi-process footprint, compounding the slowdown.
  • As before: past ~300 leaked clients, login/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_stall diagnostic reports 25 minutes apart (shutdown_stall_*_145313 and *_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:

  1. Run Cowork → auth stalls (login/sudo/Touch ID ~30 s) past ~300 clients
  2. Keep running under GPU load → WindowServer watchdog kernel panic
  3. Quit the app at high count → IOKit registry busy-timeout kernel panic (termination-queue storm)
  4. Shut down at high count → shutdown stall → forced power-off → Recovery Assistant / re-activation on next boot

Updated reproduction emphasis

  1. Reboot → ioclasscount RootDomainUserClient ≈ 120.
  2. Open Claude Desktop but stay idle → count grows only a few per hour.
  3. Start a Cowork session with scheduled/agent tasks that spawn MCP servers → growth jumps one to two orders of magnitude (~80–450/hr while churning).
  4. Close the Cowork session → growth rate collapses again (already-leaked clients remain until reboot).

This makes Cowork/local-agent-mode session+MCP teardown the highest-value place to look for the unreleased IOKit/IOPM handles.

methodalgo · 1 month ago

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, and Playground), 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-mcp
  • shadcn mcp
  • mcp-server-supabase
  • mcp (generic)
  • mcp-server-sequential-thinking
  • codegraph serve --mcp --path <repo>
  • node_repl (Codex CUA helper)
  • SkyComputerUseClient mcp

Each set is ~8–10 processes × ~150–200 MB RSS. All are direct children of the app-server process (PID 35104, /Applications/Codex.app/Contents/Resources/codex app-server). When a thread closes, app-server drops its side of the stdio pipes but never sends SIGTERM to the MCP children. The npm exec wrapper layer also fails to propagate the signal to the actual node subprocess.

Some codegraph processes were fully orphaned (PPID=1), meaning even app-server lost 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

  1. app-server only closes its pipe — does not SIGTERM the process group.
  2. npm exec wrappers buffer the actual server and do not forward parent-death signals.
  3. Some MCP servers (context-mode start.mjs, svelte-mcp) do not exit on stdin EOF.
  4. Codegraph spawned with --path <repo> gets orphaned to PID 1 when the parent session exits.

Suggested fix

When a thread/session closes, app-server should 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 the npm exec wrapper layer.

Environment

  • Codex Desktop (latest, 2026-06-11)
  • macOS 15.5, Apple Silicon
  • Configured MCP servers: ~8 per thread (context-mode, svelte, shadcn, supabase, generic mcp, sequential-thinking, codegraph, computer-use)
maxim-ultra · 1 month ago

Hi anthropics/claude-code maintainers,

I mapped this thread into a small public-evidence triage note in case it helps narrow the failure surface.

  • Public signal: this issue now has macOS + Linux reports, plus recent notes around Cowork/local-agent activity, single-user Codex Desktop reproduction, memory pressure, and power/assertion behavior.
  • Likely failure family: unbounded or insufficiently bounded MCP/tool/session fan-out that can keep spawning or retaining work after the user's visible action has stopped.
  • Narrow reproduction target: record process count, memory growth, active child sessions/tools, and termination behavior before/after a single Cowork or MCP-heavy prompt, then repeat with local-agent/Cowork disabled as the control.
  • Guardrail worth checking: a hard per-session/process ceiling plus deterministic cancellation/cleanup for child MCP/tool workers when the parent session exits, errors, or is manually stopped.
  • Safe stop condition: the fix should be considered incomplete if RSS/process count continues rising after session cancellation, or if power/assertion clients survive after the agent session ends.

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.

ygbr · 1 month ago

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 RootDomainUserClient torn 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=1 throughout:

22:01  load=2.61  free=185MB  mcp=25  iopm=247  desktop=1
22:06  load=4.53  free=632MB  mcp=35  iopm=250  desktop=1
22:11  load=4.82  free=66MB   mcp=51  iopm=256  desktop=1   WARN mcp
22:16  load=3.51  free=342MB  mcp=83  iopm=262  desktop=1   WARN mcp
22:22  load=3.47  free=58MB   mcp=83  iopm=266  desktop=1   WARN mcp
        <-- WindowServer wedges; 120s watchdog; reboot -->
22:27  load=215   mcp=13      iopm=266->127  (reset signature)

The MCP fleet doubled 25 → 83 procs in ~20 minutes and the machine panicked at the top of that burst. Two things follow:

  1. It panicked at 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.
  2. This is the same per-session/per-cycle fan-out from the original report, now caught in the act of triggering a panic rather than just inferred.

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 while desktop=0:

06-10 22:02  mcp=78  iopm=536  desktop=0
06-10 23:12  mcp=14  iopm=612  desktop=0     <-- iopm still climbing, Desktop closed

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

  1. Cap + reap per-session MCP/Cowork fan-out (the core of this issue). Finished agent/task cycles must reap their MCP set instead of holding it for the lifetime of the Desktop pid.
  2. Reap launchd-orphaned servers on quit. Servers reparented to ppid=1 survive ⌘Q and keep running — a quit should tear down the whole tree.
  3. Global concurrent ceiling across all sessions, not per-session — a session-heavy day reached ~300 concurrent servers; a global cap prevents that.
  4. Spawn-rate throttle / backoff so a burst (25 → 83 in ~20 min here; ~450 clients/hr churn bursts measured earlier) can't slam the system into the watchdog-wedge regime.

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 (apsd held 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/sysdiagnose on request.

ygbr · 26 days ago

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:

  • Apple side (FB22947849): if you tell Apple "it's all Claude Desktop," they close it as a third-party-app issue — that's literally what killed a near-identical report (#40249, closed "invalid"). The OS-escalation framing

(which event 4 nails) is the only version that gets an engineer to fix the reactivation lockout. Keep that framing in your submission.

  • Your side, today: since you can't make Apple ship a fix on your timeline, the thing actually in your control is how you run Desktop — long unattended migrations belong in a terminal CLI session (decoupled from Desktop's

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.

ygbr · 26 days ago

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_loop count 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. Each playwright/chrome-devtools server 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:

  1. Killing a server is futile — the session instantly respawns it. I force-killed all 40–55 playwright/chrome-devtools server+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.
  1. Disabling a plugin doesn't affect running sessions. Disabling playwright/chrome-devtools/context7/etc. in ~/.claude/settings.json only takes effect on new sessions — the running cowork sessions keep their full set. And Desktop cowork launches each session with explicit --plugin-dir args, so it's not clear the user-scope enabledPlugins disable even propagates to cowork at all.
  1. Servers are supervised-but-never-reaped within a session's life, and fan-out is per-session with no global cap. The count scales linearly with concurrent sessions × ~15, with zero sharing between sessions that all point at the same backends.

To even partially survive, I had to build a workaround: a single shared mcp-proxy HTTP 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)

  1. Cap and reap per-session MCP fan-out. A session should not spin up ~15 servers (× headless browsers) with no ceiling, and finished/idle sessions must reap their servers — today they're supervised but never released until the session ends.
  2. Global concurrent-server ceiling across all Cowork/agent sessions, with sharing/dedup for identical backends instead of N independent stdio copies per session (the mcp-proxy gateway I had to build proves one shared backend is viable).
  3. Honor plugin enable/disable in Cowork sessions, and provide a way to drop a server from a running session without killing the session (right now killing a server just respawns it).
  4. Throttle spawn rate / add backoff so burst fan-out can't slam the system.
  5. Reduce the always-on render/sleep footprint (per-session Electron load + the NoIdleSleep assertion that forces sustained display-on operation, which is what feeds the WindowServer wedge).

Evidence available on request: per-event WindowServer spindumps/.ips reports, 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).

ygbr · 25 days ago

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:

  • Claude Code CLI, ~10–40 parallel sessions (tmux): that's roughly 150–600 MCP server processesfar more than Desktop ever spawns. Runs stably for days. No watchdog wedge, no lockout.
  • Claude Desktop, ~6 Cowork sessions (~90 MCP servers): crashes within a day — WindowServer userspace-watchdog wedge → recoveryOS forced reactivation (full timeline + spindumps above).

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:

  1. The Electron renderer — Desktop continuously composites a GUI through WindowServer; CLI/tmux sessions render nothing to it. Only a process that drives WindowServer can wedge it.
  2. The held 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 NoIdleSleep assertion 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.

ygbr · 23 days ago

#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).

Uptime   iopm(RootDomainUserClient)   WindowServer %CPU   state
0–9.5h   FLAT ~112                    ~3–5% (resting)     cowork task IDLE — zero degradation
9.7h     131                          ~44%                task goes ACTIVE (continuous render begins)
11.0h    159                          ~40%
11.7h    232                          ~40%
12.5h    312                          ~31%
14.4h    386                          ~44%                ends in a ~1.5h login lockout

What this shows:

  • For the first 9.5 h the cowork task was idle: the leak gauge was dead-flat and WindowServer rested at ~3–5%. The moment the task began rendering continuously (9.7 h), WindowServer pinned to ~40–47% and the leak gauge began a steady, accelerating climb. Same machine, same session — only the render activity changed.
  • It reproduced with ~13 MCP servers, i.e. not a fan-out pile-up. This is consistent with the earlier CLI-vs-Desktop result (600+ MCP servers on the CLI = stable for days; ~90 on Desktop = crash). It is not the server count.

⇒ 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 releasebiomesyncd, diskarbitrationd, and kernel_task waiters 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:

Claude Desktop renderer composites continuously through WindowServer (~40% sustained) → WindowServer/SkyLight degrades over hours → the OS fails three ways (login lockout / watchdog wedge / kernel-mutex shutdown-stall).

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)

  1. Throttle/coalesce the Cowork renderer's WindowServer commits. Does a streaming text / monitor task need ~40% sustained compositor time? The curve says this sustained commit rate is what accumulates the degradation.
  2. Drop the held 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.
  3. Reduce per-frame render work for long-running Cowork/monitor tasks (offscreen throttling, lower commit frequency when output isn't changing).

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

emikysa · 22 days ago

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 to launchd (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 raising kern.tty.ptmx_max to 999, the next incident (~1.7 days) overran memory instead: load ~295, swap 98% of 37.9 GB, 1,802 procs (317 claude, 165 node, 64 server-pdf). Same defect, the wall just moved - worth treating #68492 as the same root cause.

3. Mitigation asymmetry for the fix: chrome-devtools-mcp can be removed via settings.json enabledPlugins, but @modelcontextprotocol/server-pdf is injected by the app's own local-agent-mode plugin dirs (not ~/.claude config), 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.