A pill above the prompt showing model, context, cost and rate limits; auto-compacts at 250k tokens.

Four small mods that make Claude Code cheaper and clearer to use.
Quick start · The mods · auto-model · Contributing
Claude Code is configurable, but the defaults leave money and context on the table. These mods fix the things I kept hitting:
Each mod is independent. Install only the ones you want.
Add the marketplace once:
/plugin marketplace add AneeqMalik/claude-code-mods
Then install only the mods you want, one at a time:
/plugin install auto-model@claude-code-mods
/plugin install usage-status@claude-code-mods
/plugin install token-usage@claude-code-mods
/plugin install trim-startup-context@claude-code-mods
Mods are independent, so any combination works. usage-status shows the routed model when auto-model is also installed, but needs nothing from it.
| | Mod | What it does | Command | | :-: | --- | --- | --- | | 🔀 | auto-model | Routes each turn to Haiku, Sonnet or Opus by workload, switching only when the cache re-write pays off. | /auto-model | | 📊 | usage-status | A pill above the prompt: model, context, cost, rate limits. Auto-compacts at 250k. | | | 🪙 | token-usage | A live pane charting input, output and cache tokens. | /tokens | | ✂️ | trim-startup-context | Finds and denies MCP servers you never use. | /trim-context |
Most turns don't need your biggest model. A Haiku classifier labels each prompt simple, moderate or complex and routes it to Haiku, Sonnet or Opus.
The catch: switching models invalidates the prompt cache. So a downgrade only happens when the cheaper model repays the cache re-write within a few turns. Upgrades are always allowed. If you pick a model with /model, auto-model steps aside.
| Command | Effect |
|---|---|
/auto-model | Show status and recent routing |
/auto-model on · off | Enable or pause routing |
/auto-model haiku · sonnet · opus | Pin one tier |
/auto-model set <tier> <id> | Use an exact model ID for a tier |
/auto-model reset | Clear ID overrides |
For each tier the ID is, in order:
set override;claude-opus-6-0 becomes claude-haiku-6-0);hooks/lib.ts.If a derived ID is refused before any output, the turn retries once on the built-in ID.
The cost model (relative prices, expected turns per switch) uses assumptions in hooks/lib.ts, not billing data. Adjust them to your plan.
A pill above the prompt showing the current model, context used, session cost and rate-limit usage with time to reset. It turns to a warning state at 80% and auto-compacts the conversation at 250k tokens. With auto-model installed, it shows the model actually sent and notices when you change it with /model.
/tokens opens a narrow pane beside the transcript charting this session's tokens in four colourblind-safe series: new input, cache read, cache write and output, updated turn by turn.
MCP servers add their tool schemas to every session's startup context, used or not. This mod scans your last 30 session transcripts for servers that are loaded but never called. With at least 5 sessions of history, it denies them for the current project automatically.
/trim-context show unused servers and the tokens they cost
/trim-context apply deny them in this project
/trim-context undo revert, and stop auto-trimming this project
Issues and PRs welcome. Keep each mod small, tested and independent.
MIT © Aneeq Fayyaz Karim Malik
hooks/register.tsx 116 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Snapshot } from '../types'
5import { countdown, isHigh, limitLabel, modelName, tokenCount } from './lib'
6
7const AUTO_COMPACT_AT = 250_000 // tokens; the mod compacts here on each measure
8const usage = atom({ plugin: 'usage-status', key: 'usage' } as const, null)
9const routed = atom({ plugin: 'auto-model', key: 'effective' } as const, '') // the model auto-model last sent
10const routedFrom = atom({ plugin: 'auto-model', key: 'base' } as const, '') // the session's own model when it did
11
12type Reading = {
13 context: { tokens?: number; percent?: number; window: number }
14 cost?: { usd?: number }
15 rateLimits?: { kind: string; percentUsed: number; resetsAt?: string }[]
16}
17
18// eslint-disable-next-line @typescript-eslint/no-explicit-any
19async function record($: any, r: Reading) {
20 const snap: Snapshot = {
21 model: (await read($, routed)) || (await $.session.model()),
22 percent: r.context.percent,
23 tokens: r.context.tokens,
24 window: r.context.window,
25 costUsd: r.cost?.usd,
26 limits: (r.rateLimits ?? []).map(l => ({ kind: l.kind, percentUsed: l.percentUsed, resetsAt: l.resetsAt })),
27 }
28 await update($, usage, () => snap)}
29
30let stopWatching: (() => void) | undefined
31
32// /model, the picker and the shortcut all change the session's model without any event the bar hears,
33// so the bar would only catch up when something else redraws it. Watch it and redraw on a change.
34// eslint-disable-next-line @typescript-eslint/no-explicit-any
35function watchModel($: any) {
36 stopWatching?.()
37 let seen: string | undefined
38 const timer = $.clock.every(1000, () => {
39 $.session.model().then((m: string) => {
40 if (seen !== undefined && m !== seen) $.ui.invalidate('ui.render')
41 seen = m
42 }, () => {})
43 })
44 stopWatching = () => timer.cancel()
45}
46
47export const register: Register = on => {
48 let compacting = false
49
50 // A fresh session has no measure until its first turn ends; seed the band now.
51 on('session.start', async ($, e, next) => {
52 watchModel($)
53 try {
54 await record($, await $.session.usage())
55 } catch {
56 // the next measure fills it in
57 }
58 return next(e)
59 })
60
61 on('session.measure', async ($, e, next) => {
62 await record($, e)
63
64 if (e.context.tokens !== undefined && e.context.tokens >= AUTO_COMPACT_AT && !compacting) {
65 compacting = true
66 try {
67 await $.session.compact({ trigger: 'plugin', instructions: 'Keep open tasks, decisions and file paths.' })
68 } catch {
69 // rejects while a turn runs; the next measure tries again
70 } finally {
71 compacting = false
72 }
73 }
74
75 return next(e)
76 })
77
78 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
79 const snap = await read($, usage)
80 if (e.props.hasSurvey || snap === null) return next(e)
81
82 const { Box, Text } = $.ui.resolve(e)
83 const now = await $.clock.now()
84 const pct = snap.percent
85 // Read while drawing, so auto-model's switch (or /model) shows now, not at the next measure.
86 const live = await $.session.model().catch(() => '')
87 const sent = await read($, routed)
88 const from = await read($, routedFrom)
89 // The session's own model moved since auto-model routed: the person ran /model, so theirs wins.
90 const model = (sent && (!from || !live || live === from) ? sent : live) || snap.model
91 const below = await next(e) // other mods' rows in the band (the token card) stay too
92
93 return (
94 <Box flexDirection="column">
95 <Box borderStyle="round" borderDimColor columnGap={2} paddingX={1} flexWrap="wrap">
96 <Text>
97 <Text dimColor>ctx </Text>
98 <Text bold color={pct !== undefined && isHigh(pct) ? 'red' : undefined}>{pct ?? 0}%</Text>
99 <Text dimColor> {tokenCount(snap.tokens ?? 0)}/{tokenCount(snap.window)}</Text>
100 </Text>
101 <Text bold>{modelName(model)}</Text>
102 {snap.costUsd !== undefined && <Text color="#c2691e">${snap.costUsd.toFixed(2)}</Text>}
103 {snap.limits.map(l => (
104 <Text key={l.kind}>
105 <Text dimColor>{limitLabel(l.kind)} </Text>
106 <Text bold color={isHigh(l.percentUsed) ? 'red' : undefined}>{Math.round(l.percentUsed)}%</Text>
107 {l.resetsAt && <Text dimColor> ({countdown(Date.parse(l.resetsAt) - now)})</Text>}
108 </Text>
109 ))}
110 </Box>
111 {below}
112 </Box>
113 )
114 })
115}
116hooks/lib.ts 29 lines1// "claude-opus-5-5[1m]" -> "Opus 5.5"; an unknown id is shown as it is.
2export function modelName(id: string): string {
3 const m = /(opus|sonnet|haiku|fable)-(\d+)(?:-(\d+))?/i.exec(id)
4 if (!m) return id.replace(/\[.*\]$/, '')
5 return `${m[1][0].toUpperCase()}${m[1].slice(1).toLowerCase()} ${m[2]}${m[3] ? `.${m[3]}` : ''}`
6}
7
8// 268123 -> "268k", 1000000 -> "1M"
9export function tokenCount(n: number): string {
10 if (n >= 1_000_000) return `${Math.round(n / 100_000) / 10}M`.replace('.0M', 'M')
11 if (n >= 1000) return `${Math.round(n / 1000)}k`
12 return String(n)
13}
14
15// 4h29m, 1d5h, 12m
16export function countdown(ms: number): string {
17 const mins = Math.max(0, Math.floor(ms / 60_000))
18 const d = Math.floor(mins / 1440)
19 const h = Math.floor((mins % 1440) / 60)
20 if (d > 0) return `${d}d${h}h`
21 if (h > 0) return `${h}h${mins % 60}m`
22 return `${mins}m`
23}
24
25export const isHigh = (percent: number): boolean => percent >= 80
26
27export const limitLabel = (kind: string): string =>
28 kind === 'five_hour' ? '5h' : kind === 'seven_day' ? '7d' : kind.replace(/_/g, ' ')
29types/index.d.ts 16 lines1export type Snapshot = {
2 model: string
3 percent?: number
4 tokens?: number
5 window: number
6 costUsd?: number
7 limits: { kind: string; percentUsed: number; resetsAt?: string }[]
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 'usage-status': { usage: Snapshot | null }
13 'auto-model': { effective: string; base: string }
14 }
15}
16