Hunting Down Zombie Agents: Logging Subagent Usage with a Claude Code Stop Hook A developer built a system using a Claude Code Stop hook to log subagent invocations to JSONL, revealing 44 of 44 defined agents had zero usage in 30 days. The setup parses session transcripts to track agent calls and generates daily reports to identify unused 'zombie' agents for cleanup. Quick question: how many of the subagents you've defined in ~/.claude/agents/ have actually been called this month? I couldn't have told you — until I started measuring, and this morning's tally revealed 44 agents that hadn't been invoked even once in 30 days . This post is part of my "Claude Code environment" series a follow-up to automatic terminal window tiling https://zenn.dev/bokuwalily/articles/terminal-window-tiling , continuing the theme of keeping the setup from bloating. I'll walk through, with real code, a system that records subagent invocations to JSONL via a Stop hook and uses a daily report to flush out the zombies and cull them. Adding an agent to Claude Code is as simple as writing frontmatter in .claude/agents/ .md . Because it's so easy, "let's just define it for now" agents pile up. The problem is that there's no way to check whether an agent is actually being used. INDEX.md goes stale immediatelyAn agent with zero usage still takes up space in the context injected at startup. Left alone, it becomes a zombie: defined but unused → unclear whether it even works → can't be deleted either. The system consists of three components. セッション終了 └─ Stop hook stop agent tracker.sh └─ transcript.jsonl を解析 → agent-invocations.jsonl に追記 │ 毎日 10:15 launchd ──────┘ └─ agent-usage-summary.sh 7d 30d └─ agent-usage-latest.md(Top10 + 0回リスト) 手動 or cron └─ agents-index.sh → INDEX.md 自動再生成 ~/.claude/hooks/stop agent tracker.sh runs when a session ends. The Stop hook receives session info and a transcript path on stdin, so the script parses that transcript and picks out Agent tool invocations. stop agent tracker.sh(抜粋) stdin: {"session id":"...","transcript path":"...","hook event name":"Stop",...} 出力: ~/.claude/logs/agent-invocations.jsonl OUT LOG="$LOG DIR/agent-invocations.jsonl" The heart of the Python portion is a two-pass parse of the transcript. php 第1パス: tool use name="Agent" と tool result をインデックス化 uses = {} id - ts, name, input, caller results = {} tool use id - ts, is error for b in content: if btype == "tool use" and b.get "name" == "Agent": inp = b.get "input" or {} if "subagent type" not in inp: continue uses uid = ts, b.get "name" , inp, b.get "caller" elif btype == "tool result": results rid = ts, bool b.get "is error" Note:In Claude Code transcripts, the "Task" tool is recorded as name="Agent" . The subagent type lives in input.subagent type . The same comment appears in the code itself. To prevent duplicates, the same session id + tool use id combination is skipped so even if the Stop hook fires multiple times in one session, nothing gets written twice . A single JSONL record looks like this. {"ts": "2026-05-28T16:27:41.766Z", "session id": "TEST-AGENT-TRACKER-001", "cwd": "~", "tool use id": "toolu 014MM...", "subagent type": "general-purpose", "description": "launchd + cron 総監査", "duration ms": 177, "status": "ok", "caller": {"type": "direct"}} So far, 546 records about 176 KB have accumulated. ~/.claude/scripts/agent-usage-summary.sh handles the aggregation. It's inline python3 with no external libraries. 使い方 agent-usage-summary.sh デフォルト 7d agent-usage-summary.sh 30d 30日 agent-usage-summary.sh 7d 30d 両方(launchdはこれ) Internally it's three steps. ① JOSNLをロードしてウィンドウでフィルタ cutoff = now - td recent = r for r in records if r " dt" = cutoff ② subagent type でカウント + エラー数 counts = Counter r.get "subagent type", "" for r in recent if r.get "subagent type" errors = Counter r.get "subagent type", "" for r in recent if r.get "status" == "error" ③ ~/.claude/agents/ .md のファイル名一覧と突き合わせて未使用を出す known agents = set for fp in glob.glob os.path.join agents dir, " .md" : known agents.add os.path.splitext os.path.basename fp 0 unused = sorted known agents - set counts.keys Here's this morning's actual output ~/.claude/logs/agent-usage-latest.md . === Agent usage last 7d === total invocations: 127 unique types: 5 Top 10: agent calls errors general-purpose 76 0 Explore 45 0 Content Creator 2 0 fork 2 0 reviewer 2 0 0-call agents defined locally but not used in 7d : 47 - a11y-architect - architect - build-error-resolver - code-architect - code-explorer - code-reviewer ... In 7 days, only 5 agent types were called and 47 weren't called at all. Even in the 30-day window, 44 remain at zero. general-purpose and Explore account for 95% of all invocations. To decide whether something can be deleted, you need a cross-cutting view of what each agent was written for. agents-index.sh reads the frontmatter of ~/.claude/agents/ .md and builds INDEX.md . 簡易frontmatterパーサ(PyYAML依存なし) m = re.match r"^---\n . ? \n---\n", text, flags=re.DOTALL body = m.group 1 for line in body.split "\n" : k, , v = line.partition ":" out k.strip = v.strip The generated INDEX.md is a table like this. < -- AUTO-GENERATED by ~/.claude/scripts/agents-index.sh — DO NOT EDIT MANUALLY -- Agents Index 51 agents · 2026-07-14 10:15 | Name | Model | Description | Tools | |------|-------|-------------|-------| | general-purpose general-purpose.md | - | General-purpose agent for... | | ... With the --json flag it also writes out .index.json , which can be reused programmatically by things like a cost tracker. Files missing name or description in their frontmatter get listed under a ⚠️ Validation warnings section. ~/Library/LaunchAgents/com.shun.agent-usage-daily.plist runs every day at 10:15.