Shows the model, the context fill and the rate-limit windows with pace markers in one line above the prompt

You asked a question. Claude wrote 300 words. You needed 65.
This plugin makes Claude stop writing after the answer. One skill, always on, and every rule is measured. Here is one question, asked twice with the same model. Both answers are copied word for word from one eval run.
Should we use PostgreSQL or SQLite for a single-user desktop app that stores about 50,000 records?
With the plugin: 65 words.
Use SQLite. It runs inside your app as a library, so users install nothing and manage no server. It stores the whole database in one file, which makes backup and migration simple. For 50,000 records it is fast, and one user means you avoid its main limit, which is many writers at the same time. PostgreSQL makes sense when many clients write over a network.
Use SQLite. For a single-user desktop app with about 50,000 records, it's the clear choice.
Why SQLite fits:
- Nothing to install or run. It's a library inside your app, and the whole database is one file. With PostgreSQL, every user would need a database server installed, configured, started and upgraded, which is a lot to ask of a desktop app.
- 50k records is small. SQLite handles millions of rows easily. With sensible indexes, queries at your size will take milliseconds.
- Backup and migration are simple. Copying the file is a backup. Moving the file moves the data.
- It's reliable. It supports ACID transactions and is heavily tested. It's used inside browsers, phones and many desktop apps.
- Support is everywhere. Every major language and ORM supports it, and some platforms include it by default (for example, Python's
sqlite3module).When PostgreSQL would make sense instead:
- Many users or processes writing to the same database at once. SQLite allows only one writer at a time, which doesn't matter for a single user.
- A shared database on a server that several clients connect to.
- A need for Postgres-specific features, such as advanced JSONB indexing, PostGIS, or detailed roles and permissions.
- A near-term plan to turn the app into a multi-user web service. Even then, it's usually easier to start with SQLite and migrate later, especially if you use an ORM or query builder.
Tips if you go with SQLite:
- Turn on WAL mode (
PRAGMA journal_mode=WAL;). Reads and writes block each other less, and it's more resilient.- Turn on
PRAGMA foreign_keys=ON;, since it's off by default.- Store the database file in the OS's standard app-data folder, not next to the executable.
- Add indexes for the columns you filter or sort on.
Both pick SQLite for the same reason. One of them took 235 extra words to say it.
<img src="assets/word-count.svg" alt="Same answer: 300 words without the plugin, 65 words with it" width="520">
git clone https://github.com/mrtnzlml/skills.git
claude --plugin-dir ./skills
/effort on Opus 5.5.One skill, working-rules:
Claude loads a skill only when it decides the skill fits, and it does not always decide that. So a hook injects the rules into every session with SessionStart and into every subagent with SubagentStart. The price is about 1.2k tokens each time.
See exactly what Claude receives:
bash hooks-handlers/inject-skills.sh | jq -r .hookSpecificOutput.additionalContext
A second plugin, usage-status, shows your usage in one row above the prompt: the model, the effort, the folder, the context fill, and the 5-hour and 7-day rate limits.
<img src="assets/usage-band.svg" alt="Claude Code with the usage band above the prompt, with a pace marker in each rate-limit bar" width="894">
The white marker in a bar shows where steady use would be now. The bar is green at or under that pace, yellow up to 20 points ahead, and red when it is more than 20 points ahead or 90% used. If the row does not fit, the rate limits move to a second row.
Install it from the Claude Code prompt:
/plugin install usage-status --marketplace mrtnzlml/skills
The plugin is a mod: TypeScript hooks in mods/usage-status/hooks/register.tsx. CI runs its tests with claude plugin test mods/usage-status.
Thank you for this, the change of the conversation effectiveness is quite drastic. I have to ask for elaborations and explanations quite a bit more as opposed to sifting through bulk. It's like it turned into smug knowitall vs gullible mentor it was previously. It feels more dangerous somehow as its always sure about itself 😄
— Jan Š.
Open an issue or a PR. A new rule needs an eval case that shows a difference with and without the plugin, and bash scripts/check.sh must pass.
hooks/register.tsx 207 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, SessionContextUsage, SessionRateLimit } from 'claude-code'
3
4import type { Header, Limit, Usage } from '../types'
5
6const usage = atom({ plugin: 'usage-status', key: 'usage' } as const, null)
7const header = atom({ plugin: 'usage-status', key: 'header' } as const, null)
8
9// The same 256-color codes as ~/.claude/statusline.py, as hex.
10const MODEL = '#87afff' // 111
11const META = '#585858' // 240
12const VALUE = '#8a8a8a' // 245
13const TRACK = '#303030' // 236
14const SAGE = '#87af87' // 108
15const SAND = '#d7af5f' // 179
16const CLAY = '#d75f5f' // 167
17
18const MARK = '#e4e4e4' // 254
19
20const BAR_WIDTH = 12
21const FILLED = '▰'
22const EMPTY = '▱'
23const MARKER = '┃'
24const SEPARATOR = ' │ '
25const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d', spend_limit: 'spend' }
26const WINDOW_MS: Record<string, number> = { five_hour: 5 * 3_600_000, seven_day: 7 * 86_400_000 }
27const EFFORT: Record<string, string> = { medium: 'med' }
28
29type Segment = { text: string; color: string }
30
31export function levelColor(pct: number): string {
32 if (pct >= 90) return CLAY
33 if (pct >= 70) return SAND
34 return SAGE
35}
36
37export function countdown(resetsAt: string | undefined, now: number): string | undefined {
38 if (!resetsAt) return undefined
39 const secs = Math.floor((Date.parse(resetsAt) - now) / 1000)
40 if (Number.isNaN(secs)) return undefined
41 if (secs <= 0) return 'now'
42 const days = Math.floor(secs / 86400)
43 const hours = Math.floor((secs % 86400) / 3600)
44 const mins = Math.floor((secs % 3600) / 60)
45 if (days) return `${days}d${hours}h`
46 if (hours) return `${hours}h${mins}m`
47 return `${mins}m`
48}
49
50// How much of the window has passed, 0 to 100: where steady use would be now.
51export function paceShare(limit: Limit, now: number): number | undefined {
52 const windowMs = WINDOW_MS[limit.kind]
53 if (windowMs === undefined || !limit.resetsAt) return undefined
54 const left = Date.parse(limit.resetsAt) - now
55 if (Number.isNaN(left)) return undefined
56 return Math.min(100, Math.max(0, ((windowMs - left) / windowMs) * 100))
57}
58
59export function paceColor(pct: number, pace: number | undefined): string {
60 if (pace === undefined || pct >= 90) return levelColor(pct)
61 const ahead = pct - pace
62 if (ahead <= 0) return SAGE
63 if (ahead <= 20) return SAND
64 return CLAY
65}
66
67// The bar's cells, runs of one kind joined; the marker takes the cell of the pace.
68export function barSegments(pct: number, pace: number | undefined): Segment[] {
69 const filled = Math.min(BAR_WIDTH, Math.max(0, Math.round((pct / 100) * BAR_WIDTH)))
70 const marker = pace === undefined ? -1 : Math.min(BAR_WIDTH - 1, Math.floor((pace / 100) * BAR_WIDTH))
71 const color = paceColor(pct, pace)
72 const segments: Segment[] = []
73 for (let i = 0; i < BAR_WIDTH; i++) {
74 const cell =
75 i === marker ? { text: MARKER, color: MARK } : i < filled ? { text: FILLED, color } : { text: EMPTY, color: TRACK }
76 const last = segments.at(-1)
77 if (last && last.color === cell.color && cell.text !== MARKER && !last.text.includes(MARKER)) {
78 last.text += cell.text
79 } else {
80 segments.push(cell)
81 }
82 }
83 return segments
84}
85
86function limitSegments(limit: Limit, now: number): Segment[] {
87 const left = countdown(limit.resetsAt, now)
88 return [
89 { text: (LABELS[limit.kind] ?? limit.kind) + ' ', color: META },
90 ...barSegments(limit.percentUsed, paceShare(limit, now)),
91 { text: ` ${Math.round(limit.percentUsed)}%`, color: VALUE },
92 ...(left === undefined ? [] : [{ text: ' ' + left, color: META }]),
93 ]
94}
95
96function width(segments: Segment[]): number {
97 return segments.reduce((sum, s) => sum + [...s.text].length, 0)
98}
99
100// claude-opus-5-5 -> Opus 5.5, claude-haiku-4-5-20251001 -> Haiku 4.5
101export function displayName(model: string): string {
102 const m = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?(\[1m\])?$/.exec(model)
103 if (!m) return model
104 const [, family = '', major, minor, long] = m
105 const version = minor === undefined ? major : `${major}.${minor}`
106 return `${family.charAt(0).toUpperCase()}${family.slice(1)} ${version}${long ? ' 1M' : ''}`
107}
108
109function basename(path: string): string {
110 return path.replace(/\/+$/, '').split('/').pop() ?? path
111}
112
113function toUsage(context: SessionContextUsage, limits: SessionRateLimit[]): Usage {
114 return {
115 contextPercent: context.percent,
116 limits: limits
117 .filter(l => l.kind in LABELS)
118 .map(({ kind, percentUsed, resetsAt }) => ({ kind, percentUsed, resetsAt })),
119 }
120}
121
122async function readHeader($: EngineInterface, effort: string | undefined): Promise<Header> {
123 const model = await $.session.model()
124 const cwd = await $.session.cwd()
125 const rows = await $.config.list()
126 const isThinking = rows.some(row => row.key === 'thinking' && row.value === true)
127 return { model, cwd, effort, isThinking }
128}
129
130export const register: Register = on => {
131 on('session.start', async ($, e, next) => {
132 const result = await next(e)
133 const current = await readHeader($, undefined)
134 await update($, header, () => current)
135 const now = await $.session.usage()
136 await update($, usage, () => toUsage(now.context, now.rateLimits))
137 // Countdowns move with the clock, not with the session.
138 $.clock.every(60_000, () => $.ui.invalidate('ui.render'))
139
140 return result
141 })
142
143 on('session.measure', async ($, e, next) => {
144 await update($, usage, () => toUsage(e.context, e.rateLimits))
145
146 return next(e)
147 })
148
149 on('turn.step', async function* ($, e, next) {
150 if (e.agentId === undefined) {
151 const effort = e.effort === undefined ? undefined : String(e.effort)
152 const current = await readHeader($, effort)
153 await update($, header, () => current)
154 }
155
156 return yield* next(e)
157 })
158
159 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
160 if (e.props.hasSurvey) return next(e)
161 const top = await read($, header)
162 if (top === null) return next(e)
163 const measured = await read($, usage)
164 const ctx = measured?.contextPercent
165 const now = await $.clock.now()
166 const { Box, Text } = $.ui.resolve(e)
167
168 const meta = [
169 ...(top.effort === undefined ? [] : [EFFORT[top.effort] ?? top.effort]),
170 ...(top.isThinking ? ['think'] : []),
171 ]
172 const head: Segment[] = [
173 { text: displayName(top.model), color: MODEL },
174 ...(meta.length ? [{ text: ' · ' + meta.join(' · '), color: META }] : []),
175 { text: ' ' + basename(top.cwd), color: VALUE },
176 ...(ctx === undefined
177 ? []
178 : [
179 { text: SEPARATOR + 'ctx ', color: META },
180 { text: `${ctx}%`, color: levelColor(ctx) },
181 ]),
182 ]
183 const limits = (measured?.limits ?? []).flatMap((limit, i) => [
184 ...(i === 0 ? [] : [{ text: SEPARATOR, color: META }]),
185 ...limitSegments(limit, now),
186 ])
187
188 const fits = limits.length === 0 || width(head) + SEPARATOR.length + width(limits) <= e.props.bodyColumns
189 const rows = fits
190 ? [[...head, ...(limits.length ? [{ text: SEPARATOR, color: META }, ...limits] : [])]]
191 : [head, limits]
192
193 return (
194 // One empty row between the transcript and the band.
195 <Box flexDirection="column" marginTop={1}>
196 {rows.map((row, i) => (
197 <Text key={`row${i}`}>
198 {row.map(segment => (
199 <Text color={segment.color}>{segment.text}</Text>
200 ))}
201 </Text>
202 ))}
203 </Box>
204 )
205 })
206}
207types/index.d.ts 12 lines1export type Limit = { kind: string; percentUsed: number; resetsAt?: string }
2
3export type Usage = { contextPercent?: number; limits: Limit[] }
4
5export type Header = { model: string; cwd: string; effort?: string; isThinking: boolean }
6
7declare module 'claude-code' {
8 interface PluginState {
9 'usage-status': { usage: Usage | null; header: Header | null }
10 }
11}
12