SLOPSHOPPER

Token Usage Line: per-response & session tokens

Token usage per response and per session, cost and context window, on one line above the Claude Code prompt.

newbandstatus
v0.3.0MITupdated 2026-10-07mkiselyow/claude-code-token-usage
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · token-usage-line
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ✻ Last in 97.4k (93% cached) out 1.5k │ Session in 97.4k (93% cached) out 1.5k │ ~$0.42 │ context 49% ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
✻ Last in 97.4k (93% cached) out 1.5k │ Session in 97.4k (93% cached) out 1.5k │ ~$0.42 │ context 49 ⟨Claude Code's own drawing⟩
README

Token Usage Line

A Claude Code mod that draws one line above the prompt:

Token usage line after a response

✻ Last in 38.7k (60% cached) out 4 │ Session in 38.7k (60% cached) out 4 │ ~$0.13 │ context 4%
  • Last: tokens of the most recent response, including the subagents it started.
  • Session: running total since the session started (/clear resets it).
  • ~$: the session's cost so far, Claude Code's own estimate (the same figure /cost shows), from list prices; not what a Pro/Max plan bills.
  • context: how full the context window is.
  • % cached: share of input tokens served from the prompt cache, which are billed far cheaper.

Until the first response of a session finishes, the line says so instead of showing zeros:

Token usage line in a new session

Colours come from the active Claude Code theme, so it follows light and dark mode.

Install

Requires Claude Code 2.1.292 or later (it relies on the mods API; on 2.1.278 the line fails to draw). Check with claude --version, update with claude update.

claude plugin marketplace add mkiselyow/claude-code-token-usage
claude plugin install token-usage-line@token-usage-line

Start a new session and the line appears above the prompt.

Or try it for one session without installing:

git clone https://github.com/mkiselyow/claude-code-token-usage
claude --plugin-dir ./claude-code-token-usage

Update

claude plugin marketplace update token-usage-line
claude plugin update token-usage-line@token-usage-line

Uninstall

claude plugin uninstall token-usage-line@token-usage-line
claude plugin marketplace remove token-usage-line
Source 2 files
hooks/register.tsx 122 lines
1import type { Register, TurnUsage } from 'claude-code'
2
3import type { Tokens } from '../types'
4
5const ZERO: Tokens = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 }
6
7const LAST = { plugin: 'token-usage-line', key: 'last' } as const
8const TOTAL = { plugin: 'token-usage-line', key: 'total' } as const
9const PENDING = { plugin: 'token-usage-line', key: 'pending' } as const
10
11const ACCENT = 'claude'
12const LABEL = 'inactive'
13const VALUE = 'text'
14
15const fromUsage = (u: TurnUsage): Tokens => ({
16  input: u.input_tokens,
17  cacheRead: u.cache_read_input_tokens,
18  cacheWrite: u.cache_creation_input_tokens,
19  output: u.output_tokens,
20})
21
22const add = (a: Tokens, b: Tokens): Tokens => ({
23  input: a.input + b.input,
24  cacheRead: a.cacheRead + b.cacheRead,
25  cacheWrite: a.cacheWrite + b.cacheWrite,
26  output: a.output + b.output,
27})
28
29const k = (n: number) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : `${n}`)
30
31const inputOf = (t: Tokens) => t.input + t.cacheRead + t.cacheWrite
32
33const cachedPercent = (t: Tokens) => (inputOf(t) === 0 ? 0 : Math.round((t.cacheRead / inputOf(t)) * 100))
34
35export const register: Register = on => {
36  on('session.start', async ($, e, next) => {
37    $.ui.status(undefined)
38    return next(e)
39  })
40
41  on('session.end', async ($, e, next) => {
42    if (e.reason === 'clear') {
43      await $.state.set(LAST, null)
44      await $.state.set(PENDING, ZERO)
45      await $.state.set(TOTAL, ZERO)
46    }
47    return next(e)
48  })
49
50  on('turn.complete', async ($, e, next) => {
51    const result = await next(e)
52    const used = e.usage ? fromUsage(e.usage) : ZERO
53
54    const { value: total = ZERO } = await $.state.get(TOTAL)
55    await $.state.set(TOTAL, add(total, used))
56
57    const { value: subagents = ZERO } = await $.state.get(PENDING)
58    if (e.agentId !== undefined) {
59      await $.state.set(PENDING, add(subagents, used))
60      return result
61    }
62
63    await $.state.set(LAST, add(used, subagents))
64    await $.state.set(PENDING, ZERO)
65    return result
66  })
67
68  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
69    const below = await next(e)
70    if (e.props.hasSurvey) return below
71
72    const [{ value: last = null }, { value: total = ZERO }, usage] = await Promise.all([
73      $.state.get(LAST),
74      $.state.get(TOTAL),
75      $.session.usage(),
76    ])
77    const { Box, Text } = $.ui.resolve(e)
78
79    const pair = (label: string, value: string) => (
80      <Box flexDirection="row" gap={1}>
81        <Text color={LABEL}>{label}</Text>
82        <Text color={VALUE} bold>
83          {value}
84        </Text>
85      </Box>
86    )
87    const group = (title: string, t: Tokens) => (
88      <Box flexDirection="row" gap={1}>
89        <Text color={ACCENT} bold>
90          {title}
91        </Text>
92        {pair('in', k(inputOf(t)))}
93        {inputOf(t) > 0 ? <Text color={LABEL}>{`(${cachedPercent(t)}% cached)`}</Text> : null}
94        {pair('out', k(t.output))}
95      </Box>
96    )
97
98    const empty = inputOf(total) + total.output === 0
99    const usd = usage.cost?.usd ?? 0
100    const contextPercent = Math.round(usage.context.percent ?? 0)
101
102    const sections = empty
103      ? [<Text color={LABEL}>tokens: waiting for the first response</Text>]
104      : [
105          last ? group('Last', last) : null,
106          group('Session', total),
107          usd > 0 ? <Text color={VALUE} bold>{`~$${usd.toFixed(2)}`}</Text> : null,
108          contextPercent > 0 ? pair('context', `${contextPercent}%`) : null,
109        ].filter(section => section !== null)
110
111    return (
112      <Box flexDirection="column">
113        <Box flexDirection="row" flexWrap="nowrap" gap={1}>
114          <Text color={ACCENT}>✻</Text>
115          {sections.flatMap((section, i) => (i === 0 ? [section] : [<Text color={LABEL}>│</Text>, section]))}
116        </Box>
117        {below}
118      </Box>
119    )
120  })
121}
122
types/index.d.ts 8 lines
1export type Tokens = { input: number; cacheRead: number; cacheWrite: number; output: number }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'token-usage-line': { last: Tokens | null; pending: Tokens; total: Tokens }
6  }
7}
8