Shows the active model, effort level, context fill and rate-limit usage with reset times above the prompt.

Two small mods for Claude Code that put the numbers you usually have to go looking for right above the prompt.
| Mod | What it shows | Updates on |
|---|---|---|
token-meter | Input and output tokens of the last prompt, plus the running total for the session | every completed turn |
session-stats | Active model, effort level, context-window fill, and rate-limit usage with reset times | every model step and usage measurement |
Install one or both. They stack: session-stats draws its row under whatever is already above the prompt.
╭────────────────────────────────────────────────────────────────────────────────────────╮
│ ◆ TOKENS · PROMPT 12 ↓ 48.213 in ↑ 1.904 out │ Σ TOTAL 402.118 in · 18.733 out │
╰────────────────────────────────────────────────────────────────────────────────────────╯
╭────────────────────────────────────────────────────────────────────────────────────────╮
│ ◈ MODEL opus-5-5 ✦ EFFORT high ▤ CONTEXT ████░░░░░░ 41% 82.000/200.000 │
│ ⏱ 5h ███░░░ 47% ↻ Mon 19.10 14:30 (2h05) ⏱ 7d █░░░░░ 18% ↻ Thu 22.10 09:00 (2d20h) │
╰────────────────────────────────────────────────────────────────────────────────────────╯
(Illustrative values. Context and rate-limit bars are green below 60%, yellow from 60%, red from 85%. On a narrow terminal the row wraps.)
Claude Code already tracks tokens, context and limits, but you have to ask for them (/context, /usage) and they are gone once you go back to typing. When you are working with a large context or close to a rate limit, a glance at the prompt is faster than a command, and it is easier to notice when a single prompt costs far more than expected.
The function hooks API is early access and may change between Claude Code releases. If an update breaks a mod, Claude Code skips it and keeps working. Please open an issue.
At the prompt of a Claude Code terminal session:
/plugin install token-meter --marketplace Marco-Sau/claude-code-mods
/plugin install session-stats --marketplace Marco-Sau/claude-code-mods
Answer y to add the marketplace (the first time only), then choose a scope (user scope is the default and loads the mod in every session). Each mod becomes active in that session right away.
Older Claude Code versions (error: unknown option '--marketplace'): add the marketplace once, then install by name. This form works on every version:
claude plugin marketplace add Marco-Sau/claude-code-mods
claude plugin install token-meter@marcosau-mods
claude plugin install session-stats@marcosau-mods
The same works at the prompt: /plugin marketplace add Marco-Sau/claude-code-mods, then /plugin install token-meter@marcosau-mods.
From a shell, on recent versions:
claude plugin install token-meter --marketplace Marco-Sau/claude-code-mods
claude plugin install session-stats --marketplace Marco-Sau/claude-code-mods
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with Marco-Sau/claude-code-mods, then Add plugin.
There is nothing to configure or run. Send a prompt.
token-meter
↓ in: input tokens of the last prompt, including cache reads and cache writes, which is what the model actually processed.↑ out: output tokens of the last prompt.PROMPT n and Σ TOTAL: the number of prompts and the summed input and output since the session started.session-stats
MODEL: the model that answered the last step, without the claude- prefix.EFFORT: the effort level of the last step, or default when none was set.CONTEXT: how full the context window is, as a bar, a percentage, and tokens used over window size.⏱ 5h / ⏱ 7d: rate-limit windows that Claude Code reports, with percent used and when each resets (local time and time left). These appear only when Claude Code reports rate limits for your account.Both bands step aside while a survey is shown above the prompt.
Both mods are a single TypeScript module each, which you can read in full in token-meter/hooks/register.tsx and session-stats/hooks/register.tsx.
| token-meter | session-stats | |
|---|---|---|
| Events it listens to | turn.complete | turn.step, session.measure |
| What it draws | AbovePrompt band | AbovePrompt band |
| Engine calls | session state, UI elements | session state, UI elements, clock |
| Files, shell, network, other tools | none | none |
| Data kept | counters for the current session only | last model, effort, context and limits for the current session only |
Mod code runs in Claude Code's sandboxed hooks environment, with no Node, filesystem or network access of its own. claude plugin validate reports exactly the hooks and calls listed above.
claude plugin update token-meter@marcosau-mods
claude plugin update session-stats@marcosau-mods
claude plugin uninstall token-meter@marcosau-mods
claude plugin uninstall session-stats@marcosau-mods
Updates apply after a restart (or /reload-plugins).
token-meter and do not change the model shown in session-stats.↓ in counts cache reads and cache writes together with fresh input. It reflects context size per prompt, not billed cost.48.213), the European convention.Each mod is a folder with .claude-plugin/plugin.json, hooks/hooks.json, hooks/register.tsx, a state contract in types/index.d.ts, and tests in tests/.
claude plugin validate ./token-meter # manifest, hooks and state contract
claude plugin test ./token-meter # runs tests/*.test.tsx against the engine
claude --plugin-dir ./token-meter # try a local copy in one session
The tests cover the terminal and desktop surfaces.
MIT © 2026 Marco Sau, marcosau.com
hooks/register.tsx 123 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Stats } from '../types'
5
6const stats = atom({ plugin: 'session-stats', key: 'stats' } as const, {
7 model: null,
8 effort: null,
9 contextPercent: null,
10 contextTokens: null,
11 contextWindow: null,
12 limits: [],
13} as Stats)
14
15const dots = (n: number) => String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, '.')
16
17const bar = (percent: number, width = 10) => {
18 const filled = Math.max(0, Math.min(width, Math.round((percent / 100) * width)))
19
20 return '█'.repeat(filled) + '░'.repeat(width - filled)
21}
22
23const tone = (percent: number) => (percent >= 85 ? 'red' : percent >= 60 ? 'yellow' : 'green')
24
25const label = (kind: string) =>
26 kind === 'five_hour' ? '5h' : kind === 'seven_day' ? '7d' : kind.replace('_', ' ')
27
28const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
29const two = (n: number) => String(n).padStart(2, '0')
30
31const resetLabel = (iso: string | undefined, now: number) => {
32 const at = iso ? Date.parse(iso) : NaN
33 const ms = at - now
34
35 if (!Number.isFinite(ms) || ms <= 0) {
36 return ''
37 }
38
39 const d = new Date(at)
40 const minutes = Math.round(ms / 60000)
41 const left =
42 minutes >= 1440
43 ? `${Math.floor(minutes / 1440)}d${Math.floor((minutes % 1440) / 60)}h`
44 : minutes >= 60
45 ? `${Math.floor(minutes / 60)}h${two(minutes % 60)}`
46 : `${minutes}m`
47
48 return ` ↻ ${DAYS[d.getDay()]} ${two(d.getDate())}.${two(d.getMonth() + 1)} ${two(d.getHours())}:${two(d.getMinutes())} (${left})`
49}
50
51export const register: Register = on => {
52 on('turn.step', async function* ($, e, next) {
53 if (!e.agentId) {
54 await update($, stats, s => ({ ...s, model: e.model, effort: e.effort === undefined ? null : String(e.effort) }))
55 }
56
57 yield* next(e)
58 })
59
60 on('session.measure', async ($, e, next) => {
61 await update($, stats, s => ({
62 ...s,
63 contextPercent: e.context.percent ?? null,
64 contextTokens: e.context.tokens ?? null,
65 contextWindow: e.context.window,
66 limits: e.rateLimits.map(l => ({ kind: l.kind, percentUsed: l.percentUsed, resetsAt: l.resetsAt })),
67 }))
68
69 return next(e)
70 })
71
72 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
73 const below = await next(e)
74 const s = await read($, stats)
75
76 if (e.props.hasSurvey || s.model === null) {
77 return below
78 }
79
80 const now = await $.clock.now()
81 const { Box, Text } = $.ui.resolve(e)
82 const ctx = s.contextPercent
83
84 const mine = (
85 <Box borderStyle="round" borderColor="cyan" paddingX={1} gap={2}>
86 <Text>
87 <Text color="cyan" bold>◈ MODEL </Text>
88 <Text bold>{s.model.replace(/^claude-/, '')}</Text>
89 </Text>
90 <Text>
91 <Text color="magenta" bold>✦ EFFORT </Text>
92 <Text bold>{s.effort ?? 'default'}</Text>
93 </Text>
94 {ctx !== null ? (
95 <Text>
96 <Text color="yellow" bold>▤ CONTEXT </Text>
97 <Text color={tone(ctx)}>{bar(ctx)} {Math.round(ctx)}%</Text>
98 {s.contextTokens !== null && s.contextWindow !== null ? (
99 <Text dimColor> {dots(s.contextTokens)}/{dots(s.contextWindow)}</Text>
100 ) : null}
101 </Text>
102 ) : null}
103 {s.limits.map(l => (
104 <Text key={l.kind}>
105 <Text color="blue" bold>⏱ {label(l.kind)} </Text>
106 <Text color={tone(l.percentUsed)}>{bar(l.percentUsed, 6)} {Math.round(l.percentUsed)}%</Text>
107 <Text dimColor>{resetLabel(l.resetsAt, now)}</Text>
108 </Text>
109 ))}
110 </Box>
111 )
112
113 return below ? (
114 <Box flexDirection="column">
115 {below}
116 {mine}
117 </Box>
118 ) : (
119 mine
120 )
121 })
122}
123types/index.d.ts 16 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2export type Stats = {
3 model: string | null
4 effort: string | null
5 contextPercent: number | null
6 contextTokens: number | null
7 contextWindow: number | null
8 limits: Limit[]
9}
10
11declare module 'claude-code' {
12 interface PluginState {
13 'session-stats': { stats: Stats }
14 }
15}
16