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)
- Run Claude.app.
- 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.
- Force-quit Claude.app, or let an auto-update trigger mid-task, or crash the renderer.
- Check
ps -ax | grep claude. The sub-task's claude CLI will be orphaned with PPID=1 and pegging a core.
Suggested fixes
- 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.
- 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.
- 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.
- 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.
- Redact
CLAUDE_CODE_OAUTH_TOKEN from the child's process.env after startup (defence in depth).
Related issues (pattern, not duplicates)
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}')
Environment
chrome_crashpad_handler --annotation=_version=1.3109.0is still running alongside the current 1.3883 main)CLAUDE_AGENT_SDK_VERSION=0.2.111)CLAUDE_CODE_ENTRYPOINT=claude-desktopCLAUDE_INTERNAL_FC_OVERRIDES={"tengu_ccr_bridge":true}What happened
A chaptered / spawned sub-task — identifiable on the command line by
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
claudeCLI 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_chapterfollowed by aToolSearchtool call that never received a response.Why it survives
lsofshows-> (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.count=4pending events). The CPU burn is pure JS on the main thread, no I/O.SIGTERMwhile spinning in this post-disconnect loop (it never returns to the event loop to dispatch the signal handler).SIGKILLis required to terminate it.CLAUDE_CODE_OAUTH_TOKENin its environment (visible viaps -E) — a secondary concern for users who don't notice the leak.Detection one-liner
Any match with etime more than a few minutes is a leak.
Repro (probabilistic — requires the parent to die mid-subtask)
mcp__ccd_session__spawn_task) that issues at least one tool call and is likely to take >30 s.ps -ax | grep claude. The sub-task'sclaudeCLI will be orphaned with PPID=1 and pegging a core.Suggested fixes
SIGHUP/ disconnect handshake from the Mac-app parent before it exits). Onpeer == (none)or heartbeat-lost, self-terminate.SIGTERMis actually observed — e.g. periodicsetImmediate/ cooperative yield in the stream-json consumer, or register the signal handler onprocessbefore entering any synchronous drain loop.--plugin-dirpoints into the samelocal-agent-mode-sessions/<tenant>/<install>/as the currently-running app, or whose cmdline references an olderclaude-code/<version>/bundle, and reap them.SIGTERM(andSIGKILLafter a grace period) to all live CLI children. Thedisclaimerwrapper ancestor is already in the tree — forwarding the signal should suffice.CLAUDE_CODE_OAUTH_TOKENfrom the child'sprocess.envafter startup (defence in depth).Related issues (pattern, not duplicates)
Workaround for affected users
SIGTERMwill not work; useSIGKILL: