0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

呼ばれていないエージェントを炙り出す ― Stop hookで使用率ログを取ってゾンビを刈る

0
Posted at

「Claude Code環境」シリーズです。前回のターミナルウィンドウ自動タイリングから引き続き、環境の「肥大化を防ぐ」話をします。

~/.claude/agents/ に定義したサブエージェントが、実際に何回呼ばれているか把握できていますか。私は把握できていませんでした。エージェントを追加し続けた結果、今朝の集計では 30日間で一度も呼び出されていないエージェントが44本あると判明しました。この記事では、Stop hookでサブエージェント呼び出しを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 自動再生成

Step 1:Stop hookでJSONLに記録する

~/.claude/hooks/stop_agent_tracker.sh がセッション終了時に動きます。Stop hookのstdinにはセッション情報と 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 hookが走っても二重書きしない)。

JSOBLの1レコードはこんな形です。

{"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が蓄積されています。

Step 2:7d/30d窓でTop10と0回エージェントを出す

~/.claude/scripts/agent-usage-summary.sh が集計を担います。外部ライブラリなしの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本が0回のままです。general-purposeExplore で全体の95%を占めています。

Step 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 も書き出されます。cost-tracker等でプログラマブルに再利用できます。

frontmatter に namedescription が欠けているファイルは ⚠️ Validation warnings セクションに列挙されます。

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

PATHにnvmとHomebrewを明示しているのは、launchdのデフォルトPATHだと python3 が素通りするためです。

これで毎朝10:15に agent-usage-latest.md が更新され、「30日0回のエージェントは何本か」が常に分かります。

運用フロー:ゾンビを刈る

週に一度、このコマンドを叩きます。

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

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

判断基準は次のようにしています。

状態 対応
30d 0回・description を読んで使う想定がない 削除
30d 0回・将来使う想定がある description に使用条件を明記して残す
7d 0回・30d 数回 季節ジョブの可能性あり・保留
Top5常連 権限・ツール定義を見直してさらに研ぎ澄ます

今回の棚卸しで a11y-architectdjango-reviewergo-build-resolver など30日0回が確定した14本を削除しました。INDEX.mdから消えるだけで他は何も変わりません。

踏んだ落とし穴

  • name="Task" でフィルタしてもヒットしない → Claude CodeのtranscriptではTask呼び出しが name="Agent" として記録される。最初これで全レコードが空だった
  • 同一セッションで複数回Stop hookが走り二重書きが発生seen_ids(session_id, tool_use_id) の重複チェックを入れて解消
  • 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パスを明示
  • 30d窓で0回のエージェントが7d窓では「存在しない」扱いになるknown_agentsagents_dir のファイル一覧から取るので窓に依存しない。正しい動作

まとめ

  • Stop hookがsession_id + tool_use_idの組でJSONLに追記 → セッションまたぎで呼び出し履歴が蓄積される
  • agent-usage-summary.sh は7d/30d窓でTop10と0回エージェント一覧を出す。~/.claude/agents/*.md のファイル名と突き合わせるので、新しいエージェントを追加すれば自動的に追跡対象に入る
  • agents-index.sh がfrontmatterからINDEX.mdを自動再生成。手書きINDEXは即腐るのでこれで代替する
  • launchd が毎日10:15に集計を走らせて agent-usage-latest.md に書き出す。「今日の時点で何本ゾンビか」が常に分かる
  • 30日0回が確定したエージェントは削除する。削除の根拠が数値で取れると迷わない

次回は、このログデータをもとに「どの作業でどのエージェントを組み合わせているか」のパターンを可視化した話を書きます。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?