Band showing token usage and cache hit ratio per turn and per session.

A band above the prompt that shows token usage for the last turn and the whole session, including the prompt-cache hit ratio.
/plugin marketplace add namtrdev/claude-mods
/plugin install cost-meter@jackie-mods
Commands: shows after the first reply.
Built for Claude Code mods (hook modules). Nothing here runs outside your machine: the mod reads session events and draws its own interface.
You set these when you install the mod, and can change them later in the Claude Code config menu.
defaultTtl, default 5m): Which prompt-cache lifetime the countdown assumes until you press a TTL button.showCacheClock, default true): Show the second row with the cache clock and the TTL buttons.MIT, Copyright (c) 2026 Jackie.
Icons: Lucide (ISC). See THIRD_PARTY_NOTICES.md.
hooks/register.tsx 117 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { icon } from './icons'
5
6import type { TurnUse } from '../types'
7
8const last = atom({ plugin: 'cost-meter', key: 'last' } as const, null)
9const ttl = atom({ plugin: 'cost-meter', key: 'ttl' } as const, '5m')
10const lastAt = atom({ plugin: 'cost-meter', key: 'lastAt' } as const, 0)
11const total = atom({ plugin: 'cost-meter', key: 'total' } as const, { input: 0, output: 0, read: 0, write: 0 })
12
13const NAME = { '5m': '5 min', '1h': '1 hour' }
14const TTL_MS = { '5m': 5 * 60_000, '1h': 60 * 60_000 }
15const clock = (ms: number) => {
16 const s = Math.max(0, Math.ceil(ms / 1000))
17 return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
18}
19
20const k = (n: number) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))
21const hit = (u: TurnUse) => {
22 const all = u.input + u.read + u.write
23 return all === 0 ? 0 : Math.round((u.read / all) * 100)
24}
25
26export const register: Register = (on, options) => {
27 let timer: { cancel: () => void } | undefined
28
29 on('session.start', async ($, e, next) => {
30 if ((await read($, lastAt)) === 0) {
31 await update($, ttl, () => (options.defaultTtl === '1h' ? '1h' : '5m'))
32 }
33 timer?.cancel()
34 timer = $.clock.every(1000, async () => {
35 const at = await read($, lastAt)
36 if (at === 0 || options.showCacheClock === false) return
37 const left = at + TTL_MS[await read($, ttl)] - (await $.clock.now())
38 $.ui.status(left > 0 ? `cache warm ${clock(left)}` : 'cache cold')
39 $.ui.invalidate('ui.render')
40 })
41
42 return next(e)
43 })
44
45 on('turn.complete', async ($, e, next) => {
46 if (e.usage !== undefined && e.agentId === undefined) {
47 const use: TurnUse = {
48 input: e.usage.input_tokens,
49 output: e.usage.output_tokens,
50 read: e.usage.cache_read_input_tokens,
51 write: e.usage.cache_creation_input_tokens,
52 }
53 await update($, last, () => use)
54 const now = await $.clock.now()
55 await update($, lastAt, () => now)
56 await update($, total, sum => ({
57 input: sum.input + use.input,
58 output: sum.output + use.output,
59 read: sum.read + use.read,
60 write: sum.write + use.write,
61 }))
62 }
63
64 return next(e)
65 })
66
67 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
68 const turn = await read($, last)
69 if (e.props.hasSurvey || turn === null) {
70 return next(e)
71 }
72 const ui = $.ui.resolve(e)
73 const { Box, Button, Text } = ui
74 const sum = await read($, total)
75 const span = await read($, ttl)
76 const at = await read($, lastAt)
77 const left = at + TTL_MS[span] - (await $.clock.now())
78 const hasSvg = ui.Svg !== undefined
79
80 return (
81 <Box flexDirection="column" gap={1}>
82 <Box gap={2}>
83 <Box gap={1}>
84 {ui.Svg !== undefined && icon(ui, 'gauge', '#9ca3af', '')}
85 <Text bold>USAGE</Text>
86 </Box>
87 <Text>
88 turn <Text color="#5fe3ff">in {k(turn.input + turn.read + turn.write)}</Text>{' '}
89 <Text color="#ffe066">out {k(turn.output)}</Text>{' '}
90 <Text color="#7dff9a">cache {hit(turn)}%</Text>
91 </Text>
92 <Text dimColor>
93 session in {k(sum.input + sum.read + sum.write)} · out {k(sum.output)} · cache {hit(sum)}%
94 </Text>
95 </Box>
96 {options.showCacheClock !== false && (
97 <Box gap={2}>
98 <Box gap={1}>
99 {hasSvg && icon(ui, 'timer', left > 0 ? '#7dff9a' : '#9ca3af', '')}
100 <Text color={left > 0 ? 'green' : undefined} dimColor={left <= 0}>
101 {left > 0 ? `cache warm ${clock(left)}` : 'cache cold'}
102 </Text>
103 </Box>
104 <Text dimColor>Cache TTL:</Text>
105 {(['5m', '1h'] as const).map(one => (
106 <Box gap={1}>
107 {hasSvg && icon(ui, span === one ? 'circle-dot' : 'circle', span === one ? '#7dff9a' : '#9ca3af', '', 12)}
108 <Button key={`ttl-${one}`} label={hasSvg ? NAME[one] : span === one ? `● ${NAME[one]}` : `○ ${NAME[one]}`} onPress={() => update($, ttl, () => one)} />
109 </Box>
110 ))}
111 </Box>
112 )}
113 </Box>
114 )
115 })
116}
117hooks/icons.tsx 26 lines1// Icons from Lucide (https://lucide.dev, ISC license), lucide-static 0.469.0.
2// Svg draws on the desktop, editor and mobile surfaces; the terminal has none, so a glyph stands in.
3const PATHS = {
4 'gauge': '<path d="m12 14 4-4" /> <path d="M3.34 19a10 10 0 1 1 17.32 0" />',
5 'circle': '<circle cx="12" cy="12" r="10" />',
6 'circle-dot': '<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="1" />',
7 'timer': '<line x1="10" x2="14" y1="2" y2="2" /> <line x1="12" x2="15" y1="14" y2="11" /> <circle cx="12" cy="14" r="8" />',
8}
9
10export type IconName = keyof typeof PATHS
11
12export const svg = (name: IconName, color: string, size: number) =>
13 `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${PATHS[name]}</svg>`
14
15// eslint-disable-next-line @typescript-eslint/no-explicit-any
16export type Ui = { Svg?: any; Text: any }
17
18export const icon = (ui: Ui, name: IconName, color: string, glyph: string, size = 16) => {
19 const { Svg, Text } = ui
20 return Svg === undefined ? (
21 <Text color={color}>{glyph}</Text>
22 ) : (
23 <Svg source={svg(name, color, size)} alt={name} width={size} height={size} />
24 )
25}
26types/index.d.ts 9 lines1export type TurnUse = { input: number; output: number; read: number; write: number }
2export type CacheTtl = '5m' | '1h'
3
4declare module 'claude-code' {
5 interface PluginState {
6 'cost-meter': { last: TurnUse | null; total: TurnUse; ttl: CacheTtl; lastAt: number }
7 }
8}
9