Shows the tokens each Claude turn uses beside the spinner, and adds a /tokens command for the session total

A small Claude Code mod that shows where your tokens go, live.
Thinking · tokens: 6,000…./tokens for the totals since the mod loaded: requests, input, output, cache read and cache write.Checking is free. The mod never calls a model. /tokens runs with no Claude turn and prints a log line that Claude doesn't read.
Made for the CorporateChittyChat Short "Do Claude Code Mods Eat Your Tokens?".
A mod runs with your own permissions. Read hooks/register.js first. It's 66 lines. Then check what it does without running it:
claude plugin validate ./token-meter
The calls: line should list only $.command.register, $.ui.invalidate and $.ui.log. That means no file access, no network and no model calls.
Mods need Claude Code v2.1.287 or later. Check your version with claude --version.
claude --plugin-dir ./token-meter
Ask Claude anything and watch the spinner. When it's done, type /tokens.
The tests need no session, sign-in or network:
cd token-meter
claude plugin test
The counts come from the token usage the Claude API reports for each request. They include subagents' requests, and they reset when the mod reloads. These are not billing figures. For your actual usage, check your plan or the Claude Console.
Tested with Claude Code 2.1.296. Mods are new, and their events can change between releases.
hooks/register.js 67 lines1// token-meter: watch where your tokens go, live.
2// The mod itself never calls a model, and /tokens prints with $.ui.log,
3// a transcript line that Claude doesn't read, so checking costs no tokens.
4
5// Totals since the mod loaded, shared by the hooks below
6let requests = 0
7let input = 0
8let output = 0
9let cacheRead = 0
10let cacheWrite = 0
11// Tokens used by the turn that's running now
12let thisTurn = 0
13
14function fmt(n) {
15 return n.toLocaleString('en-US')
16}
17
18// Claude Code calls this once when the mod loads
19export function register(on) {
20 // Runs when the session starts: add the /tokens command
21 on('session.start', async ($, e, next) => {
22 await $.command.register({
23 name: 'tokens',
24 description: 'Show the tokens used since token-meter loaded',
25 })
26 return next(e)
27 })
28
29 // A new turn starts: reset the per-turn count
30 on('turn.start', async ($, e, next) => {
31 thisTurn = 0
32 $.ui.invalidate('ui.render')
33 return next(e)
34 })
35
36 // Each request to the model: let it stream, then read its token usage
37 on('turn.step', async function* ($, e, next) {
38 const result = yield* next(e)
39 const u = result && result.usage
40 if (u) {
41 requests += 1
42 input += u.input_tokens || 0
43 output += u.output_tokens || 0
44 cacheRead += u.cache_read_input_tokens || 0
45 cacheWrite += u.cache_creation_input_tokens || 0
46 thisTurn += (u.input_tokens || 0) + (u.output_tokens || 0) +
47 (u.cache_read_input_tokens || 0) + (u.cache_creation_input_tokens || 0)
48 $.ui.invalidate('ui.render')
49 }
50 return result
51 })
52
53 // Each time the spinner is drawn: add this turn's count after its word
54 on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
55 return next({ ...e, props: { ...e.props, suffix: ' · tokens: ' + fmt(thisTurn) + '…' } })
56 })
57
58 // /tokens: print the totals as a log line Claude doesn't read, and return nothing
59 on('command.run', { command: 'tokens' }, async ($) => {
60 $.ui.log(
61 fmt(requests) + ' requests · in ' + fmt(input) + ' · out ' + fmt(output) +
62 ' · cache read ' + fmt(cacheRead) + ' · cache write ' + fmt(cacheWrite)
63 )
64 return {}
65 })
66}
67