Routes every delegated agent to the cheapest model tier that can do the job well, at an explicit effort, with spawn limits and a routing guide Claude always…

A Claude Code plugin marketplace.
| Plugin | What it does |
|---|---|
clean-view | Hides tool calls, diffs and command output, and shows a calm checklist of Claude's plan above the prompt, with a row for each helper Claude starts. /simple on or /simple off switches it. Also holds Agent Dock: pick a Team Size from 1 to 100 and every request is split across that many helpers, each shown live in a pane. /dock opens or folds it, /dock 10 sets the size. |
status-bar | One colored line under the prompt: plan, model, effort, context, 7-day and 5-hour limits, and running agents by model and effort. |
model-routing | Routes every delegated agent to the cheapest model tier that can do the job well, at an explicit effort. Refuses agents with no model, fills in effort by tier, blocks spawns from haiku agents and past two levels, and keeps a routing guide in Claude's instructions. |
mods-menu | A ☰ Mods button beside the Clean View toggle. It opens a small menu listing your mods with their controls: Clean View on or off, the Agent Dock and its Team Size, and the mods that are always on. /mods opens or closes it. |
prompt-upgrade | Turns a rough prompt into a great one before it is sent. /better <prompt> opens a pane that asks a few quick multiple-choice questions, rewrites the prompt, and shows your original and the upgrade side by side; nothing runs until you press Run. /better on checks every typed prompt first: one that already has what it needs is sent as typed, one that falls short opens the pane, and the same prompt sent twice goes straight through. Press Enter twice to send a prompt as typed. A Detail setting (Auto, Low, Medium or High; /better level auto) sets the questions and how long and deep the upgrade is: Low, Medium and High ask up to 3, 5 or 7; Auto also turns automatic mode on, asks as many questions as needed (up to 9) and, if you don't answer within 30 seconds or press Do it for me, fills in the rest and sends the upgrade itself. /better history shows the last 20. |
At the prompt of a terminal session:
/plugin install clean-view --marketplace ataege/claude-skills
/plugin install status-bar --marketplace ataege/claude-skills
/plugin install model-routing --marketplace ataege/claude-skills
/plugin install mods-menu --marketplace ataege/claude-skills
/plugin install prompt-upgrade --marketplace ataege/claude-skills
Answer y to add the marketplace, then pick a scope with Enter.
From a shell:
claude plugin install clean-view --marketplace ataege/claude-skills
claude plugin install status-bar --marketplace ataege/claude-skills
claude plugin install model-routing --marketplace ataege/claude-skills
claude plugin install mods-menu --marketplace ataege/claude-skills
claude plugin install prompt-upgrade --marketplace ataege/claude-skills
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with ataege/claude-skills, then Add plugin.
Each plugin lives in plugins/<name>. Check one with:
claude plugin validate plugins/<name>
claude plugin test plugins/<name>hooks/register.ts 9 lines1import type { Register } from 'claude-code'
2
3import { registerModelRouting } from './model-routing'
4
5// Each mod registers itself here, so more can sit beside model routing later.
6export const register: Register = on => {
7 registerModelRouting(on)
8}
9hooks/model-routing.ts 221 lines1// Model routing: each delegated agent runs on the cheapest tier whose judgment is enough, at an explicit effort.
2import { atom, read, update } from 'claude-code'
3import type { EngineInterface, On } from 'claude-code'
4
5import type { ModelRoutingAgent, ModelTier } from '../types'
6
7// ---------------------------------------------------------------------------
8// Tiers, defaults and limits
9// ---------------------------------------------------------------------------
10
11type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'
12
13const TIER_RANK: Readonly<Record<ModelTier, number>> = { haiku: 1, sonnet: 2, opus: 3, fable: 4 }
14
15// The effort a tier runs at when Claude names the tier but not the effort (the skill's routing table).
16export const DEFAULT_EFFORT: Readonly<Record<ModelTier, Effort>> = {
17 haiku: 'low',
18 sonnet: 'medium',
19 opus: 'high',
20 fable: 'xhigh',
21}
22
23// Agent types with no model of their own: without `model` they run on the session's model.
24const UNPINNED_AGENT_TYPES: ReadonlySet<string> = new Set(['general-purpose', 'claude'])
25
26// Agents nest at most this deep: the main conversation's agents are depth 1.
27const MAX_SPAWN_DEPTH = 2
28// The agent table keeps the most recent spawns only.
29const MAX_TRACKED_AGENTS = 300
30
31const agentsAtom = atom({ plugin: 'model-routing', key: 'agents' } as const, {})
32const typeTiersAtom = atom({ plugin: 'model-routing', key: 'typeTiers' } as const, {})
33
34type Engine = EngineInterface
35
36/** The tier a model alias or id names: "claude-haiku-5-5" and "haiku" are both haiku. */
37export function tierOf(model: unknown): ModelTier | null {
38 if (typeof model !== 'string') return null
39 const family = model.toLowerCase().match(/haiku|sonnet|opus|fable/)?.[0]
40
41 return (family as ModelTier | undefined) ?? null
42}
43
44function isAboveCeiling(requested: ModelTier, sessionTier: ModelTier | null): boolean {
45 return sessionTier !== null && TIER_RANK[requested] > TIER_RANK[sessionTier]
46}
47
48// ---------------------------------------------------------------------------
49// What Claude reads: the always-on guide and the fixes for refused calls
50// ---------------------------------------------------------------------------
51
52/** The routing guide added to Claude's instructions whenever agent tools are available. */
53export function routingGuide(sessionTier: ModelTier | null): string {
54 const session = sessionTier ?? 'the session model'
55 return `# Model routing
56Delegated work runs on the cheapest tier whose judgment is enough. This session runs on ${session}, which is the ceiling for delegated work, not the default. Effectiveness comes first: never pick a tier too weak for the task to save money.
57
58On every Agent call, set \`model\` and \`effort\` yourself as this table says. This is the explicit instruction the Agent tool's effort field asks for. Before a task's first spawn, state the routing in one line, like \`Routing: 6 × haiku/low (rename batches), 1 × opus/high (JWT design).\`
59
60| Subtask | model | effort |
61|---|---|---|
62| Deterministic: count, sort, grep, a sed-able rename | no agent, use the shell | – |
63| Output fixed by the input plus a template, a wrong result is cheap to spot: renames, short summaries, extracting fields, formatting, run a command and report | haiku | low |
64| Must decide what matters: scoped research, locating code, explaining a subsystem, drafting, a contained change with a clear spec | sonnet | medium |
65| Judging or verifying findings, design tradeoffs, migrations, auth, payments, data changes | opus | high, or xhigh when regressions are costly |
66| Final synthesis that must hold everything in view | ${session} | leave effort out |
67| Core of a long-horizon problem, only when the session is on fable | fable | xhigh or max |
68
69Prices in $ per million tokens in / out: haiku 0.10 / 0.50, sonnet 2 / 10, opus 4 / 20, fable 10 / 50. Judge cost per finished task, not per call.
70
71- Split a task that mixes tiers into one agent per tier and fan out. Never send it whole to the strongest tier.
72- A fork always runs on your own model and effort: fork only when the agent truly needs your full context, never for cheap work.
73- Workflow scripts: set model and effort on every agent() call and leave both out only for the final synthesis. Finders over auth, crypto, payments, migrations or raw data access run on opus, because a miss there is never caught downstream.
74- Haiku agents never spawn agents, and agents nest at most two levels deep. The model-routing mod refuses these spawns.
75- A subagent that needs more judgment returns to its parent to re-scope instead of escalating itself. A haiku stage that fails twice moves to sonnet instead of a third try.
76- If the session's own model looks mismatched to the task, say so once and let the user switch with /model.`
77}
78
79/** The fix for an Agent call that names no model. */
80export function missingModelMessage(sessionTier: ModelTier | null): string {
81 const session = sessionTier ?? 'the session model'
82 return `Model routing: set \`model\` and \`effort\` on this Agent call, then call again. Without \`model\` it runs on the session's own model (${session}), the most expensive choice. Pick the cheapest tier whose judgment is enough:
83- haiku, effort low: output fixed by the input plus a template, a wrong result is cheap to spot (renames, short summaries, extracting fields, formatting, run a command and report)
84- sonnet, effort medium: must decide what matters (scoped research, locating code, explaining a subsystem, drafting, a contained change with a clear spec)
85- opus, effort high (xhigh when regressions are costly): judging or verifying findings, design tradeoffs, migrations, auth, payments, data changes
86- ${session} with effort left out: final synthesis that must hold everything in view
87Split a task that mixes tiers into one call per tier.`
88}
89
90function aboveCeilingMessage(requested: ModelTier, sessionTier: ModelTier): string {
91 return `Model routing: this session runs on ${sessionTier}, which caps delegated work. Use ${sessionTier} or a cheaper tier for this agent. If it truly needs ${requested}, tell the user and let them switch the session with /model.`
92}
93
94const HAIKU_SPAWN_MESSAGE =
95 'Model routing: haiku agents do not spawn agents. Finish what you can, and return to your parent with ' +
96 'whatever needs more judgment so it can re-scope.'
97
98const DEPTH_MESSAGE =
99 'Model routing: agents nest at most two levels deep. Do this part yourself, or return to your parent so it ' +
100 'can spawn the work.'
101
102function workflowMessage(sessionTier: ModelTier | null): string {
103 const session = sessionTier ?? 'the session model'
104 return (
105 `Model routing: none of this script's agent() calls set \`model\`, so every agent would run on ${session}. ` +
106 'Set model and effort on each agent() call: scan or transform stages on haiku/low, find or draft stages on ' +
107 'sonnet/medium, verify or judge stages on opus/high. Leave both out only for the final synthesis. Finders ' +
108 'over auth, crypto, payments, migrations or raw data access run on opus.'
109 )
110}
111
112/** True when a workflow script spawns agents but never names a model for any of them. */
113export function workflowLacksModels(script: unknown): boolean {
114 if (typeof script !== 'string') return false
115 return /\bagent\s*\(/.test(script) && !/\bmodel\s*:/.test(script)
116}
117
118// ---------------------------------------------------------------------------
119// Bookkeeping
120// ---------------------------------------------------------------------------
121
122async function sessionTierOf($: Engine): Promise<ModelTier | null> {
123 try {
124 return tierOf(await $.session.model())
125 } catch {
126 return null
127 }
128}
129
130/** Records a spawned agent, keeping the table to its most recent entries. */
131async function rememberAgent($: Engine, agentId: string, agent: ModelRoutingAgent): Promise<void> {
132 await update($, agentsAtom, agents => {
133 const entries = Object.entries({ ...agents, [agentId]: agent })
134 return Object.fromEntries(entries.slice(-MAX_TRACKED_AGENTS))
135 })
136}
137
138// ---------------------------------------------------------------------------
139// Hooks
140// ---------------------------------------------------------------------------
141
142export function registerModelRouting(on: On): void {
143 // The routing guide, whenever Claude can spawn agents.
144 on('prompt.compose', async ($, e, next) => {
145 const composed = await next(e)
146 const offersAgents = e.tools.includes('Agent') || e.tools.includes('Workflow')
147 if (!offersAgents || e.traits.includes('teammate')) return composed
148
149 return {
150 sections: [
151 ...composed.sections,
152 { id: 'model-routing:guide', text: routingGuide(tierOf(e.model)), scope: 'session' },
153 ],
154 }
155 })
156
157 // Every Agent call: a model for agents that have none, an effort for every tier, nothing above the session.
158 on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
159 const agentType = e.subagent_type ?? 'general-purpose'
160 // Forks always run on the caller's model and effort: nothing to route.
161 if (agentType === 'fork') return next(e)
162
163 const sessionTier = await sessionTierOf($)
164 const requestedTier = tierOf(e.model)
165
166 if (requestedTier === null) {
167 // An agent type with no model of its own would run on the session's model.
168 if (UNPINNED_AGENT_TYPES.has(agentType)) return { deny: missingModelMessage(sessionTier) }
169
170 // A typed agent may pin its model: fill the effort only once its tier is known.
171 if (e.effort !== undefined) return next(e)
172 const typeTier = (await read($, typeTiersAtom))[agentType]
173 if (typeTier === undefined || typeTier === sessionTier) return next(e)
174 return next({ ...e, effort: DEFAULT_EFFORT[typeTier] })
175 }
176
177 if (sessionTier !== null && isAboveCeiling(requestedTier, sessionTier)) {
178 return { deny: aboveCeilingMessage(requestedTier, sessionTier) }
179 }
180
181 // A cheaper tier with no effort would inherit the session's: give it its own.
182 // The session's own tier without effort is the final-synthesis case, so it inherits.
183 if (e.effort === undefined && requestedTier !== sessionTier) {
184 return next({ ...e, effort: DEFAULT_EFFORT[requestedTier] })
185 }
186
187 return next(e)
188 }).catch(($, e, next) => next(e)) // Never block work on a fault of this mod.
189
190 // Workflow scripts whose agents would all run on the session's model.
191 on('tool.call', { tool: 'Workflow' }, async ($, e, next) => {
192 if (workflowLacksModels(e.script)) return { deny: workflowMessage(await sessionTierOf($)) }
193 return next(e)
194 }).catch(($, e, next) => next(e))
195
196 // Every spawn, from any source: haiku agents never spawn, and nesting stops at two levels.
197 on('agent.spawn', async ($, e, next) => {
198 if (e.resume !== undefined) return next(e)
199
200 const agents = await read($, agentsAtom)
201 const parent = e.parentAgentId === undefined ? undefined : agents[e.parentAgentId]
202 // An agent spawned before this mod loaded is taken to sit at depth 1.
203 const parentDepth = e.parentAgentId === undefined ? 0 : (parent?.depth ?? 1)
204
205 if (parent?.tier === 'haiku') return { deny: HAIKU_SPAWN_MESSAGE }
206 if (parentDepth + 1 > MAX_SPAWN_DEPTH) return { deny: DEPTH_MESSAGE }
207
208 const spawned = await next(e)
209 if (spawned.deny === undefined && spawned.agentId !== undefined) {
210 const tier = tierOf(spawned.model)
211 await rememberAgent($, spawned.agentId, { tier, depth: parentDepth + 1, type: e.subagentType })
212 // The tier a type runs on when no model is named: its pinned model, or the caller's.
213 if (tier !== null && e.model === undefined && !e.fork) {
214 await update($, typeTiersAtom, typeTiers => ({ ...typeTiers, [e.subagentType]: tier }))
215 }
216 }
217
218 return spawned
219 }).catch(($, e, next) => next(e))
220}
221types/index.d.ts 26 lines1// Model routing's shared state: other mods can read which tier each agent runs on.
2
3/** The four model tiers, cheapest first. */
4export type ModelTier = 'haiku' | 'sonnet' | 'opus' | 'fable'
5
6/** One spawned agent as the mod tracks it. */
7export type ModelRoutingAgent = {
8 /** The tier it runs on; null when the model named no known tier. */
9 tier: ModelTier | null
10 /** 1 for an agent the main conversation spawned, 2 for one an agent spawned. */
11 depth: number
12 /** The agent type, like "general-purpose" or "Explore". */
13 type: string
14}
15
16declare module 'claude-code' {
17 interface PluginState {
18 'model-routing': {
19 /** Every agent spawned this session, by agent id. */
20 agents: Record<string, ModelRoutingAgent>
21 /** The tier each agent type ran on when no model was named, by type. */
22 typeTiers: Record<string, ModelTier>
23 }
24 }
25}
26