Automated cleanup for orphan Claude Code processes (subagents, MCP servers, plugins) that leak memory after sessions end.
Claude Code spawns subagent processes and MCP servers for each session. When sessions end (especially abnormally), these processes become orphans (PPID=1) and keep consuming RAM and CPU — often 200-400 MB each, with some (like Cloudflare's MCP server) hitting 550%+ CPU. With multiple sessions over a day, this can accumulate to 7+ GB of wasted memory.
This is a widely reported issue affecting macOS and Linux users.
| Process Type | Pattern | Typical Size |
|---|---|---|
| Subagents | claude --output-format stream-json |
180-300 MB each |
| MCP servers (short-lived) | npx mcp-server-cloudflare, npm exec mcp-*, etc. |
40-110 MB each |
| claude-mem worker | worker-service.cjs --daemon (bun) |
100 MB |
| Agent browser sessions | agent-browser-darwin-arm64, Chrome-for-Testing with agent-browser-chrome-* profiles |
100-600 MB each |
| Puppeteer headless Chrome | Chrome/Chrome Helper with puppeteer_dev_chrome_profile-* profiles |
Can pin CPU/GPU |
| Codex background sessions | node /usr/local/bin/codex, @openai/codex/.../codex --yolo |
Session + MCP tree |
| File descriptors | VM processes, settings.json, MCP stdio pipes | ~6,200 FDs/hr leak rate |
Not killed: User apps and system services such as ChatGPT.app, cmux.app, Bitdefender, Spotlight (
mdworker/mds_stores), normal Chrome browsing, and web dev servers are protected. Long-running MCP servers shared across sessions (Supabase, Stripe, claude-mem, chroma-mcp, Cloudflare/sequential-thinking variants) are also protected. Stale browser/Codex cleanup only targets orphaned or old automation processes.
All layers use PGID-based process group cleanup as the primary method — killing entire process groups spawned by a Claude session with a single kill -- -$PGID. Pattern-based detection is kept as a fallback for edge cases.
Session ends normally
└── Stop hook — kills session's process group via PGID (catches all children)
Session crashes / terminal force-closed
└── proc-janitor daemon — scans every 30s, kills orphans after 60s grace
└── OR: LaunchAgent — zero-dependency macOS native, PGID group kill + PPID=1 fallback
Manual intervention needed
└── cc-monitor — explain current CPU heat by process family before cleanup
└── claude-cleanup — finds orphaned PGIDs and stale agent-browser/Puppeteer/Codex stragglers
└── claude-ram — check RAM/CPU usage breakdown with orphan visibility
Claude Code sessions are process group leaders (PGID = session PID). All spawned MCP servers, subagents, and their children inherit this PGID. This means one kill -- -$PGID reliably cleans up everything — including third-party MCP servers that pattern matching might miss.
Safety: PGID cleanup only targets groups whose leader is a Claude CLI session (claude.*stream-json). It never matches by group membership — other apps like Chrome and Cursor have claude subprocesses in their process groups, so matching by membership would kill them.
git clone https://github.com/theQuert/cc-reaper.git
cd cc-reaper
chmod +x install.sh
./install.shUpdating:
git pull
./install.shThe installer auto-updates hook and shell functions. For proc-janitor users, manually sync the config:
cp proc-janitor/config.toml ~/.config/proc-janitor/config.toml
# Edit the log path: replace ~ with your actual home directoryAdd to ~/.zshrc or ~/.bashrc:
source /path/to/cc-reaper/shell/claude-cleanup.sh
source /path/to/cc-reaper/shell/cc-monitor.shCommands available after restart:
cc-monitor— explain current CPU heat contributors by process family before cleanup (read-only)cc-monitor --once— take one process snapshot and return immediatelycc-monitor --json— emit structured JSON for future automationcc-monitor --apply <module>— sample, print report, then dispatch a cleanup module (skips menu/confirm; cannot combine with--json)cc-monitor --no-prompt— disable the interactive optimization menu on a TTYclaude-ram— show RAM/CPU usage breakdown with per-session details and orphan visibility (read-only)claude-fd— show file descriptor usage per session and VirtualMachine processes (read-only)claude-sessions— list all active sessions with idle detection and process tree RAMclaude-cleanup— kill orphan processes immediately (PGID group kill + pattern fallback, plus stale agent-browser/Puppeteer/Codex cleanup)claude-guard— automatic session reaper: kills FD-leaking, bloated (RSS > threshold), and excess idle sessionsclaude-guard --dry-run— preview what claude-guard would kill without actually killing
Copy the hook script:
mkdir -p ~/.claude/hooks
cp hooks/stop-cleanup-orphans.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/stop-cleanup-orphans.shAdd to ~/.claude/settings.json in the "Stop" hooks array:
{
"type": "command",
"command": "\"$HOME\"/.claude/hooks/stop-cleanup-orphans.sh",
"timeout": 15
}Full settings.json example
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$HOME\"/.claude/hooks/stop-cleanup-orphans.sh",
"timeout": 15
}
]
}
]
}
}Native macOS approach — no Homebrew or Rust required. Runs every 10 minutes, detects orphans by PPID=1.
mkdir -p ~/.cc-reaper/logs
cp launchd/cc-reaper-monitor.sh ~/.cc-reaper/
chmod +x ~/.cc-reaper/cc-reaper-monitor.sh
# Install and replace __HOME__ with actual path
sed "s|__HOME__|$HOME|g" launchd/com.cc-reaper.orphan-monitor.plist \
> ~/Library/LaunchAgents/com.cc-reaper.orphan-monitor.plist
launchctl load ~/Library/LaunchAgents/com.cc-reaper.orphan-monitor.plistUseful commands:
launchctl list | grep cc-reaper # check if running
cat ~/.cc-reaper/logs/monitor.log # view cleanup log
launchctl unload ~/Library/LaunchAgents/com.cc-reaper.orphan-monitor.plist # stopRust-based daemon with grace period, whitelist, and detailed logging. Requires Homebrew or Cargo.
# Install
brew install jhlee0409/tap/proc-janitor # or: cargo install proc-janitor
# Copy config
mkdir -p ~/.config/proc-janitor
cp proc-janitor/config.toml ~/.config/proc-janitor/config.toml
chmod 600 ~/.config/proc-janitor/config.tomlEdit ~/.config/proc-janitor/config.toml and replace ~ in the log path with your actual home directory.
Start daemon:
brew services start jhlee0409/tap/proc-janitor # auto-start on boot
proc-janitor start # or manualUseful commands:
proc-janitor scan # dry run — show orphans without killing
proc-janitor clean # kill detected orphans
proc-janitor status # check daemon healthclaude-guard is an automatic session reaper that prevents runaway resource consumption. It operates in three phases:
- FD-leak session kill — Sessions whose open file descriptor count exceeds
CC_MAX_FDare killed immediately. This addresses the widely reported FD exhaustion issue where VM processes leak ~6,200 FDs/hour, eventually causing system-wide "Operation not permitted" errors. - Bloated session kill — Sessions whose tree RSS (process + all children) exceeds
CC_MAX_RSS_MBare killed immediately via PGID, regardless of whether they're idle or active. This addresses the ~42 GB/hr memory leak caused by unreleased streaming ArrayBuffers. - Idle session eviction — If session count still exceeds
CC_MAX_SESSIONS, the oldest idle sessions are killed.
claude-guard # run the guard (kills bloated + excess idle)
claude-guard --dry-run # preview without killing| Variable | Default | Description |
|---|---|---|
CC_MAX_SESSIONS |
3 | Max allowed concurrent sessions before idle eviction |
CC_IDLE_THRESHOLD |
1 | CPU% below which a session is considered idle |
CC_MAX_RSS_MB |
4096 | Tree RSS threshold (MB); sessions exceeding this are killed regardless of activity |
CC_MAX_FD |
10000 | File descriptor threshold; sessions exceeding this are killed as FD-leak |
CC_AGENT_STALE_MINUTES |
360 | Age threshold for stale agent-browser, Puppeteer Chrome, and detached Codex/MCP cleanup |
CC_RUNAWAY_CPU |
80 | CPU% above which a protected process is treated as stuck/runaway (combined with CC_RUNAWAY_MIN) |
CC_RUNAWAY_MIN |
60 | Minutes of elapsed time required before a hot protected process is treated as runaway |
CC_RUNAWAY_GRACE_SEC |
5 | Seconds claude-guard waits (Ctrl+C to abort) before SIGTERM-ing runaway protected processes |
CC_RUNAWAY_DISABLE |
0 | Set to 1 to skip claude-guard's runaway phase entirely |
Example: lower the thresholds for constrained machines:
export CC_MAX_RSS_MB=2048
export CC_MAX_FD=5000
export CC_AGENT_STALE_MINUTES=120
claude-guard
claude-cleanupCC_AGENT_STALE_MINUTES is used by claude-cleanup and the LaunchAgent monitor. Lower it only if browser automation frequently leaks on your machine; the default is intentionally conservative.
Run cc-monitor when the laptop is hot and you want to understand the cause before cleaning anything:
cc-monitor # sample for 60s at 5s intervals; progress prints to stderr
cc-monitor --once # immediate snapshot
cc-monitor --json # machine-readable outputThe monitor is read-only. It groups processes into families such as editor, cmux, Codex, Claude, MCP, agent-browser, Chrome, dev server, system, and other. Each finding is classified as:
| Classification | Meaning |
|---|---|
SAFE_TO_REAP |
Stale or orphaned process that matches existing cc-reaper cleanup criteria |
ASK_BEFORE_KILL |
Active user tool or recent automation; inspect before stopping |
DO_NOT_KILL |
System, security, UI, or normal browsing process |
JSON output includes command strings for automation, with common token/key/secret/password argument values redacted.
After printing the report, cc-monitor can dispatch the right cleanup module so you don't have to switch commands.
Interactive mode (default on a TTY when the report has SAFE_TO_REAP candidates or family-level heat):
$ cc-monitor --once
=== cc-monitor: heat attribution ===
... report ...
Optimization options:
1. claude-cleanup (kill all stale orphans) (recommended)
2. claude-guard --dry-run (preview only)
3. proc-janitor scan (preview only)
4. skip
> 1
Run claude-cleanup (kill all stale orphans)? [y/N] y
The recommended option is claude-cleanup when stale/orphan candidates exist, otherwise claude-guard --dry-run when family RSS or per-process CPU is high. The menu is skipped when no candidates exist, on --json, when stdin/stdout is not a TTY, or when --no-prompt is passed. Modules whose binary is not on PATH are hidden from the menu and listed below with an install hint.
Script-friendly mode with --apply:
cc-monitor --once --apply claude-cleanup # kill stale orphans
cc-monitor --once --apply claude-guard-dry # preview-only
cc-monitor --once --apply proc-janitor-scan # preview-only via daemon--apply skips the confirmation prompt — the flag is itself the explicit opt-in. It cannot be combined with --json (exit 2). Module exit codes propagate.
Long-running MCP servers, dev servers, and security daemons are intentionally protected — claude-cleanup will never kill them. But "protected" is not absolute: a process pinned at high CPU for hours is broken, regardless of category.
cc-monitor and claude-guard detect runaway protected processes when both thresholds are met:
- average CPU% ≥
CC_RUNAWAY_CPU(default80) - elapsed time ≥
CC_RUNAWAY_MINminutes (default60)
cc-monitor then reclassifies the finding from DO_NOT_KILL to family runaway / ASK_BEFORE_KILL, and prints a dedicated "Stuck/runaway protected processes" section with a copy-pasteable kill line:
Stuck/runaway protected processes:
PID 9594 node avg 102.70% etime 09:07:51 — protected process appears stuck (sustained high CPU over long elapsed time); review and kill if not actively serving
suggested: kill 9594
claude-guard adds a Phase 0.5 that reaps these PIDs in PGID-aware mode after CC_RUNAWAY_GRACE_SEC (default 5) seconds, so you can Ctrl+C if the report surprises you:
=== Claude Guard ===
Config: max_sessions=3, idle_threshold=1%, max_rss=4096 MB, max_fd=10000, runaway=80%/60min
--- Runaway protected processes (CPU >= 80% for >= 60 min) ---
PID 9594 CPU 102.7% ETIME 09:07:51 node /Users/.../mcp-server-cloudflare run abc
Sending SIGTERM in 5 seconds (Ctrl+C to abort)...
Reaped 1 runaway protected process(es), freed ~340 MB
Set CC_RUNAWAY_DISABLE=1 to skip the runaway phase entirely. JSON consumers see runaway entries in the existing findings array (with family: "runaway") plus a dedicated runaway_candidates array.
Example:
=== cc-monitor: heat attribution ===
Sample: once, snapshots: 1
Mode: read-only (no signals sent)
Top contributors:
1. cmux pid 62199 avg 93.00% max 93.00% rss 561 MB ASK_BEFORE_KILL cmux
2. WindowServer pid 384 avg 14.90% max 14.90% rss 128 MB DO_NOT_KILL system
Safe cleanup candidates:
PID 37915 agent-browser avg 0.00% max 0.00% - stale or orphaned browser automation matches cc-reaper cleanup criteria
| Tool | Required | Install |
|---|---|---|
| bash/zsh | Required | Pre-installed on macOS/Linux |
| macOS LaunchAgent | Option A (recommended) | Built-in, zero dependencies |
| proc-janitor | Option B | brew install jhlee0409/tap/proc-janitor |
| Claude Code | — | The tool this project cleans up after |
cc-reaper/
├── install.sh # One-command installer/updater (interactive daemon choice)
├── hooks/
│ └── stop-cleanup-orphans.sh # Claude Code Stop hook (PGID + pattern fallback)
├── launchd/
│ ├── cc-reaper-monitor.sh # LaunchAgent monitor script (PGID + PPID=1 fallback)
│ └── com.cc-reaper.orphan-monitor.plist # LaunchAgent config (10-min interval)
├── proc-janitor/
│ └── config.toml # proc-janitor daemon config (alternative to LaunchAgent)
├── shell/
│ ├── cc-monitor.sh # Read-only heat attribution monitor
│ └── claude-cleanup.sh # Shell functions (claude-ram, claude-fd, claude-cleanup, claude-sessions, claude-guard)
├── tests/
│ └── agent-process-patterns.sh # Lightweight matcher/candidate validation
└── README.md
See CHANGELOG.md for version history.
- anthropics/claude-code#20369 — Orphaned subagent process leaks memory
- anthropics/claude-code#22554 — Subagent processes not terminating on macOS
- anthropics/claude-code#25545 — Excessive RAM when idle
- thedotmack/claude-mem#650 — worker-service spawns subagents that don't exit
- anthropics/claude-code#29888 — VM process FD leak (~6,200/hr)
- anthropics/claude-code#28896 — settings.json FD leak (1 per tool call)
- anthropics/claude-code#37482 — MCP server stdio pipe breaks (orphaned FDs)
Apache 2.0