cd /news/developer-tools/hunting-down-zombie-agents-logging-s… · home topics developer-tools article
[ARTICLE · art-69415] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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.

read6 min views1 publishedJul 23, 2026

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


OUT_LOG="$LOG_DIR/agent-invocations.jsonl"

The heart of the Python portion is a two-pass parse of the transcript.

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 asname="Agent"

. Thesubagent_type

lives ininput.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.

cutoff = now - td
recent = [r for r in records if r["_dt"] >= cutoff]

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

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

.

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

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

<key>StartCalendarInterval</key>
<dict>
    <key>Hour</key>
    <integer>10</integer>
    <key>Minute</key>
    <integer>15</integer>
</dict>

The command it runs is simple.

<string>/bin/zsh -c
  ~/.claude/scripts/agent-usage-summary.sh 7d 30d
  > ~/.claude/logs/agent-usage-latest.md 2>&1</string>

The PATH explicitly includes nvm and Homebrew because launchd's default PATH won't find python3

.

With this in place, agent-usage-latest.md

gets refreshed every morning at 10:15, so I always know how many agents have sat at zero for 30 days.

Once a week, I run these commands.

~/.claude/scripts/agents-index.sh

cat ~/.claude/logs/agent-usage-latest.md | grep -A 9999 "0-call agents"

My decision criteria look like this.

State Action
0 calls in 30d, and reading the description reveals no realistic use case Delete
0 calls in 30d, but there's a future use case Keep, and spell out the usage conditions in the description
0 calls in 7d, a few calls in 30d Possibly a seasonal job — hold off
Top 5 regular Revisit its permissions and tool definitions to sharpen it further

In this round of housekeeping I deleted 14 agents confirmed at zero calls over 30 days, including a11y-architect

, django-reviewer

, and go-build-resolver

. They simply disappear from INDEX.md — nothing else changes.

name="Task"

matched nothingname="Agent"

. At first every record came back empty because of thisseen_ids

duplicate check on (session_id, tool_use_id)

subagent_type

crept inif r.get("subagent_type")

(the empty string being falsy is enough)INDEX.md

itself, creating a loopif p.name == "INDEX.md": continue

python3

known_agents

is built from the file listing of agents_dir

, so it doesn't depend on the window. This is the correct behavior~/.claude/agents/*.md

, any newly added agent is automatically trackedagent-usage-latest.md

, so "how many zombies do I have as of today?" is always answerableNext time, I'll use this log data to visualize which agents get combined for which kinds of work.

*Written by Lily — I ship iOS apps and automate my content stack with Claude Code.

Follow along: Portfolio · X · GitHub*

── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/hunting-down-zombie-…] indexed:0 read:6min 2026-07-23 ·