Runs search subagents on Haiku and general-purpose ones on Sonnet, so the main model's tokens go to the work only it can do

A Claude Code mod that starts subagents on a cheaper model than the main one. A subagent reads files and searches in its own context and hands back a summary, so a cheaper model reading through a codebase saves tokens without touching the main conversation's prompt cache.
| Agent type | Model |
|---|---|
| Explore | Haiku |
| general-purpose | Sonnet |
Every other type, such as Plan or an agent you wrote, keeps the model its definition names.
The mod leaves a spawn alone when:
Effort isn't set here. With auto-effort installed, Explore agents run at low.
/subagent-router shows whether routing is on/subagent-router off and /subagent-router on turn it off and back on for the session/plugin install subagent-router --marketplace noash-xrc/claude-tools
Answer y to add the marketplace, then pick a scope.
claude plugin marketplace update noash-tools
claude plugin update subagent-router@noash-tools
Restart Claude Code to load the new version.
MIT
hooks/register.ts 39 lines1import { atom, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4// Agent types whose work a cheaper model does as well; any other type keeps its own model.
5const ROUTES: Record<string, string> = { Explore: 'haiku', 'general-purpose': 'sonnet' }
6const RANK = ['haiku', 'sonnet', 'opus']
7// ponytail: a model id naming none of the three ranks as unknown, so the route is skipped
8const rank = (model: string) => RANK.findIndex(m => model.toLowerCase().includes(m))
9
10let enabled = true
11// savings-meter reads this to price the routed agents' tokens
12const routedAtom = atom({ plugin: 'subagent-router', key: 'routed' } as const, [])
13
14export const register: Register = on => {
15 on('session.start', async ($, e, next) => {
16 await $.command.register({ name: 'subagent-router', description: 'Show, or turn on or off, routing subagents to cheaper models', argumentHint: '[on | off]' })
17 return next(e)
18 })
19
20 on('command.run', { command: 'subagent-router' }, async ($, e) => {
21 const arg = e.args.trim().toLowerCase()
22 if (arg === 'on' || arg === 'off') enabled = arg === 'on'
23 else if (arg) return { text: `Unknown argument "${arg}". Use on or off.` }
24 const routes = Object.entries(ROUTES).map(([type, model]) => `${type} on ${model}`).join(', ')
25 return { text: enabled ? `subagent-router is on: ${routes}.` : 'subagent-router is off.' }
26 })
27
28 // A model Claude named in the call, a fork or a teammate is left alone; so is a route that wouldn't be cheaper than the parent.
29 on('agent.spawn', async ($, e, next) => {
30 const model = ROUTES[e.subagentType]
31 const cheaper = model !== undefined && rank(model) < rank(e.parentModel)
32 if (!enabled || !cheaper || e.model !== undefined || e.fork || e.isTeammate) return next(e)
33 const r = await next({ ...e, model })
34 const id = 'agentId' in r ? r.agentId : undefined
35 if (id) await update($, routedAtom, ids => [...ids, id])
36 return r
37 }).catch(($, e, next) => next(e))
38}
39types/index.d.ts 8 lines1// ids of the subagents this session started on a cheaper model
2declare module 'claude-code' {
3 interface PluginState {
4 'subagent-router': { routed: string[] }
5 }
6}
7export type Contract = never
8