幻のメモリリークに丸ごと一日を費やしましたが、それは決してリークではありませんでした。私のMacBookは這うように遅くなっていました — 14GBのRAMが私が起動していないプロセスによって消費されていました。原因は? AIのコーディングセッションの残骸として、何十もの孤立した MCP サーバ、ヘッドレス Chrome のインスタンス、そしてサブエージェントが残っていました。私はそれらを自動的に止める zclean を作りました。以下が全設定です。
要約: Claude Code や Codex のような AI コーディングツールは、セッションが終わってもクリーンアップされない子プロセス(MCP サーバ、ブラウザデーモン、サブエージェント)を生み出します。これらの孤児は静かに蓄積され、1日の作業日で 10GB 以上の RAM を消費することがあります。
zcleanはそれらを安全に検出して終了させます — セッションのライフサイクルにフックし、スケジュールで実行します。npx zclean initの 1 回で全てが設定されます。私は週 3–4 回の強制再起動から手動介入ゼロへと移行しました。
誰も話さない問題
AI コーディングツールは自分自身を後始末しません。Claude Code を 4 か月間集中的に使った後、午後の半ばにはマシンが重くなるのに気づき始めました — 十数個の node プロセス、いくつかの chrome-headless-shell、そしていくつかの mcp-server-* プロセスが、それらのセッションを終了した数時間後も動作しています。
すべての AI コーディングセッションは子プロセスの樹を生み出します。 Claude Code はファイルアクセス、ウェブ検索、カスタムツールのために MCP サーバを起動します。ウェブリサーチにはヘッドレスブラウザを起動します。 Codex はサブエージェントを生み出します。セッションが終了すると、これらのプロセスは終了するはずです。実際にはそう簡単には終わりません。
ある晩、さっと確認を行いました:
ps aux | grep -E 'mcp-server|chrome-headless|agent-browser' | wc -l
** 37 プロセス。** すべて孤児。すべてメモリを消費。合計RAM使用量は、全く何もしないプロセスで 6GB 以上。
これは私だけの話ではありません。X やデベロッパーフォーラムにも同様の報告が現れます — 「Claude Code は重い」「数回のセッションの後にマシンが遅くなる」。ツール自体は重くありません。残していくゾンビは重く、セッションを重ねるごとに蓄積します。
### ズルなしの対象リスト
デフォルトで `zclean` が探す対象は以下の通りです:
| カテゴリ | プロセスパターン | 出典 | |----------|----------------|--------| | MCP サーバ | `mcp-server-*` | Claude Code | | ブラウザデーモン | `agent-browser`, `chrome-headless-shell`, `playwright/driver` | Claude Code, Codex | | サブエージェント | 孤立した `claude --print`, `codex exec` | Claude Code, Codex | | ビルド・ゾンビ | `esbuild`, `vite`, `next dev`, `webpack` (24h+ 孤児) | Common | | npm ゾンビ | `npm exec`, `npx` (親なし) | Common | | Node 孤児 | `node` (親なし + 24h+ もしくは 500MB+ + cmdline に AI ツールのパス) | Common | | ランタイム孤児 | `tsx`, `ts-node`, `bun`, `deno`, `python` (MCP サーバのパターン) | Common | **`vite` や `webpack` のようなビルドツールには 24 時間の猶予期間があります。長時間実行されるビルドは正当に何時間もかかるため、安易には kill すべきではありません。しかし、孤立した `esbuild` プロセスが親を持たずに 1 日以上放置されている場合、それは死重量です。
設定方法: 1 コマンド
bash
npx zclean init
これで全てです。内部で何が起きているのかを見てみましょう。
ステップ 1: OS 検出。 zclean は macOS、Linux、Windows のいずれかを判定し、正しいプロセススキャンとスケジューリング機構を構成します。
ステップ 2: Claude Code フック。 Claude Code の settings.json に SessionEnd フックを登録します:
{
"hooks": {
"SessionEnd": [
{
"type": "command",
"command": "npx zclean --session-pid $SESSION_PID --yes"
}
]
}
}
この防御の第一線は、 Claude Code セッションが終了するたびに zclean がそのセッションの孤立した子プロセスをミリ秒単位で即座にクリーンアップする点です。
bash
~/Library/LaunchAgents/com.zclean.hourly.plist
On Linux, a systemd user timer:
~/.config/systemd/user/zclean.timer
On Windows, a user-scoped Task Scheduler entry.
**Step 4: Config file.** Drops a config at `~/.zclean/config.json` where you can whitelist processes, adjust thresholds, and customize behavior.
**Step 5: First scan.** Runs an immediate dry-run so you can see what it would have killed before enabling automatic cleanup.
## The Safety Mechanisms
I spent more time on the "don't kill the wrong thing" logic than on the actual killing. **Getting a false positive means terminating someone's running dev server — that's a non-starter.** Three independent safety layers prevent this.
### PID Reuse Protection
Between the time `zclean` scans and the time it kills, a process could die and its PID could be reassigned to something completely different. On a busy system, PID reuse happens faster than you'd expect — Linux recycles PIDs in order, so a freshly spawned process can inherit a just-killed PID within seconds.
Before every kill, `zclean` re-verifies three things:
1. **The PID still exists**
2. **The process start time matches** what was recorded during the scan (to the second)
3. **The command line matches** what was recorded during the scan
**If any of these three checks fail, the kill is skipped entirely.** This eliminates the complete class of PID reuse bugs without requiring atomic operations or locks.
### The Whitelist
Some processes look like zombies but aren't. The config handles persistent legitimate orphans:
json
{
"whitelist": [
"mcp-server-custom-db",
"my-persistent-agent"
],
"maxAge": 86400,
"maxMemoryMB": 500,
"dryRun": false
}
-
whitelist: Process names that are never touched, regardless of orphan status -
maxAge: Seconds before an orphan gets flagged (default: 86400 = 24 hours for build tools) -
maxMemoryMB: Memory threshold that escalates urgency (default: 500MB — above this, the process is flagged sooner) -
dryRun: Global toggle — set totrueto audit without committing
Protected Process Trees
Beyond the whitelist, zclean walks the full process tree to protect anything descended from:
- tmux / screen sessions — if the process is a descendant of a terminal multiplexer, it's intentional
- Daemon managers — pm2, forever, supervisord, systemd services
- VS Code — gets a 48-hour grace period since VS Code's process tree can appear orphaned after restarts
-
Docker containers — checked via PID namespace on Linux (
/proc/<pid>/ns/pid); Docker Desktop on macOS runs in a VM so container processes aren't visible to the hostpsat all
Daily Usage
Most of the time, you forget zclean exists. That's the goal. When you want visibility:
bash
# See what would be killed (dry-run, default)
npx zclean
# Actually kill the zombies
npx zclean --yes
# Check current zombie status
npx zclean status
# View kill history with timestamps and RAM reclaimed
npx zclean logs
# Show current config
npx zclean config
A typical dry-run output looks like this:
zclean — scanning for zombie processes...
Found 4 zombie processes:
PID CMD RAM AGE
──── ─────────────────────────── ─────── ──────
8234 mcp-server-filesystem 42 MB 3h 12m
8891 chrome-headless-shell 287 MB 2h 45m
9102 mcp-server-fetch 18 MB 1h 58m
12044 node (claude subagent) 156 MB 4h 03m
Total reclaimable: 503 MB
Run with --yes to kill these processes.
**503MB from four processes on a light day.** Peak scans have returned 15+ zombies consuming over 3GB on days with multiple long AI coding sessions.
## The Dual Protection Architecture
`zclean` doesn't rely on a single cleanup mechanism — redundancy is intentional.
**Layer 1: Session Hook** — fires on every clean session exit via the Claude Code `SessionEnd` hook. This catches the common case immediately, with zero delay between session end and cleanup.
**Layer 2: OS Scheduler** — runs hourly (configurable down to every 15 minutes). This catches everything the hook misses: crashed sessions, force-quits, Codex sessions that lack hook support, and any AI tool that spawns processes without a cleanup contract.
**The hook handles approximately 80% of cases instantly. The scheduler handles the remaining 20% within one hour.** Together, zombie RAM accumulation drops effectively to zero over any meaningful time period — verified over six weeks of continuous use on a MacBook Pro M2.
## What I'd Do Differently
**最初にプロセスツリー・ウォーカーを作るべきだった。** 私は単純な PPID チェックとパターンマッチングから始め、そこにエッジケースを次々と追加していきました―― tmux の保護、VS Code の猶予期間、Docker のネームスペース検査。ツリー・ウォーカーが基盤であるべきだった。全体のコード量を約 30%削減し、保護ロジックを特殊ケースの鎖ではなく、構成可能なものにしたであろう。
**Windows の実装は、より現実世界でのテストが必要です。** macOS と Linux は `/proc` と `ps` を使用しており、それらはよく理解され安定しています。 Windows は PowerShell 経由の WMI クエリを必要とし、プロセスモデルは根本的に異なります — 同じ意味の PPID の概念はなく、名前空間の分離も異なります。 私のテスト環境では動作しますが、Unix 系のシステムに比べて Windows のエッジケースには自信が薄いです。
**VS Code 自身が孤児になるプロセスの数を過小評価していました。** VS Code の子孫プロセスの 48 時間の猶予期間は、正当な TypeScript 言語サーバを誤って終了させた後、反応的に追加されました。『VS Code の孤児』と『VS Code の統合ターミナルを通じて生成された AI ツールの孤児』との境界は本当に曖昧です――AI ツールを混ぜる前から VS Code のプロセスツリーはすでに異常です。
## 数値
### zclean の前後
| 指標 | 前 | 後 |
|--------|--------|-------|
| 日終わり時点の平均孤児プロセス数 | 12–20 | 0–2 |
| 孤児プロセスによるRAM使用量 | 2–8 GB | < 100 MB |
| 週あたりの手動強制再起動回数 | 3–4 | 0 |
| 「なぜ私の Mac が遅いのか」を調べるのに費やす時間 | ~30 分/日 | 0 |
### ツールのリソースフットプリント
| 指標 | 値 |
|--------|-------|
| zclean のスキャン時間 | < 200ms |
| スキャン時のRAM使用量 | ~12 MB |
| npm 依存関係 | 0 (純粋な Node.js) |
| 対応プラットフォーム | macOS、Linux、Windows |
| 設定ファイルサイズ | ~200 バイト |
| インストール + 初期化時間 | < 10 秒 |
**NPM依存関係ゼロ。** スキャナーは `child_process.execSync` をネイティブOSコマンドと組み合わせて使用します(Unix では `ps`、Windows では `Get-Process`)。 ネイティブモジュールなし、コンパイルステップなし、`node-gyp` の悪夢もなし。 全体のツールは 1 つの Node.js ファイルで、10 分未満で読んで監査できます。
## よくある質問
**zclean は実行中の開発サーバを終了しますか?** いいえ。`zclean` は孤児プロセスだけを対象とします — 親が死んで init/launchd に再割り当てられたもの(PPID = 1)です。 開いている端末から起動された開発サーバであれば、その親は生きているため、影響はありません。 pm2、forever、supervisord によって管理されるプロセスも、ツリー・ウォーク検出を通じて明示的に保護されます。
**正当な長時間実行の MCP サーバがある場合は?** それを `~/.zclean/config.json` のホワイトリストに追加します。ホワイトリストに登録されたプロセス名は、孤児状態やメモリ使用量に関係なく決して終了されません。デフォルトの 24 時間の猶予期間がワークフローに十分でない場合は、`maxAge` を調整できます。
**Codex、Cursor、その他のAI コーディングツールと動作しますか?** はい。`SessionEnd` フックは Claude Code に特有ですが、OS のスケジューラ(レイヤー2)はどのツールからの孤児も捕捉します。対象プロセスのパターンには Codex、Cursor、他のツールが MCP サーバとヘッドレスブラウザを生成する際の一般的な署名が含まれます。ツールのプロセスが孤児化し、既知のパターンと一致する場合、`zclean` は次の毎時実行でそれらを検出します。
**Docker 内のプロセスを誤って終了させてしまうことがありますか?** いいえ。Linux では `/proc//ns/pid` を介して PID ネームスペースを確認し、プロセスがホストネームスペース内にあることを確認します。Docker コンテナは分離された PID ネームスペースで実行され、スキャンの対象外です。macOS では Docker Desktop が Linux VM 上で動作しており、ホストの `ps` にはコンテナプロセスが見えません。
**zclean 自体が kill の途中でクラッシュした場合は?** 各 kill は独立しています — 共有トランザクション状態はありません。7 匹の zombie のうち 3 匹を終了させた後に `zclean` がクラッシュしても、残りの 4 匹は次の予定実行の 1 時間以内に検出されます。**PID 再利用保護により、スキャンと終了の間にシステム状態が変わっても、誤ったプロセスが終了されることはありません。**
## 自分で試してみる
1. **インストールと初期化** — `npx zclean init` は OS を検出し、フックを登録し、10 秒未満でスケジューラを設定します
2. **ドライランを実行** — `npx zclean` は何を削除するかを、何も変更せずに表示します
3. **出力を確認** — 検出されたプロセスが本当に孤児であることを検証します
4. **削除を有効化** — `npx zclean --yes`、または自動クリーンアップのために設定の `dryRun: true` を削除
5. **忘れてください** — ここからはフックとスケジューラがすべてを処理します
## 結論
AI コーディングツールを毎日使用していて、日中にマシンの速度が徐々に低下する場合、メモリリークが原因ではなくゾンビ問題を抱えている可能性があります。`zclean` は依存関係ゼロの Node.js ユーティリティで、単一のインストールコマンドと2 つの保護レイヤーでそれを恒久的に解決します。
**最悪のゾンビ蓄積をマシンで見たことがありますか?** macOS に偏っているのか、それとも Linux と Windows ユーザーにも等しく見られるのか、興味があります。
もしこれで次の強制再起動を避けられたなら、チーム内で「Claude は重い」と愚痴をこぼす人に共有してみてください。
今後は AI 支援開発の背後にある地味なインフラ作業と、開発者ツール構築に関する投稿を追ってください。
---
*I build AI-powered developer tools and write about the engineering behind them. Currently running an AI agent orchestration system with multi-model routing across Claude, Gemini, and GPT — which is, ironically, also the source of most of my zombie processes.*