Skip to content

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

Closed as not planned

Description

@GigiTiti-Kai

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 Create SECURITY.md #1 and I want to use openrouter #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

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

Activity

  1. github-actions commented on Apr 21, 2026

    @github-actions

    Found 3 possible duplicate issues:

    1. [BUG] MCP server and subagent processes not cleaned up on session end — orphan accumulation (macOS PPID=1) #33947
    2. Background processes started by Bash tool are not cleaned up on session exit #43944
    3. [BUG] MCP servers not properly terminated when Claude Code exits, causing orphaned processes #1935

    This issue will be automatically closed as a duplicate in 3 days.

    • If your issue is a duplicate, please close it and 👍 the existing issue instead
    • To prevent auto-closure, add a comment or 👎 this comment

    🤖 Generated with Claude Code

  2. github-actions commented on May 27, 2026

    @github-actions

    Closing for now — inactive for too long. Please open a new issue if this is still relevant.

  3. github-actions commented on Jul 24, 2026

    @github-actions

    This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.

  4. locked as resolved and limited conversation to collaborators on Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions