📊 transcript.jsonlを直接パースして「何が呼ばれているか」を可視化する — リーダー×
📊

transcript.jsonlを直接パースして「何が呼ばれているか」を可視化する

#claudecode#自動化#個人開発#ai2026-07-27 · 約8

前作「Agent呼び出しをファイルに記録してSlackに流す」でツール呼び出しの記録について書きました。今回はその延長で、記録済みのtranscript.jsonlを直接パースして「どのSkillが何回・どのAgentが何回・どのMCPサーバーが何回呼ばれたか」を集計するスクリプトの話です。

Claude Codeの /usage はコスト($)と残量を出しますが、「今週どのSkillを一番使ったか」「どのAgentが重いか」は見えません。transcript.jsonlには全ての tool_use ブロックが残っているので、それを直接読めば /usage が教えてくれないレイヤーの使用状況を出せます。

困りごと:/usage はコストしか出さない

/usage が出すのは5hブロック・7日ブロックの消費率と、セッション単位のトークン数です。「このAutoTriggerのSkillが実は週20回呼ばれていた」「general-purpose Agentが全Agentの半数を占めている」といった品目別の内訳は出ません。

コスト最適化や環境の健全性チェックをするには、「何にトークンが使われているか」の品目が必要です。transcript.jsonlはその情報源として最適で、~/.claude/projects/ 以下に全セッション分が蓄積されています。

transcript.jsonl の構造

各ファイルは1行1レコードのJSONLです。Claude Codeが送受信した各ターンが1レコードになります。

{
  "type": "assistant",
  "uuid": "...",
  "timestamp": "...",
  "sessionId": "...",
  "message": {
    "model": "claude-sonnet-4-6",
    "role": "assistant",
    "content": [
      {
        "type": "tool_use",
        "id": "toolu_01M5Rw...",
        "name": "Read",
        "input": { "file_path": "~/..." },
        "caller": { "type": "direct" }
      }
    ],
    "usage": { "input_tokens": 12043, "output_tokens": 421 }
  }
}

集計に必要なのは message.content[]type == "tool_use" ブロックだけです。name フィールドがツール名、input に引数が入っています。

ツール集計のキー抽出先
Skillinput.skillスキル名(plugin:name 形式でnamespace分離も)
Agentinput.subagent_typeサブエージェント種別
mcp__*name の第2セグメントMCPサーバー名(mcp__<server>__<tool>

スクリプトの核心

~/.claude/scripts/usage-breakdown.sh はBashのヒアドキュメントでPythonを埋め込んでいます。Bashからディレクトリとウィンドウ期間を受け取り、Pythonがファイルをフルスキャンします。

#!/usr/bin/env bash
ARG="${1:-7d}"
TR_DIR="$HOME/.claude/projects/-Users-matsubara"
[ -d "$TR_DIR" ] || { echo "(no transcript dir)"; exit 0; }

python3 - "$TR_DIR" "$ARG" <<'PY'
import sys, json, datetime, glob, collections, os
tr_dir, arg = sys.argv[1], sys.argv[2]
SHORT = arg == "--short"

ウィンドウ期間はファイルの mtime でフィルタします。--short フラグが来たらParse処理は全て同じですが出力だけ1行に絞ります。

パース本体はシンプルです:

for path in glob.glob(f"{tr_dir}/*.jsonl"):
    mtime = os.path.getmtime(path)
    if mtime < cutoff_ts: continue
    with open(path, "r", encoding="utf-8", errors="replace") as f:
        for line in f:
            rec = json.loads(line)
            msg = rec.get("message", {}) if isinstance(rec.get("message"), dict) else {}
            content = msg.get("content") if isinstance(msg, dict) else None
            if not isinstance(content, list): continue
            for block in content:
                if block.get("type") != "tool_use": continue
                name = block.get("name", "")
                inp = block.get("input") or {}
                tool_calls[name] += 1
                if name == "Skill":
                    skill_name = inp.get("skill", "?")
                    skill_calls[skill_name] += 1
                elif name == "Agent":
                    agent_calls[inp.get("subagent_type", "?")] += 1
                elif name.startswith("mcp__"):
                    parts = name.split("__")
                    if len(parts) >= 2:
                        mcp_calls[parts[1]] += 1

Skillの plugin:name 形式(例: hookify:configure)は : でsplitしてnamespace集計も別途やっています。これでどのpluginパッケージが重いか、個別スキル名とは別軸で見られます。

実際に走らせてみる

直近7日で走らせると、こう出ます。

$ usage-breakdown.sh 7d
=== usage breakdown (last 7d, 51 transcripts) ===

total tool_use: 4230

--- top tools ---
   2583  Bash
    567  Read
    402  Edit
    151  Write
    103  mcp__claude-in-chrome__computer
     73  mcp__claude-in-chrome__javascript_tool
     62  TaskUpdate
     50  ToolSearch
     42  mcp__claude-in-chrome__navigate
     36  TaskCreate

--- top skills (2 unique) ---
      1  harness-audit
      1  superpowers:brainstorming

--- top agents (5 unique) ---
     20  general-purpose
      7  Explore
      4  ?
      2  reviewer
      1  fork

--- top MCP servers (4 unique) ---
    261  claude-in-chrome
      3  claude_ai_Google_Calendar
      3  computer-use
      2  claude_ai_Gmail

--- top plugin namespaces (1 unique) ---
      1  superpowers

この週は51セッション・4,230 tool_useで、Bashが全体の61%(2,583回)を占めていました。MCPは claude-in-chrome が群を抜いていて261回。Agentは general-purpose が最多で20回、Exploreが7回でした。

Skill呼び出しが合計2回しかないのは、この週はSkillをほとんど手動で呼ばずにAutoTrigger(CLAUDE.mdのキーワード一致)で動く設定が多かったためです。AutoTriggerはトランスクリプトには Skill tool_useとして現れるはずなので、少なさは正直に出ています。

subagent_type"?" になっているAgentが4回あります。これは Agent ツールに subagent_type を指定しなかった(デフォルトのgeneral-purpose相当)呼び出しで、スクリプトが inp.get("subagent_type", "?") でフォールバックしている分です。実運用では "?" が増えてきたら「どのSessionか」を絞り込むと、サブエージェント指定漏れの発見に使えます。

--short モードでステータスラインに埋める

--short で1行サマリが出ます。

$ usage-breakdown.sh --short
4235 tool_use across 51 sessions (7d)

Claude Codeのステータスラインのカスタムスクリプトに組み込んでいます。コスト枠(前作の token-budget-advisor.sh 出力)と並べると「使用量と残量が一目で見える」状態になります。

# statusline hook の一例
BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)
USAGE=$(~/.claude/scripts/usage-breakdown.sh --short)
echo "💰 $BUDGET | 🔧 $USAGE"

踏んだ落とし穴

  • mtime フィルタはファイル単位なので精度が荒い — セッションが日をまたぐと、古いターンも「最近のファイル」として集計に入ります。厳密にやるなら rec.get("timestamp") で行レベルフィルタが必要ですが、傾向把握レベルなら mtime で十分でした。
  • サブディレクトリのJSONLを拾わないglob.glob(f"{tr_dir}/*.jsonl") はフラットな1階層しか見ません。サブエージェントのtranscriptは <session-uuid>/subagents/agent-*.jsonl に入るため、現行実装では集計外です。**/*.jsonl に変えると拾えますが、重いので意図的に外しています。
  • mcp__claude-in-chrome__computer のような長いMCPツール名split("__") の第2セグメントを取る実装なので、parts[1] だけが残りサーバー名は正しく抽出できます。ただしサーバー名自体に __ が入るケースでは誤抽出します(今のところ遭遇なし)。
  • 30d ウィンドウは数分かかる — 890ファイル超をフルスキャンするため、7d または --short を日常使いにして、深掘り時だけ 30d にするのが現実的です。

まとめ

  • transcript.jsonlmessage.content[].type == "tool_use" だけを拾えば、Skill・Agent・MCPサーバーの呼び出し回数が取れる
  • Pythonのカウンタで most_common(10) を出すだけで「重い品目」がすぐ分かる
  • mtime 期間フィルタとPythonのヒアドキュメント埋め込みでBashスクリプト1本に収まる
  • --short で1行サマリにしてステータスラインに常時表示すると、環境の健全性をコストとは別軸で監視できる

/usage でコストを見て、このスクリプトで品目の内訳を見る。2つを合わせると「何のために何トークン使ったか」の解像度が上がります。


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

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