Status line with 5h usage and context %, toasts at 70/85% usage, and keyword-based model choice for unpinned subagents.

Usage in the status line, spend warnings, cheaper models for subagents.
Shows 5h NN% | ctx NN% in the status line, toasts once when your 5-hour usage passes 70% and 85%, and picks haiku/sonnet/opus for subagents that were given no model, based on keywords in their task. Explicit models are never overridden.
/plugin marketplace add cloudcruncher/claude-mod-garage
/plugin install cost-router@mod-garage
Run /reload-plugins (or restart Claude Code) afterwards. To try it from a local clone without installing: claude --plugin-dir ./dev-mods/cost-router
Illustrative output.
Status line
5h 62% | ctx 34%
Toast
5h usage passed 70% (now 71%)
n/a and no toasts fire.hooks/register.ts 27 lines1import type { Register } from 'claude-code'
2import { crossed, pickTier, statusLine } from './route'
3
4const THRESHOLDS = [70, 85] as const
5
6export const register: Register = on => {
7 const fired = new Set<number>()
8
9 on('session.measure', ($, e, next) => {
10 const five = e.rateLimits.find(r => r.kind === 'five_hour')?.percentUsed
11 $.ui.status(statusLine(five, e.context.percent))
12 if (five !== undefined) {
13 for (const t of crossed(five, THRESHOLDS, fired)) {
14 fired.add(t)
15 $.ui.toast(`5h usage passed ${t}% (now ${Math.round(five)}%)`)
16 }
17 }
18 return next(e)
19 })
20
21 on('tool.call', { tool: 'Agent' }, ($, e, next) => {
22 if (e.model !== undefined) return next(e)
23 const tier = pickTier(`${e.description ?? ''} ${e.prompt ?? ''}`)
24 return tier ? next({ ...e, model: tier }) : next(e)
25 })
26}
27hooks/route.ts 25 lines1export type Tier = 'haiku' | 'sonnet' | 'opus'
2
3const OPUS = /\b(review|audit|security|vulnerab\w*|architect\w*|design|trade-?offs?|threat|plan(ning)?)\b/i
4const SONNET = /\b(implement\w*|fix\w*|bug|test(s|ing)?|refactor\w*|build|write|add|migrat\w*|debug\w*|update|create)\b/i
5const HAIKU = /\b(search|find|look ?up|locate|list|grep|summari[sz]e|summary|explore|triage|read|check|where)\b/i
6
7/** Pick a tier from the delegated prompt; opus beats sonnet beats haiku. Undefined when nothing matches. */
8export function pickTier(text: string): Tier | undefined {
9 if (OPUS.test(text)) return 'opus'
10 if (SONNET.test(text)) return 'sonnet'
11 if (HAIKU.test(text)) return 'haiku'
12 return undefined
13}
14
15/** Crossed thresholds (ascending) between the previous and current percent, not already fired. */
16export function crossed(percent: number, thresholds: readonly number[], fired: ReadonlySet<number>): number[] {
17 return thresholds.filter(t => percent >= t && !fired.has(t))
18}
19
20export function statusLine(fiveHour: number | undefined, context: number | undefined): string {
21 const f = fiveHour === undefined ? '5h n/a' : `5h ${Math.round(fiveHour)}%`
22 const c = context === undefined ? 'ctx n/a' : `ctx ${Math.round(context)}%`
23 return `${f} | ${c}`
24}
25