Picks the right model for every subagent without being told: Haiku for lookups, Sonnet for building, Opus for judgment calls

Mods that load in every Claude Code session: the terminal, the desktop app's Code tab, and cloud sessions.
| Mod | What it shows |
|---|---|
| repo-sentry | Business, repo, branch, behind or ahead of origin, dirty count. Warns on work sitting on main, a stale repo, or a staged secret. |
| progress-pulse | A live gradient progress band for the session's task list, a cheer on every finished task, /progress for the full view, /progress demo to see it run. |
| usage-meter | The 5-hour and weekly plan limits with reset times, context fill and session cost, right above the message box. /usage-meter hides or shows it. |
setup-mac.js (run once) installs every mod as a Claude Code plugin read straight from this folder, and adds a session-start hook that runs sync-mods.js --quiet. A mod added to the marketplace installs itself at the next session start; an edit to a mod takes effect at the next session start or /reload-plugins.cloud-setup.sh goes into each cloud environment's setup script. Every new container clones this repo and runs sync-mods.js.Put it in its own folder with .claude-plugin/plugin.json, add it to .claude-plugin/marketplace.json, check it with claude plugin validate <folder> and claude plugin test <folder>, commit and push.
mcp-diagnose.js, mcp-where.js, move-mcp-to-repo.js, mcp-set-secret.js, mcp-env-from-dotenv.js: MCP server helpers that never print secret values. servicetrade-mcp/: the read-only ServiceTrade MCP server.
hooks/register.ts 61 lines1import type { Register } from 'claude-code'
2import { lane, LANE_MODEL, POLICY } from './route'
3
4type Decision = { at: number; task: string; type: string; model: string; source: 'claude' | 'router' | 'definition'; why: string }
5const LOG = 'decisions', OFF = 'off'
6
7export const register: Register = on => {
8 on('session.start', async ($, e, next) => {
9 const r = await next(e)
10 await $.command.register({ name: 'model-router', description: 'Show which model each subagent got and why (on, off)' })
11 return r
12 })
13
14 // Tell the main loop the policy, so it chooses explicitly. Only loops that can spawn agents get it.
15 on('prompt.compose', async ($, e, next) => {
16 const r = await next(e)
17 if (!e.tools.includes('Agent')) return r
18 try { if (await $.store.get(OFF)) return r } catch { /* store unavailable: keep routing on */ }
19 return { sections: [...r.sections, { id: 'model-router:policy', text: POLICY, scope: 'session' as const }] }
20 }).catch(($, e, next) => next(e))
21
22 on('agent.spawn', async ($, e, next) => {
23 let off = false
24 try { off = Boolean(await $.store.get(OFF)) } catch { /* keep routing on */ }
25 let input = e, source: Decision['source'] = 'claude', why = 'chosen by Claude'
26 if (e.model) {
27 // Claude chose: respect it.
28 } else if (e.subagentType !== 'general-purpose') {
29 // A named agent type carries its own model setting, or deliberately inherits. Leave it alone.
30 source = 'definition'; why = `${e.subagentType} uses its own model setting`
31 } else if (!off) {
32 const pick = lane(e.description, e.prompt)
33 input = { ...e, model: LANE_MODEL[pick.lane] }
34 source = 'router'; why = pick.why
35 }
36 const r = await next(input)
37 if (r.deny === undefined) {
38 try {
39 const log = ((await $.store.get(LOG)) ?? []) as Decision[]
40 log.push({ at: await $.clock.now(), task: e.description.slice(0, 80), type: e.subagentType, model: r.model, source, why })
41 await $.store.set(LOG, log.slice(-25))
42 } catch { /* the log is a nicety */ }
43 }
44 return r
45 }).catch(($, e, next) => next(e))
46
47 on('command.run', { command: 'model-router' }, async ($, e) => {
48 const arg = e.args.trim().toLowerCase()
49 if (arg === 'off' || arg === 'on') {
50 await $.store.set(OFF, arg === 'off')
51 return { text: arg === 'off' ? 'Model routing off: subagents without a model inherit the session model again.' : 'Model routing on.' }
52 }
53 const off = Boolean(await $.store.get(OFF))
54 const log = ((await $.store.get(LOG)) ?? []) as Decision[]
55 const lines = [`Model routing is ${off ? 'OFF' : 'on'}. Last ${Math.min(10, log.length)} of ${log.length} subagents logged:`]
56 for (const d of log.slice(-10).reverse()) lines.push(` ${d.model.padEnd(18)} ${d.task} (${d.source}: ${d.why})`)
57 if (!log.length) lines.push(' none yet')
58 return { text: lines.join('\n') }
59 })
60}
61hooks/route.ts 31 lines1// The routing policy, kept pure so it is tested without an engine.
2// Three lanes. When a task matches both a heavy and a light word, heavy wins: when in doubt, go up.
3export type Lane = 'light' | 'build' | 'heavy'
4export const LANE_MODEL: Record<Lane, string> = { light: 'haiku', build: 'sonnet', heavy: 'opus' }
5
6// Judgment work, and anything touching money, compliance, legal or patient data.
7const HEAVY = /\b(review|audit|architect\w*|design|plan|planning|security|secur\w*|debug\w*|root[- ]cause|investigat\w*|diagnos\w*|refactor\w*|migrat\w*|schema|reconcil\w*|financ\w*|revenue|pricing|payroll|tax\w*|invoice\w*|payment\w*|compliance|legal|contract\w*|regulat\w*|patient\w*|hipaa|statement\w*|decide|decision|tradeoff|trade-off|strategy|verify|verification|prove)\b/i
8// Read-only lookups and mechanical summaries.
9const LIGHT = /\b(find|search|list|count|grep|locate|look ?up|scan|map|inventory|collect|gather|fetch|read|summari[sz]e|survey|check (if|whether|that)|which files|where is|print|show)\b/i
10const READ_ONLY = /\bread[- ]only\b|\bdo not (edit|modify|change|write)\b|\bdon'?t (edit|modify|change|write)\b|\bno (edits|changes)\b/i
11
12export function lane(description: string, prompt: string): { lane: Lane; why: string } {
13 const head = prompt.slice(0, 600)
14 const heavy = description.match(HEAVY) ?? head.match(HEAVY)
15 if (heavy) return { lane: 'heavy', why: `"${heavy[0]}" is judgment work` }
16 const light = description.match(LIGHT)
17 if (light && (READ_ONLY.test(head) || !/\b(write|edit|fix|build|implement|create|add|change|update|delete|remove)\b/i.test(description))) {
18 return { lane: 'light', why: `"${light[0]}" is a lookup` }
19 }
20 return { lane: 'build', why: 'default for building and editing' }
21}
22
23// The general rule the main model follows when it chooses for itself. Its explicit choice always wins.
24export const POLICY = [
25 'Model routing for subagents (set by the model-router mod): pick the model on every Agent call yourself, by the work, not by habit.',
26 '- haiku: read-only lookups, searches, counts, listing files, mechanical summaries.',
27 '- sonnet: writing and editing code, tests, docs, routine data work.',
28 '- opus: reviews, debugging, architecture and planning, migrations, and anything touching money, compliance, legal or patient data. When unsure, go one tier up.',
29 'If you leave model unset on a general-purpose agent, the mod picks by these rules instead of letting it inherit the most expensive model.',
30].join('\n')
31