[BUG] Stop hook bash commands leave orphan child processes on Windows when parent dies abnormally

Resolved 💬 1 comment Opened Jun 8, 2026 by chuckcap Closed Jul 14, 2026

Summary

On Windows + Git Bash (MSYS2), bash-based commands configured as Stop hooks leave their child processes alive as orphans when Claude Code terminates abnormally mid-hook (session archive, force-quit, machine sleep, harness 120s timeout). These orphans accumulate over hours, each holding hundreds of MB to multiple GB of working set, and ultimately crash the machine via memory exhaustion.

Believed to be the same underlying Windows process-tree weakness as #1935 (MCP server orphans) and #54626 (subagent leaks), but for synchronous Stop hook commands rather than long-running daemons.

Environment

  • OS: Windows 11
  • Claude Code: 2.1.x (also observed on prior versions)
  • Shell: Git Bash for Windows (MSYS2)
  • Repo: TypeScript monorepo (backend + frontend), ~5-30s tsc --noEmit per workspace

Reproduction

  1. Configure a Stop hook that runs a bash script which spawns background processes:

``json
"Stop": [{ "hooks": [{ "type": "command", "command": "bash scripts/typecheck-changed.sh", "timeout": 120 }] }]
`
where the script does:
`bash
(cd backend && npx --no-install tsc --noEmit) &
(cd frontend && npx --no-install tsc -b) &
wait
``

  1. Use Claude Code normally — script fires every turn, normally completes
  2. Force-close Claude Code mid-typecheck (or archive a session with the warning ignored)
  3. Open Task Manager — observe orphaned node.exe / bash.exe processes still running, still consuming memory
  4. Over the course of a working day, repeated abnormal terminations leave 2-5 orphans, each growing from ~200 MB initial working set toward multiple GB as the day progresses
  5. Eventually the machine runs out of memory and hard-crashes

Observed evidence (founder's machine)

Two memory-exhaustion crashes in 24 hours:

Crash 1: Three orphan git.exe / node.exe processes at 3.2, 3.1, 3.0 GB working set. Oldest started ~20 hours prior — a session archived the previous evening. ~9.3 GB total just from the leak.

Crash 2: Two Git for Windows group processes at ~6 GB each (12 GB combined). Same pattern.

Both crashes cleared on reboot, but leak re-accumulated within hours of resuming work.

Mid-session sampling on a fresh boot showed orphans accumulating in real time — six node processes (~89 MB each) from the Stop hook within ~30 seconds of consecutive turn endings.

Root cause hypothesis

Three contributing factors:

  1. Bash backgrounded & jobs survive parent death on MSYS2. When the bash parent receives a hard termination signal, backgrounded children don't reliably get HUP/TERM propagated. This is a known MSYS2 / Cygwin / Windows limitation — Unix signal semantics don't fully map.
  1. npx spawns a detached node child, so even if bash does receive a signal and propagates it to $!, that PID is the npx wrapper, not the actual node process holding the memory.
  1. The harness's 120s timeout doesn't appear to walk the process tree. When the harness force-kills the hook command, only the bash process itself dies; descendants survive.

Proposed fix (upstream)

Wrap the hook command's process in a Windows Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. The Windows kernel then guarantees that all processes in the job get killed when the job handle is released — including when Claude Code itself dies abruptly. This is the structural fix that closes the leak for all bash-hook commands on Windows, not just typecheck scripts.

Reference: JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE — MSDN

The same fix would close #1935 (MCP server orphans) and #54626 (subagent process leaks) — anything Claude Code spawns can be put in a per-instance Job Object.

Workaround (project-side, not a fix)

For projects that can't wait, we implemented a script-level mitigation in chuckcap/bookieassist-bet-trader#609:

  • Atomic per-worktree, per-mode lockdir prevents overlapping hook fires
  • Stale-owner detection reaps prior orphans on next hook fire
  • Direct node node_modules/typescript/bin/tsc invocation (no npx wrapper) so the tracked PID is the actual leaking process
  • Windows-native taskkill /T /F walks the process tree on cleanup
  • SessionStart hook with --reap-only mode catches orphans from prior crashed sessions

This mitigates the symptom but each project has to implement it independently, and only for hooks they control. MCP servers and subagents (same root cause, different code path) remain affected.

Related

  • #1935 — MCP servers not properly terminated when Claude Code exits
  • #54626 — Scheduled tasks and background sub-agents leak processes/UI state
  • #60747 (closed) — Reap MCP child processes on parent claude exit

If a Job Object wrapper is added to one of these surfaces (MCP, subagent, or hook), the same pattern could plausibly close the others.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗