One row above the prompt: how much of the 5-hour and weekly usage limits is used, in percent, with the time until each resets.

<h1 align="center">Claude Code Mods</h1>
<a href="https://github.com/T-AI-Labs/claude-code-mods/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/T-AI-Labs/claude-code-mods/ci.yml?branch=main&style=flat-square&label=ci" alt="CI"></a> <a href="https://github.com/T-AI-Labs/claude-code-mods/releases"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FT-AI-Labs%2Fclaude-code-mods%2Fmain%2Fcache-meter%2F.claude-plugin%2Fplugin.json&query=%24.version&prefix=v&label=cache-meter&style=flat-square" alt="cache-meter version"></a> <a href="https://github.com/T-AI-Labs/claude-code-mods/releases"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FT-AI-Labs%2Fclaude-code-mods%2Fmain%2Fusage-limits%2F.claude-plugin%2Fplugin.json&query=%24.version&prefix=v&label=usage-limits&style=flat-square" alt="usage-limits version"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/T-AI-Labs/claude-code-mods?style=flat-square" alt="License"></a> <img src="https://img.shields.io/badge/Claude%20Code-2.1.295-d97757?style=flat-square" alt="Claude Code 2.1.295"> <img src="https://img.shields.io/badge/Claude%20desktop%20app-2.31226.1-d97757?style=flat-square" alt="Claude desktop app 2.31226.1">
<a href="#install"><strong>Install</strong></a> · <a href="#cache-meter">cache-meter</a> · <a href="#usage-limits">usage-limits</a> · <a href="#token-cost">Token cost</a> · <a href="#configure">Configure</a> · <a href="#update-and-uninstall">Update</a> · <a href="#credits">Credits</a>
claude plugin marketplace add T-AI-Labs/claude-code-mods
claude plugin install cache-meter@T-AI-Labs
claude plugin install usage-limits@T-AI-Labs
Run the three commands above in any shell (cmd, PowerShell, bash). The mods load in every session started afterwards, in the terminal and in the desktop app.
From the prompt of a running Claude Code terminal session, the same install is:
/plugin install cache-meter --marketplace T-AI-Labs/claude-code-mods
/plugin install usage-limits --marketplace T-AI-Labs/claude-code-mods
The mods use Claude Code's function-hook plugin API, which is in early access and may change between releases. They are built and tested against Claude Code 2.1.295, the engine in the Claude desktop app 2.31226.1.
Cache ▮▮▮▮▮ 92% (92k/100k) Context ▮▯▯▯▯ 10% (100k/1M) Auto 97% ▾ [ Compact ] [ Clear ]
| Element | What it shows |
|---|---|
| Cache | Share of the last turn's input tokens that the prompt cache served, with tokens read and total input in parentheses. Turns yellow below 40 %. |
| Context | Fill of the model's context window, with tokens used and window size in parentheses. Turns yellow within 15 points of the auto-compact threshold and red at it. |
| Auto | The auto-compact threshold: the context fill, as shown under Context, at which the mod compacts on its own. A dropdown over 97, 90, 70, 60 and 50 %. The choice is kept across sessions. |
| Compact | Runs /compact now. |
| Clear | Runs /clear. |
Both figures read – until the first response of a session, and again after a clear.
After each turn the mod compares the context fill with the threshold and runs /compact once it is reached. It does not fire again until the fill has dropped below the threshold, so a compaction that lands above it does not loop.
Claude Code's built-in auto-compaction stays on and still runs at its own limit. A threshold above that limit therefore never fires, and a long turn can pass the threshold before the mod gets to compact.
Showing the rows costs nothing. The mods read figures Claude Code already has; they make no model requests of their own and add nothing to the conversation.
A compaction does cost tokens, whether you press Compact or the threshold triggers it: /compact is a model request that summarizes the conversation, and the next turn starts on a cold cache. The lower the threshold, the more often that happens. To keep usage down, leave the threshold high and compact by hand when it suits you.
5h ▮▮▯▯▯ 42% (2h 13m) Week ▮▮▮▮▯ 78% (3d 4h)
Each rate-limit window Claude Code reports gets a gauge, the percentage used and, in parentheses, the time left until it resets. 5h is the 5-hour limit, Week the weekly one. A figure turns yellow from 75 % and red from 90 %.
The readings arrive with the first response of a session and are only reported on a subscription plan. Until then the row shows the last reading of the previous session, marked with its age (as of 3h 20m ago) and without the windows that have reset since. With nothing to show it reads Usage limits: no reading yet.
Both mods take the same two options:
| Option | Values | Default | Effect |
|---|---|---|---|
display | gauge, text | gauge | gauge draws a five-segment fill level before each percentage; text leaves it out. |
details | true, false | true | Shows the figures in parentheses. |
Change them at any time from a shell, once per mod. In PowerShell:
'{"display":"text","details":"false"}' | claude plugin configure cache-meter@T-AI-Labs --values-stdin
In cmd:
echo {"display":"text","details":"false"} | claude plugin configure cache-meter@T-AI-Labs --values-stdin
Leave out an option to keep its value. claude plugin configure cache-meter@T-AI-Labs alone shows what is set. A new session picks up the change.
They can also be set while installing, with --config display=text, and in a terminal session they are rows of the /config menu. Give both mods the same settings to keep their columns aligned.
cache-meter needs about 92 columns with the defaults. On a narrower window its buttons wrap to a second line. Turning details off saves about 22 columns, display set to text about 12:
Cache ▮▮▮▮▮ 92% Context ▮▯▯▯▯ 10% Auto 97% ▾ [ Compact ] [ Clear ]
In the terminal, use the mouse, or move the focus into the row with ctrl+x tab: Tab walks the controls, Enter presses a button, and the arrow keys pick a threshold. To hide both rows there, collapse the band with its [-] mark or ctrl+x ctrl+a.
claude plugin marketplace update T-AI-Labs
claude plugin update cache-meter@T-AI-Labs
claude plugin update usage-limits@T-AI-Labs
claude plugin uninstall cache-meter@T-AI-Labs
claude plugin uninstall usage-limits@T-AI-Labs
A new session picks up the change.
The idea for these mods comes from Julian Ivanov and his video Diese 10 Claude Code Mods solltest du unbedingt kennen. The code in this repository was written independently.
hooks/register.tsx 211 lines1import { atom, read, update } from 'claude-code'
2import type { Color, Elements, EngineInterface, Register } from 'claude-code'
3
4import type { Reading, UsageWindow } from '../types'
5
6const LABELS: Record<string, string> = {
7 five_hour: '5h',
8 seven_day: 'Week',
9 spend_limit: 'Spend',
10}
11const ORDER = ['five_hour', 'seven_day', 'spend_limit']
12const MINUTE = 60_000
13const HOUR = 60 * MINUTE
14const DAY = 24 * HOUR
15const STORE_KEY = 'reading'
16
17// Shared with cache-meter so the columns of both rows line up.
18const LABEL_WIDTHS = [5, 7]
19const SEGMENTS = 5
20const PERCENT_WIDTH = 4
21const DETAIL_WIDTH = 10
22
23type Tags = Pick<Elements['terminal'], 'Box' | 'Text'>
24type Layout = { hasGauge: boolean; hasDetails: boolean }
25type Cell = {
26 label: string
27 column: number
28 percent: number | null
29 tone: Color
30 detail: string
31}
32
33const windows = atom({ plugin: 'usage-limits', key: 'windows' } as const, [])
34const now = atom({ plugin: 'usage-limits', key: 'now' } as const, 0)
35const staleSince = atom(
36 { plugin: 'usage-limits', key: 'staleSince' } as const,
37 null,
38)
39
40const labelOf = (kind: string) => LABELS[kind] ?? kind.replaceAll('_', ' ')
41
42const rankOf = (kind: string) => {
43 const rank = ORDER.indexOf(kind)
44
45 return rank === -1 ? ORDER.length : rank
46}
47
48const toneOf = (percent: number): Color =>
49 percent >= 90 ? 'error' : percent >= 75 ? 'warning' : 'text'
50
51const span = (duration: number) => {
52 const days = Math.floor(duration / DAY)
53 const hours = Math.floor((duration % DAY) / HOUR)
54 const minutes = Math.max(1, Math.floor((duration % HOUR) / MINUTE))
55
56 if (days > 0) {
57 return `${days}d ${hours}h`
58 }
59
60 return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`
61}
62
63const untilReset = (resetsAt: string | undefined, at: number) => {
64 const left = resetsAt === undefined ? Number.NaN : Date.parse(resetsAt) - at
65
66 if (Number.isNaN(left)) {
67 return ''
68 }
69
70 return left <= 0 ? '(now)' : `(${span(left)})`
71}
72
73const isOpen = (one: UsageWindow, at: number) =>
74 one.resetsAt === undefined || Date.parse(one.resetsAt) > at
75
76const meter = ({ Box, Text }: Tags, layout: Layout, one: Cell) => {
77 const filled =
78 one.percent === null
79 ? 0
80 : Math.max(
81 0,
82 Math.min(SEGMENTS, Math.round((one.percent / 100) * SEGMENTS)),
83 )
84
85 return (
86 <Box flexDirection="row" flexShrink={0}>
87 <Box
88 minWidth={LABEL_WIDTHS[one.column] ?? LABEL_WIDTHS[1]}
89 marginRight={1}
90 flexShrink={0}
91 >
92 <Text dimColor>{one.label}</Text>
93 </Box>
94 {layout.hasGauge && (
95 <Box minWidth={SEGMENTS} marginRight={1} flexShrink={0}>
96 <Text color={one.tone}>{'▮'.repeat(filled)}</Text>
97 <Text dimColor>{'▯'.repeat(SEGMENTS - filled)}</Text>
98 </Box>
99 )}
100 <Box
101 minWidth={PERCENT_WIDTH}
102 marginRight={1}
103 flexShrink={0}
104 justifyContent="flex-end"
105 >
106 <Text color={one.percent === null ? 'inactive' : one.tone}>
107 {one.percent === null ? '–' : `${Math.round(one.percent)}%`}
108 </Text>
109 </Box>
110 {layout.hasDetails && (
111 <Box minWidth={DETAIL_WIDTH} marginRight={1} flexShrink={0}>
112 <Text dimColor>{one.detail}</Text>
113 </Box>
114 )}
115 </Box>
116 )
117}
118
119const refresh = async ($: EngineInterface, list?: readonly UsageWindow[]) => {
120 const at = await $.clock.now()
121 await update($, now, () => at)
122
123 if (list === undefined) {
124 return
125 }
126
127 const sorted = [...list].sort((a, b) => rankOf(a.kind) - rankOf(b.kind))
128 await update($, windows, () => sorted)
129 await update($, staleSince, () => null)
130
131 if (sorted.length > 0) {
132 await $.store.set(STORE_KEY, { windows: sorted, at })
133 }
134}
135
136const restore = async ($: EngineInterface) => {
137 const at = await $.clock.now()
138 const stored = (await $.store.get(STORE_KEY)) as Reading | undefined
139 await update($, now, () => at)
140
141 if (stored !== undefined) {
142 await update($, windows, () => stored.windows.filter(one => isOpen(one, at)))
143 await update($, staleSince, () => stored.at)
144 }
145}
146
147export const register: Register = (on, options) => {
148 const layout: Layout = {
149 hasGauge: options.display !== 'text',
150 hasDetails: options.details !== false,
151 }
152
153 on('session.start', async ($, e, next) => {
154 const usage = await $.session.usage()
155
156 // The engine has no reading before the first response of a session.
157 if (usage.rateLimits.length > 0) {
158 await refresh($, usage.rateLimits)
159 } else {
160 await restore($)
161 }
162
163 $.clock.every(MINUTE, () => void refresh($))
164
165 return next(e)
166 })
167
168 on('session.measure', async ($, e, next) => {
169 await refresh($, e.rateLimits)
170
171 return next(e)
172 })
173
174 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
175 const below = await next(e)
176
177 if (e.props.hasSurvey) {
178 return below
179 }
180
181 const { Box, Text } = $.ui.resolve(e)
182 const list = await read($, windows)
183 const at = await read($, now)
184 const since = await read($, staleSince)
185
186 return (
187 <Box flexDirection="column">
188 {list.length === 0 ? (
189 <Text dimColor>Usage limits: no reading yet</Text>
190 ) : (
191 <Box flexDirection="row" flexWrap="wrap">
192 {list.map((one, column) =>
193 meter({ Box, Text }, layout, {
194 label: labelOf(one.kind),
195 column,
196 percent: one.percentUsed,
197 tone: toneOf(one.percentUsed),
198 detail: untilReset(one.resetsAt, at),
199 }),
200 )}
201 {since !== null && (
202 <Text dimColor>{`as of ${span(at - since)} ago`}</Text>
203 )}
204 </Box>
205 )}
206 {below}
207 </Box>
208 )
209 })
210}
211types/index.d.ts 18 lines1export type UsageWindow = {
2 kind: string
3 percentUsed: number
4 resetsAt?: string
5}
6
7export type Reading = { windows: UsageWindow[]; at: number }
8
9declare module 'claude-code' {
10 interface PluginState {
11 'usage-limits': {
12 windows: UsageWindow[]
13 now: number
14 staleSince: number | null
15 }
16 }
17}
18