Lily

Lily

投稿日:7月23日 • オリジナル公開先 dev.to

簡単な質問:~/.claude/agents/ に定義したサブエージェントのうち、今月実際に呼び出されたのはいくつでしょうか? 私は今まで答えられませんでした — 測定を始めるまでは。そして今朝の集計で、30日間で一度も呼び出されなかった44個のエージェントが明らかになりました。この記事は「Claude Code環境」シリーズ(ターミナルウィンドウの自動タイリングの続編)の一環で、セットアップが肥大化しないよう維持するテーマを引き継いでいます。実際のコードを交え、Stopフックでサブエージェントの呼び出しをJSONLに記録し、日次レポートでゾンビを洗い出して整理する仕組みを解説します。

問題:定義と使用のギャップ

Claude Codeにエージェントを追加するのは、.claude/agents/*.md にfrontmatterを書くだけで簡単です。そのため「とりあえず定義しておこう」なエージェントが溜まっていきます。問題は、そのエージェントが実際に使われているかを確認する方法がないことです。

  • セッション間で呼び出し回数を集計する仕組みがない
  • 手書きのINDEX.mdはすぐに古くなる
  • 「削除してよいか」を判断する根拠がない

使用回数がゼロのエージェントも、起動時に注入されるコンテキストを占有します。放置すればゾンビ化します:定義されているが未使用 → 動作するかも不明 → 削除もできない。

全体の流れ

このシステムは3つのコンポーネントで構成されます。

セッション終了
  └─ 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 自動再生成

全画面表示 全画面表示を終了

手順1:StopフックでJSONLに記録

~/.claude/hooks/stop_agent_tracker.sh はセッション終了時に実行されます。Stopフックは標準入力でsession情報とtranscript_pathを受け取るので、スクリプトはtranscriptを解析してAgentツールの呼び出しを抽出します。

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

全画面表示 全画面表示を終了

Python部分の核心は、transcriptの2パス解析です。

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

全画面表示 全画面表示を終了

注:Claude Codeのtranscriptでは、「Task」ツールはname="Agent"として記録されます。subagent_typeinput.subagent_typeにあります。このコメントはコード内にも記載されています。

重複を防ぐため、同じsession_id + tool_use_idの組み合わせはスキップされます(Stopフックが1セッションで複数回発火しても、同一データが2度書き込まれることはありません)。

1件のJSONLレコードは以下のようになります。

{"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"}}

全画面表示 全画面表示を終了

現在までに546件(約176KB)のレコードが蓄積されています。

手順2:7日/30日ウィンドウでTop10と0回リストを作成

~/.claude/scripts/agent-usage-summary.sh が集計処理を担います。外部ライブラリ不要のinline python3です。

# 使い方
agent-usage-summary.sh           # デフォルト 7d
agent-usage-summary.sh 30d       # 30日
agent-usage-summary.sh 7d 30d    # 両方(launchdはこれ)

全画面表示 全画面表示を終了

内部処理は3ステップです。

# ① 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()))

全画面表示 全画面表示を終了

今朝の実際の出力(~/.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
  ...

全画面表示 全画面表示を終了

7日間で呼び出されたエージェントタイプは5種類のみで、47種類は一度も呼び出されていませんでした。30日ウィンドウでも44種類がゼロのままです。general-purposeExploreが全呼び出しの95%を占めています。

手順3:agents-index.shでINDEX.mdを自動再生成

削除可否を判断するには、各エージェントが何のために書かれたかを横断的に把握する必要があります。agents-index.sh~/.claude/agents/*.mdのfrontmatterを読み、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()

全画面表示 全画面表示を終了

生成されたINDEX.mdは以下のようなテーブルです。

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

全画面表示 全画面表示を終了

--jsonフラグを付けると.index.jsonも出力され、コストトラッカーなどからプログラム的に再利用できます。

frontmatterにnameまたはdescriptionが欠けているファイルは、⚠️ Validation warningsセクションに一覧表示されます。

手順4:launchdで日次レポートを生成

~/Library/LaunchAgents/com.shun.agent-usage-daily.plist は毎日10:15に実行されます。

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

全画面表示 全画面表示を終了

実行されるコマンドはシンプルです。

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

全画面表示 全画面表示を終了

launchdのデフォルトPATHではpython3が見つからないため、PATHにnvmとHomebrewを明示的に含めています。

これにより、agent-usage-latest.mdは毎朝10:15に更新され、「今日時点で何個のゾンビがいるか」を常に把握できます。

運用ルーチン:ゾンビの駆除

週に一度、以下のコマンドを実行します。

# INDEX再生成(frontmatter変更・追加後に必ず走らせる)
~/.claude/scripts/agents-index.sh

# 0回リスト確認
cat ~/.claude/logs/agent-usage-latest.md | grep -A 9999 "0-call agents"

全画面表示 全画面表示を終了

判断基準は以下の通りです。

状態 対応
30日間で0回呼び出され、説明を読んでも現実的なユースケースがない 削除
30日間で0回だが、将来のユースケースがある 保持し、説明に使用条件を明記
7日間で0回だが、30日間で数回呼び出されている 季節的なジョブの可能性 — 保留
Top5の常連 権限やツール定義を見直してさらに洗練

今回の整理で、30日間で0回が確認された14個のエージェント(a11y-architectdjango-reviewergo-build-resolverなど)を削除しました。これらはINDEX.mdから単純に消えるだけで、他の変更はありません。

遭遇した落とし穴

  • name="Task"でフィルタしても何も一致しなかった → Claude CodeのtranscriptではTaskの呼び出しはname="Agent"として記録される。最初はこれで全レコードが空になった
  • Stopフックが1セッションで複数回発火し、二重書き込みが発生した(session_id, tool_use_id)で重複チェックするseen_idsを追加して修正
  • 空文字のsubagent_typeが紛れ込んだif r.get("subagent_type")でフィルタ(空文字はfalsyなので十分)
  • agents-index.shがINDEX.md自身をパースしてループしたif p.name == "INDEX.md": continueで除外
  • launchdの最小限PATHでpython3が見つからなかった → plistのEnvironmentVariablesにnvmとHomebrewのパスを明示的に設定
  • 30日ウィンドウで0回のエージェントが7日ウィンドウでは「存在しない」ように見えたknown_agentsagents_dirのファイル一覧から構築するため、ウィンドウに依存しない。これは正しい動作

まとめ

  • Stopフックはsession_id + tool_use_idをキーとしてJSONLに追記 → セッションを跨いで呼び出し履歴が蓄積される
  • agent-usage-summary.shは7日/30日ウィンドウでTop10と0回エージェント一覧を出力。~/.claude/agents/*.mdのファイル名と照合するため、新規追加されたエージェントも自動的に追跡される
  • agents-index.shはfrontmatterからINDEX.mdを自動再生成。手書きのindexはすぐに古くなるため、これで置き換えられる
  • launchdは毎日10:15に集計を実行し、agent-usage-latest.mdに書き込むため、「今日時点で何個のゾンビがいるか」に常に答えられる
  • 30日間で0回が確認されたエージェントは削除される。数字が根拠になるため、迷うことはない

次回は、このログデータを用いて、どのエージェントがどのような作業で組み合わせられているかを可視化する予定です。


Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*