Routes each subagent to Haiku, Sonnet or Opus by the [tier:...] tag from your plan. The main session never switches model, so its prompt cache stays intact.

Claude Code mod: plan in chat, route execution by tier.
Each subagent's model comes from the [tier:...] tag in its task: light → Haiku · standard → Sonnet · deep → Opus. Untagged tasks are classified by Haiku. Above 85 % rate-limit usage, tasks step down one tier. Every subagent is told to return a summary under 150 words.
The main session never switches model, so its prompt cache stays intact.
Claude Code v2.1.287+ (mods).
claude --plugin-dir ./routing-mod-claude
claude plugin validate ./routing-mod-claude
/plugin marketplace add blackmath88/routing-mod-claude
/plugin install routing-mod@routing-mod-claude
Clone this repository, then run:
./setup.sh
./setup.sh --check
/model sonnet/advisor opus (subagents inherit it when the pairing is valid)templates/CLAUDE-snippet.md into the project's CLAUDE.md.plans/<feature>.md (format: templates/PLAN.md).Tune tiers and the limit at the top of hooks/register.js.
hooks/register.js 49 lines1// routing-mod: route subagents by tier. Main session is never switched (keeps its cache).
2
3const TIERS = { light: 'haiku', standard: 'sonnet', deep: 'opus' }
4const STEP_DOWN = { deep: 'standard', standard: 'light', light: 'light' }
5const LIMIT_PERCENT = 85 // step down one tier above this rate-limit usage
6const TAG = /\[tier:(light|standard|deep)\]/i
7const RETURN_RULE =
8 '\n\nWhen done, reply with a summary under 150 words: what you changed (files), ' +
9 'what you verified, open issues. No full file contents, no long logs.'
10
11export function register(on) {
12 on('agent.spawn', async ($, e, next) => {
13 // Forks always inherit the parent's model; leave them alone
14 if (e.fork) return next(e)
15
16 let tier = e.prompt.match(TAG)?.[1]?.toLowerCase()
17 let source = 'Plan'
18
19 // No tag but the caller named a model explicitly: respect it
20 if (!tier && e.model) return next(e)
21
22 if (!tier) {
23 try {
24 tier = await $.model.classify(
25 `${e.description}\n\n${e.prompt.slice(0, 2000)}`,
26 ['light', 'standard', 'deep'],
27 { model: 'haiku' }
28 )
29 source = 'klassifiziert'
30 } catch {}
31 if (!tier) { tier = 'standard'; source = 'Standard' }
32 }
33
34 try {
35 const { rateLimits = [] } = await $.session.usage()
36 if (rateLimits.some(r => r.percentUsed >= LIMIT_PERCENT) && tier !== STEP_DOWN[tier]) {
37 tier = STEP_DOWN[tier]
38 source += ', Limit-Schutz'
39 }
40 } catch {}
41
42 const model = TIERS[tier]
43 const prompt = e.prompt.replace(TAG, '').trim() + RETURN_RULE
44 const result = await next({ ...e, model, prompt })
45 if (!result.deny) $.ui.log(`${e.description} → ${tier} (${model}) · ${source}`)
46 return result
47 })
48}
49