A live status line showing how much of your session's tokens go to subagents, and which agent type uses the most.

A live status line showing how much of your session's tokens go to subagents, and which agent type uses the most.
Part of Naren's Claude Toolkit.
One line under the prompt, refreshed after every turn once a subagent has run:
subagents 14x · 12% of output · 54% of all tokens · top Explore
14x: how many subagents (and in-process teammates) have finished a turn this session.12% of output: the share of output tokens that subagents produced, out of main thread plus subagents.54% of all tokens: the same share counting every token processed (fresh input, cache reads, cache writes and output).top Explore: the agent type that used the most tokens.Read the two shares together. When "all tokens" is far above "output", the subagents are mostly re-reading context, not producing work: that gap is the context tax. The line disappears when no subagent has run, and resets on /clear and /resume.
It is tokens only: no prices, no history, and it never reads old transcripts. For cost after the fact, per agent type, with suggestions for cheaper models, use the subagent-tax-auditor skill. The mod only watches: it never changes, delays or blocks a turn.
This is a mod, so it loads from a folder rather than from the skills directory:
claude --plugin-dir plugins/subagent-meter
Then ask Claude to run a few subagents (for example "use three Explore agents to look at three folders") and watch the line appear. The counts live in session state, so they survive a reload of the mod.
Marketplace install:
/plugin marketplace add NarenDawar/narens-claude-toolkit
/plugin install subagent-meter@narens-claude-toolkit
The logic is unit-tested and the hooks are tested with the mod test kit:
claude plugin test plugins/subagent-meter
claude plugin validate plugins/subagent-meter
These run locally. The repository's CI runs on a runner without the Claude CLI, so it checks the manifest and the module path but does not run these tests yet.
unknown.Made by Naren. If this helped, star the repo.
hooks/register.ts 60 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { addTurn, emptyStats, statusLine } from './meter'
5
6const stats = atom({ plugin: 'subagent-meter', key: 'stats' } as const, emptyStats())
7
8export const register: Register = on => {
9 on('turn.complete', async ($, e, next) => {
10 const result = await next(e)
11
12 try {
13 const { usage, agentId } = e
14 if (usage !== undefined) {
15 let type: string | undefined
16 if (agentId !== undefined && (await read($, stats)).agents[agentId] === undefined) {
17 try {
18 type = (await $.agent.list()).find(agent => agent.id === agentId)?.type
19 } catch {
20 type = undefined
21 }
22 }
23 await update($, stats, held => addTurn(held, { agentId, type, usage }))
24 $.ui.status(statusLine(await read($, stats)))
25 }
26 } catch {
27 // The meter only watches; a failure here must never reach the turn.
28 }
29
30 return result
31 })
32
33 on('session.start', async ($, e, next) => {
34 const result = await next(e)
35
36 try {
37 $.ui.status(statusLine(await read($, stats)))
38 } catch {
39 // Same rule: never break the session.
40 }
41
42 return result
43 })
44
45 on('session.end', async ($, e, next) => {
46 // /clear and /resume both end the conversation while the process goes on under another
47 // session, and the counts in session state would otherwise carry over into it.
48 if (e.reason === 'clear' || e.reason === 'resume') {
49 try {
50 await update($, stats, () => emptyStats())
51 $.ui.status(undefined)
52 } catch {
53 // Same rule: never break the session.
54 }
55 }
56
57 return next(e)
58 })
59}
60hooks/meter.ts 98 lines1import type { MeterStats } from '../types'
2
3export type Usage = {
4 input_tokens?: number
5 output_tokens?: number
6 cache_read_input_tokens?: number
7 cache_creation_input_tokens?: number
8}
9
10export type Turn = { agentId?: string; type?: string; usage?: Usage }
11
12export type Summary = {
13 spawns: number
14 outputShare: number
15 allShare: number
16 topType: string
17}
18
19const MAX_TYPE_LENGTH = 16
20
21const count = (n: unknown): number =>
22 typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : 0
23
24const share = (part: number, whole: number): number => (whole > 0 ? part / whole : 0)
25
26const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
27
28const percent = (ratio: number): string => {
29 const value = ratio * 100
30 return value > 0 && value < 1 ? '<1%' : `${Math.round(value)}%`
31}
32
33export const emptyStats = (): MeterStats => ({ main: { output: 0, all: 0 }, agents: {} })
34
35export const tokensOf = (usage: Usage): { output: number; all: number } => {
36 const output = count(usage.output_tokens)
37 const all =
38 count(usage.input_tokens) +
39 count(usage.cache_read_input_tokens) +
40 count(usage.cache_creation_input_tokens) +
41 output
42 return { output, all }
43}
44
45export const addTurn = (stats: MeterStats, turn: Turn): MeterStats => {
46 if (turn.usage === undefined) {
47 return stats
48 }
49 const { output, all } = tokensOf(turn.usage)
50 if (turn.agentId === undefined) {
51 return {
52 ...stats,
53 main: { output: stats.main.output + output, all: stats.main.all + all },
54 }
55 }
56 const held = stats.agents[turn.agentId] ?? { type: turn.type ?? 'unknown', output: 0, all: 0 }
57 return {
58 ...stats,
59 agents: {
60 ...stats.agents,
61 [turn.agentId]: { type: held.type, output: held.output + output, all: held.all + all },
62 },
63 }
64}
65
66export const summarize = (stats: MeterStats): Summary | undefined => {
67 const agents = Object.values(stats.agents)
68 if (agents.length === 0) {
69 return undefined
70 }
71 const sum = agents.reduce(
72 (total, agent) => ({ output: total.output + agent.output, all: total.all + agent.all }),
73 { output: 0, all: 0 },
74 )
75 const perType = new Map<string, number>()
76 for (const agent of agents) {
77 perType.set(agent.type, (perType.get(agent.type) ?? 0) + agent.all)
78 }
79 const [topType] = [...perType.entries()].sort(
80 (a, b) => b[1] - a[1] || compare(a[0], b[0]),
81 )[0]
82 return {
83 spawns: agents.length,
84 outputShare: share(sum.output, sum.output + stats.main.output),
85 allShare: share(sum.all, sum.all + stats.main.all),
86 topType,
87 }
88}
89
90export const formatLine = (summary: Summary): string =>
91 `subagents ${summary.spawns}x · ${percent(summary.outputShare)} of output · ` +
92 `${percent(summary.allShare)} of all tokens · top ${summary.topType.slice(0, MAX_TYPE_LENGTH)}`
93
94export const statusLine = (stats: MeterStats): string | undefined => {
95 const summary = summarize(stats)
96 return summary === undefined ? undefined : formatLine(summary)
97}
98types/index.d.ts 13 lines1export type MeterAgent = { type: string; output: number; all: number }
2
3export type MeterStats = {
4 main: { output: number; all: number }
5 agents: Record<string, MeterAgent>
6}
7
8declare module 'claude-code' {
9 interface PluginState {
10 'subagent-meter': { stats: MeterStats }
11 }
12}
13