Weekly cap, context fill and each subagent's call count above the prompt, with a warning when an agent runs long.

fleet-gauge is a Claude Code mod that warns when usage needs a decision. It needs Claude Code 2.1.287 or later. Cursor and Codex get a smaller version, described at the end.
A row appears above the prompt only when one of these is true:
A toast fires when a subagent reaches 150 tool calls, and again at 300. An agent rereads its whole context on every call, so long agents cost the most.
/fleet prints every reading and lists the session's subagents by tool calls made.
For one session:
claude --plugin-dir ~/Developer/GitHub/skill-issue/fleet-gauge
For every session, including the desktop app, add the path to ~/.claude/settings.json:
{
"env": {
"CLAUDE_CODE_PLUGIN_DIRS": "~/Developer/GitHub/skill-issue/fleet-gauge"
}
}
Start a new session after changing it.
They are constants at the top of the hooks module. Run claude plugin test . in this folder after an edit.
Neither agent lets a mod draw in its interface, so they get the parts that can be rebuilt.
python3 other-agents/install.py
FLEET_GAUGE_CALLS to change the limit.Codex skips a new hook until you trust it. Open Codex, type /hooks, and press t.
python3 other-agents/install.py --remove takes it out.
hooks/register.tsx 198 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { AgentRow, Gauge } from '../types'
5
6// An agent rereads its whole context on every call, so cost grows with the
7// square of its length. In the week of 2026-09-25 the agents past 150 calls
8// were 72% of the weekly cap, and the ones past 300 were 36%.
9const LONG = 150
10const VERY_LONG = 300
11const KEEP = 80
12
13const agents = atom({ plugin: 'fleet-gauge', key: 'agents' } as const, {})
14const gauge = atom({ plugin: 'fleet-gauge', key: 'gauge' } as const, null)
15
16const tint = (value: number, warn: number, stop: number) =>
17 value >= stop ? 'red' : value >= warn ? 'yellow' : undefined
18
19// The band is silent until a number needs a decision. /fleet prints them all.
20const SHOW_AT: Record<string, number> = { seven_day: 50, five_hour: 70 }
21const CONTEXT_AT = 200_000
22
23const thousands = (tokens: number) =>
24 tokens >= 1_000_000
25 ? `${(tokens / 1_000_000).toFixed(tokens % 1_000_000 === 0 ? 0 : 1)}M`
26 : `${Math.round(tokens / 1000)}k`
27
28function gaugeLine(now: Gauge | null): string {
29 if (now === null) {
30 return 'No usage reading yet.'
31 }
32
33 const parts = now.limits.map(l => `${windowName(l.kind)} ${Math.round(l.percentUsed)}%`)
34
35 if (now.tokens !== undefined) {
36 parts.push(`context ${thousands(now.tokens)} of ${thousands(now.window)}`)
37 }
38
39 return parts.join(', ')
40}
41
42const windowName = (kind: string) =>
43 kind === 'seven_day' ? 'week' : kind === 'five_hour' ? '5h' : kind
44
45function trimmed(rows: Record<string, AgentRow>): Record<string, AgentRow> {
46 const entries = Object.entries(rows)
47
48 if (entries.length <= KEEP) {
49 return rows
50 }
51
52 return Object.fromEntries(
53 entries.sort((a, b) => b[1].lastAt - a[1].lastAt).slice(0, KEEP),
54 )
55}
56
57export const register: Register = on => {
58 on('session.start', async ($, e, next) => {
59 await $.command.register({
60 name: 'fleet',
61 description: "List this session's subagents by tool calls made",
62 })
63
64 return next(e)
65 })
66
67 on('session.measure', async ($, e, next) => {
68 const now: Gauge = {
69 tokens: e.context.tokens,
70 window: e.context.window,
71 limits: e.rateLimits.map(({ kind, percentUsed, resetsAt }) => ({
72 kind,
73 percentUsed,
74 resetsAt,
75 })),
76 }
77 await update($, gauge, () => now)
78
79 return next(e)
80 })
81
82 on('agent.spawn', async ($, e, next) => {
83 const started = await next(e)
84 const id = 'agentId' in started ? started.agentId : undefined
85
86 if (id !== undefined) {
87 const at = await $.clock.now()
88 await update($, agents, rows => ({
89 ...rows,
90 [id]: {
91 calls: 0,
92 isDone: false,
93 ...rows[id],
94 label: e.description || e.subagentType,
95 lastAt: at,
96 },
97 }))
98 }
99
100 return started
101 })
102
103 on('tool.call', async ($, e, next) => {
104 const id = e.agentId
105
106 if (id === undefined) {
107 return next(e)
108 }
109
110 const at = await $.clock.now()
111 let row: AgentRow = { label: id.slice(0, 8), calls: 0, lastAt: at, isDone: false }
112 await update($, agents, rows => {
113 row = { ...row, ...rows[id], lastAt: at, isDone: false }
114 row = { ...row, calls: row.calls + 1 }
115
116 return trimmed({ ...rows, [id]: row })
117 })
118
119 if (row.calls === LONG || row.calls === VERY_LONG) {
120 $.ui.toast(
121 `${row.label} has made ${row.calls} tool calls. Long agents cost the most. Consider a handoff.`,
122 )
123 }
124
125 return next(e)
126 })
127
128 on('turn.complete', async ($, e, next) => {
129 const id = (e as { agentId?: string }).agentId
130
131 if (id !== undefined) {
132 await update($, agents, rows =>
133 rows[id] ? { ...rows, [id]: { ...rows[id], isDone: true } } : rows,
134 )
135 }
136
137 return next(e)
138 })
139
140 on('command.run', { command: 'fleet' }, async $ => {
141 const rows = Object.values(await read($, agents)).sort((a, b) => b.calls - a.calls)
142 const head = gaugeLine(await read($, gauge))
143
144 if (rows.length === 0) {
145 return { text: `${head}\nNo subagent has made a tool call in this session.` }
146 }
147
148 return {
149 text: [
150 head,
151 ...rows.map(
152 r => `${String(r.calls).padStart(4)} calls ${r.isDone ? 'done ' : 'running'} ${r.label}`,
153 ),
154 ].join('\n'),
155 }
156 })
157
158 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
159 const now = await read($, gauge)
160 const rows = Object.values(await read($, agents))
161
162 const running = rows.filter(r => !r.isDone)
163 const longest = running.reduce<AgentRow | null>(
164 (top, r) => (top === null || r.calls > top.calls ? r : top),
165 null,
166 )
167 const limits = (now?.limits ?? []).filter(l => l.percentUsed >= (SHOW_AT[l.kind] ?? 80))
168 const tokens = now?.tokens !== undefined && now.tokens >= CONTEXT_AT ? now.tokens : undefined
169
170 if (e.props.hasSurvey || (limits.length === 0 && tokens === undefined && longest === null)) {
171 return next(e)
172 }
173
174 const { Box, Text } = $.ui.resolve(e)
175
176 return (
177 <Box>
178 {limits.map(limit => (
179 <Text color={tint(limit.percentUsed, 50, 80)}>
180 {windowName(limit.kind)} {Math.round(limit.percentUsed)}%{' '}
181 </Text>
182 ))}
183 {now !== null && tokens !== undefined && (
184 <Text color={tint(tokens, CONTEXT_AT, 400_000)}>
185 context {thousands(tokens)} of {thousands(now.window)}{' '}
186 </Text>
187 )}
188 {longest !== null && (
189 <Text color={tint(longest.calls, LONG, VERY_LONG)} dimColor={longest.calls < LONG}>
190 {running.length} {running.length === 1 ? 'agent' : 'agents'} running, longest{' '}
191 {longest.calls} calls ({longest.label})
192 </Text>
193 )}
194 </Box>
195 )
196 })
197}
198types/index.d.ts 17 lines1export type AgentRow = {
2 label: string
3 calls: number
4 lastAt: number
5 isDone: boolean
6}
7
8export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
9
10export type Gauge = { tokens?: number; window: number; limits: Limit[] }
11
12declare module 'claude-code' {
13 interface PluginState {
14 'fleet-gauge': { agents: Record<string, AgentRow>; gauge: Gauge | null }
15 }
16}
17