[BUG] Child/nested claude processes orphan on /exit in WSL systemd environments, triggering cross-process GUI freezes via vmmem CPU spikes

Resolved 💬 2 comments Opened Apr 21, 2026 by GigiTiti-Kai Closed May 27, 2026

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:

  1. Nested/--resume spawn has no cleanup contract. A child claude survives 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.
  2. 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.
  3. MCP child processes do not clean up when their parent claude dies abnormally. This amplifies #1 and #2 since each orphaned claude keeps 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):

  1. Propagate /exit down the session tree. When /exit is 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-exit or an env CLAUDE_CODE_REAP_CHILDREN=1) is acceptable.
  2. Detect nested launches and warn/refuse by default. When CLAUDECODE is already set in the environment, claude should 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.
  3. 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.
  4. MCP child cleanup on abnormal parent exit. When a claude process dies, its MCP children should not outlive it by more than a few seconds. This may be implementable via prctl(PR_SET_PDEATHSIG) on Linux — a one-line call per MCP child would eliminate a whole class of accumulation.
  5. (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 --user or Relay(<pid>) rather than PID 1. Tools checking PPID == 1 on Linux silently miss real orphans.

Related issues

  • #19433 (closed, macos) — /exit leaves 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).

View original on GitHub ↗

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