[BUG] Child/nested claude processes orphan on /exit in WSL systemd environments, triggering cross-process GUI freezes via vmmem CPU spikes
Summary
On Linux/WSL with systemd enabled, /exit does not reliably terminate child claude processes spawned from within an interactive session. Orphaned children keep holding MCP child processes (node/python/bun), which continue background polling and accumulate over days. In multi-tab setups, the accumulated background load causes a cross-process impact: WSL vmmem CPU spikes propagate through the Windows DWM compositor, freezing the terminal emulator (WezTerm) that hosts the WSL shells — producing typing stalls, scroll hitches, and full "Not Responding" ghost-window overlays on Windows.
Existing issues (#19433 closed, #20369 stale, #46493, #51264, #43944) touch adjacent symptoms, but all of them are reported on macOS. Linux/WSL has its own reaper topology (processes get reparented to systemd --user or WSLg Relay(<pid>) rather than PID 1), and the GUI-freeze impact path is not yet documented.
This issue requests first-class lifecycle management for nested and hook-spawned claude processes.
Environment
| Item | Value |
| --- | --- |
| OS | Windows 11 + WSL2 Ubuntu 24.04 (systemd enabled) |
| Claude Code | latest as of 2026-04-21 |
| Shell | bash (interactive login), multiple tabs in WezTerm |
| Parallelism | 4 concurrent Claude Code sessions across tabs |
| MCP servers in use | context7, brave-search, freee-mcp, oh-my-claudecode bridge |
Observed Behavior (reproducible)
1. /exit does not terminate nested children
Starting a claude session from within another claude session (for example, running claude --resume <session-id> inside a Bash executed by the parent Claude Code, or accidentally starting it from a shell already inside Claude Code) produces a multi-level process tree. /exit on the outer session returns the user to bash but the inner claude --resume process, and any MCP children it spawned, continue running.
Example observed tree (PIDs redacted):
pts/5 bash
└─ claude --allow-dangerously-skip-permissions (5h+ uptime)
└─ claude --resume <uuid> (3h+ uptime)
└─ claude --resume <uuid> (3h+ uptime, same uuid as parent)
├─ node .../mcp-server.cjs (oh-my-claudecode bridge)
├─ npm exec @upstash/context7-mcp
├─ npm exec freee-mcp
└─ npm exec @modelcontextprotocol/server-brave-search
The user had no intention of running a nested Claude Code; the inner instances are the result of claude being invoked from inside a running session, and outer /exit not propagating termination downward.
2. WSL systemd reparents orphans in a non-PID 1 way
Tools checking for orphans via PPID == 1 miss most real cases on WSL systemd. The reparenting destinations observed on my system:
PID 1: systemd (/sbin/init)
PID 569: Relay(570) (WSLg interop relay)
PID 570: -bash (login shell)
Orphaned processes are typically reparented to systemd --user or Relay(<pid>) rather than PID 1. Any cleanup tooling that only checks PPID=1 silently misses real orphans on WSL.
3. Impact beyond WSL: Windows GUI freeze
With multiple orphans accumulated (observed peak ~900 MB of chroma-mcp Python RSS, plus several node/bun MCP children), background polling creates bursty CPU load on vmmem. The impact is not confined to WSL: WezTerm's GUI thread (Windows side) blocks while waiting on WSL pane I/O, DWM triggers Ghost Window overlay after 5 s, and the user sees "Not Responding" freezes — even though WezTerm itself is healthy.
Symptoms (typical):
- Typing in the active Claude Code tab stalls for 50–500 ms at arbitrary intervals
- Scroll stalls briefly during output bursts
- Tab+Shift (Claude Code mode switch) delays noticeably
- Occasional multi-second "Not Responding" overlay on the WezTerm window
Root cause analysis (our reading)
Three compounding sources, all caused by missing lifecycle management:
- Nested/
--resumespawn has no cleanup contract. A childclaudesurvives parent termination by design of the POSIX process model, but there is no mechanism in Claude Code to opt into "inherit-termination" behavior on nested launches. - Hook-spawned side processes escape reaping. Tools registered via hooks (Stop/SessionStart/etc.) can spawn long-running subprocesses (for example, a tab-title generator that calls a remote LLM). When the Claude Code process exits, the hook-spawned chain keeps running.
- MCP child processes do not clean up when their parent
claudedies abnormally. This amplifies #1 and #2 since each orphanedclaudekeeps its MCP fleet alive.
Workaround currently deployed (user-space, 3 layers)
Because there is no built-in solution, the following workaround was deployed:
Layer A — shell trap EXIT to reap children when a bash shell closes
# ~/.bashrc
trap 'pkill -TERM -P $$ claude 2>/dev/null' EXIT
Layer B — function guard to detect nested claude launches
# ~/.bashrc
claude() {
if [ -n "$CLAUDECODE" ] || [ -n "$CLAUDE_CODE_ENTRYPOINT" ]; then
echo "⚠️ Already inside Claude Code (nested launch)" >&2
echo " Nested launches are a primary source of orphans." >&2
echo " Use: command claude $*" >&2
return 1
fi
command claude "$@"
}
Layer C — SessionStart hook that scans and terminates obvious orphans
# ~/.claude/hooks/cleanup-orphan-claude.sh (SessionStart)
is_orphan() {
local pid="$1"
local ppid pcomm etime
ppid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ')
pcomm=$(ps -o comm= -p "$ppid" 2>/dev/null | tr -d ' ')
etime=$(ps -o etimes= -p "$pid" 2>/dev/null | tr -d ' ')
# (b) parent is an init-like reaper
case "$pcomm" in
systemd|init|Relay*) return 0 ;;
esac
# (a) parent is another claude and the child is over 60 min old (not a fresh intentional nest)
[ "$pcomm" = "claude" ] && [ -n "$etime" ] && [ "$etime" -gt 3600 ] && return 0
return 1
}
# ...iterate pgrep -x claude, skip ancestors of self, kill -TERM matches
After deploying these 3 layers, orphan accumulation stopped and GUI-freeze symptoms were substantially reduced.
Secondary observation (adjacent problem, separate feature request)
In addition to the orphan issue above, I observed that a Stop hook that called a remote LLM for every response completion caused synchronized LLM bursts across 4 tabs, which by itself contributed to WSL vmmem CPU spikes. I worked around this by removing the hook entirely (keeping the script on disk but unregistered). If Claude Code exposed a hook coalescing / rate-limit mechanism or a lightweight "job queue" abstraction for hook side effects, this adjacent problem could also be addressed cleanly — but that is a separate feature and I have no strong opinion on whether it belongs in this issue or a separate one. Happy to split.
Feature requests
The workaround is functional but the user-space layering is brittle, duplicates across machines, and does not help users who do not know the problem exists. Requests (any subset is welcome):
- Propagate
/exitdown the session tree. When/exitis issued on an outer Claude Code that spawned other Claude Code instances (via Bash,--resume, or any other channel) under the same process group, terminate the children as well. A small opt-in flag (--reap-children-on-exitor an envCLAUDE_CODE_REAP_CHILDREN=1) is acceptable. - Detect nested launches and warn/refuse by default. When
CLAUDECODEis already set in the environment,claudeshould print a prominent warning (and ideally refuse with a single override path like--force-nest) before starting a second session inside the first. This aligns with how other REPLs (psql, mysql, python) treat accidental recursion. - Track hook-spawned side processes and reap them on session end. If a hook forks a long-running subprocess, Claude Code should track the PID (or the process group) and send SIGTERM at
SessionEnd. A simple contract such as "hook processes must remain in the PGID of Claude Code to be auto-reaped" is low-cost to implement and communicates intent. - MCP child cleanup on abnormal parent exit. When a
claudeprocess dies, its MCP children should not outlive it by more than a few seconds. This may be implementable viaprctl(PR_SET_PDEATHSIG)on Linux — a one-line call per MCP child would eliminate a whole class of accumulation. - (Nice to have) Linux/WSL-aware orphan detection guidance. Any built-in cleanup or documentation should consider that on systemd / WSLg, orphans are reparented to
systemd --userorRelay(<pid>)rather thanPID 1. Tools checkingPPID == 1on Linux silently miss real orphans.
Related issues
- #19433 (closed, macos) —
/exitleaves orphan child processes. Filing this issue to document the Linux/WSL re-occurrence and the cross-process GUI-freeze impact that were not covered by the original closure. - #20369 (open, stale, macos) — Orphaned subagent leaks memory when parent terminal terminated.
- #46493 (open, macos) — Orphaned Claude session survives 11h, ignores SIGTERM.
- #51264 (open, macos) — Channel plugin bun processes orphan on session exit.
- #43944 (open, macos) — Background processes started by Bash tool not cleaned up.
Willing to contribute
Happy to provide additional repro data (strace, process trees with higher fidelity, before/after vmmem CPU timelines). If an Anthropic maintainer can point at the right module to patch for any of the feature requests above, I can also submit a PR for the Linux-specific pieces (e.g., PR_SET_PDEATHSIG on MCP children, PPID != 1 detection on WSL).
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗