[BUG] Mac app: orphaned claude-code CLI from ccd_session chaptered sub-task spins at 100% CPU indefinitely after parent exits non-gracefully

Resolved 💬 4 comments Opened Apr 22, 2026 by Wideyedwonderer Closed May 29, 2026

Environment

  • Claude.app (Desktop) 1.3883.0 (incident originated on 1.3109.0 and survived the upgrade — a chrome_crashpad_handler --annotation=_version=1.3109.0 is still running alongside the current 1.3883 main)
  • Bundled claude-code CLI 2.1.111 (CLAUDE_AGENT_SDK_VERSION=0.2.111)
  • macOS 15.5 (Sequoia), Intel MacBook Pro 16,1 (i7-9750H, 16 GB RAM)
  • CLAUDE_CODE_ENTRYPOINT=claude-desktop
  • CLAUDE_INTERNAL_FC_OVERRIDES={"tengu_ccr_bridge":true}

What happened

A chaptered / spawned sub-task — identifiable on the command line by

--allowedTools mcp__computer-use,mcp__ccd_session__spawn_task,mcp__ccd_session__mark_chapter
--disallowedTools AskUserQuestion
--replay-user-messages
--include-partial-messages

was running over the Tengu↔CCR IPC bridge. The Claude.app parent exited non-gracefully (appears to coincide with an auto-update from 1.3109 → 1.3883). The child claude CLI was reparented to launchd (PPID=1) and has been running continuously for 2 d 4 h, pegging a CPU core at ~92–100 %, holding ~316 MB RSS.

The session's JSONL transcript recorded 18 entries over ~35 seconds and then stopped writing; the process has kept running for two more days. Last recorded action was an mcp__ccd_session__mark_chapter followed by a ToolSearch tool call that never received a response.

Why it survives

  • Its stdin/stdout/stderr/IPC channels are unix sockets. The peer side is gone (lsof shows -> (none) on fds 0/1/2/4/5), but Node on the CLI side does not observe peer death on unix sockets the way it does on pipes — no EOF is surfaced to the stream-json reader, so the event loop never gets a stream-end signal.
  • A tool call was in flight at the moment the parent died; the JS runtime keeps the kqueue alive waiting for a reply that will never come. 3 KQUEUEs are still registered (one with count=4 pending events). The CPU burn is pure JS on the main thread, no I/O.
  • The process does not respond to SIGTERM while spinning in this post-disconnect loop (it never returns to the event loop to dispatch the signal handler). SIGKILL is required to terminate it.
  • The orphan also still carries a live CLAUDE_CODE_OAUTH_TOKEN in its environment (visible via ps -E) — a secondary concern for users who don't notice the leak.

Detection one-liner

ps -axo pid,ppid,etime,%cpu,command |
  awk '$2==1 && /claude-code\/[0-9.]+\/claude\.app\/Contents\/MacOS\/claude/'

Any match with etime more than a few minutes is a leak.

Repro (probabilistic — requires the parent to die mid-subtask)

  1. Run Claude.app.
  2. In an agent session, trigger a chaptered sub-task (via mcp__ccd_session__spawn_task) that issues at least one tool call and is likely to take >30 s.
  3. Force-quit Claude.app, or let an auto-update trigger mid-task, or crash the renderer.
  4. Check ps -ax | grep claude. The sub-task's claude CLI will be orphaned with PPID=1 and pegging a core.

Suggested fixes

  1. In the CLI's stream-json input path, install peer-liveness detection (heartbeat or explicit SIGHUP / disconnect handshake from the Mac-app parent before it exits). On peer == (none) or heartbeat-lost, self-terminate.
  2. Break out of the post-disconnect tight loop so SIGTERM is actually observed — e.g. periodic setImmediate / cooperative yield in the stream-json consumer, or register the signal handler on process before entering any synchronous drain loop.
  3. On Claude.app startup, sweep PPID=1 claude-code CLIs whose --plugin-dir points into the same local-agent-mode-sessions/<tenant>/<install>/ as the currently-running app, or whose cmdline references an older claude-code/<version>/ bundle, and reap them.
  4. Before an auto-update replaces the current app instance, broadcast SIGTERM (and SIGKILL after a grace period) to all live CLI children. The disclaimer wrapper ancestor is already in the tree — forwarding the signal should suffice.
  5. Redact CLAUDE_CODE_OAUTH_TOKEN from the child's process.env after startup (defence in depth).

Related issues (pattern, not duplicates)

  • #51264 (channel plugin bun orphans)
  • #51026 (app-server-broker leak on abnormal exit)
  • #1935 (MCP servers orphan on exit)
  • #44507 (CLI survives terminal close, no stdin end handler) — closed as dup; this issue is the Mac-app Tengu-bridge variant where the parent is the Claude.app renderer, not a terminal.

Workaround for affected users

SIGTERM will not work; use SIGKILL:

kill -9 $(ps -axo pid=,ppid=,command= |
          awk '$2==1 && /claude-code\/[0-9.]+\/claude\.app\/Contents\/MacOS\/claude/ {print $1}')

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗