Posted on Jul 23 • Originally published at dev.to
Claude Code's /usage tells you how much you're spending, but not what you're spending it on. In my previous post, "Logging Agent invocations to a file and piping them to Slack," I wrote about recording tool calls. This time is a follow-up: a script that parses the already-recorded transcript.jsonl directly and tallies how many times each Skill, each Agent, and each MCP server was called.
Claude Code's /usage shows cost ($) and remaining quota, but it can't show you "which Skill did I use most this week" or "which Agent is the heaviest." Every tool_use block is preserved in transcript.jsonl, so by reading it directly you can surface usage at a layer /usage never tells you about.
The problem: /usage only shows cost
What /usage reports is the consumption rate for 5-hour and 7-day blocks, plus per-session token counts. It does not give you a per-item breakdown like "this AutoTrigger Skill was actually called 20 times this week" or "the general-purpose Agent accounts for half of all Agent calls."
To do cost optimization or check the health of your environment, you need the line items for "what are the tokens being spent on." transcript.jsonl is the ideal source for this, and every session is accumulated under ~/.claude/projects/.
The structure of transcript.jsonl
Each file is JSONL, one record per line. Each turn Claude Code sent or received becomes one record.
{
"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 }
}
}
Enter fullscreen mode Exit fullscreen mode
All you need for the tally is the type == "tool_use" blocks inside message.content[]. The name field is the tool name, and input holds the arguments.
| Tool | Tally key | Extracted from |
|---|---|---|
Skill |
input.skill |
Skill name (also namespace-split for the plugin:name form) |
Agent |
input.subagent_type |
Subagent type |
mcp__* |
2nd segment of name
|
MCP server name (mcp__<server>__<tool>) |
The heart of the script
~/.claude/scripts/usage-breakdown.sh embeds Python via a Bash heredoc. Bash receives the directory and the window period, and Python does a full scan of the files.
#!/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"
Enter fullscreen mode Exit fullscreen mode
The window period is filtered by the file's mtime. When the --short flag comes in, the parsing is all the same; only the output is narrowed down to a single line.
The parsing itself is simple:
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
Enter fullscreen mode Exit fullscreen mode
For the plugin:name form of Skill (e.g. hookify:configure), I also split on : and tally the namespace separately. This lets me see which plugin package is heavy on a different axis from the individual skill names.
Running it for real
Running it for the last 7 days looks like this.
$ 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
Enter fullscreen mode Exit fullscreen mode
This week had 51 sessions and 4,230 tool_use calls, with Bash accounting for 61% of the total (2,583 calls). For MCP, claude-in-chrome was in a league of its own at 261 calls. For Agents, general-purpose was the most frequent at 20, with Explore at 7.
The reason there are only 2 Skill calls total is that this week I had a lot of setups where Skills fire via AutoTrigger (keyword matching in CLAUDE.md) rather than being invoked manually. AutoTrigger should show up in the transcript as Skill tool_use, so the low count is honestly reflected.
:::message
There are 4 Agents where subagent_type comes out as "?". These are calls that didn't specify subagent_type on the Agent tool (effectively the default general-purpose), which the script falls back to via inp.get("subagent_type", "?"). In practice, when "?" starts increasing, narrowing down "which Session" is useful for discovering missing subagent specifications.
:::
The --short mode for embedding in the status line
--short outputs a one-line summary.
$ usage-breakdown.sh --short
4235 tool_use across 51 sessions (7d)
Enter fullscreen mode Exit fullscreen mode
I've built it into a custom script for Claude Code's status line. Placing it alongside the cost quota (the output of the previous post's token-budget-advisor.sh) puts you in a state where "usage and remaining quota are visible at a glance."
# an example statusline hook
BUDGET=$(~/.claude/scripts/token-budget-advisor.sh --short)
USAGE=$(~/.claude/scripts/usage-breakdown.sh --short)
echo "💰 $BUDGET | 🔧 $USAGE"
Enter fullscreen mode Exit fullscreen mode
Pitfalls I hit
-
The
mtimefilter is file-granular, so it's coarse — when a session spans multiple days, older turns get included in the tally as part of a "recent file." To be strict you'd need line-level filtering viarec.get("timestamp"), but for grasping trends,mtimewas good enough. -
It doesn't pick up JSONL in subdirectories —
glob.glob(f"{tr_dir}/*.jsonl")only looks one flat level. Subagent transcripts go under<session-uuid>/subagents/agent-*.jsonl, so they're outside the tally in the current implementation. Switching to**/*.jsonlwould pick them up, but it's heavy, so I've left it out intentionally. -
Long MCP tool names like
mcp__claude-in-chrome__computer— since the implementation takes the 2nd segment ofsplit("__"), onlyparts[1]remains and the server name is extracted correctly. However, in cases where the server name itself contains__, it will be misextracted (haven't run into it so far). -
The 30d window takes several minutes — because it full-scans over 890 files, using
7dor--shortfor daily use and only reaching for30dwhen digging deep is the realistic approach.
Summary
- If you pick up just the
message.content[].type == "tool_use"fromtranscript.jsonl, you can get the call counts for Skills, Agents, and MCP servers - Just outputting
most_common(10)from a Python counter immediately shows you the "heavy items" - The
mtimeperiod filter and the embedded Python heredoc fit it all into a single Bash script - Making it a one-line summary with
--shortand always displaying it in the status line lets you monitor the health of your environment on an axis separate from cost
Look at cost with /usage, and look at the per-item breakdown with this script. Combining the two raises the resolution of "how many tokens were spent for what."
Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code.
Follow along: Portfolio · X · GitHub*
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.