A token-economical, quality-first model & effort router for Claude Code — now an installable plugin that actively nudges every turn, proves what it saved, and never blocks you.
Claude Code has no built-in router that switches models by task complexity. v1 of this project added one as a static prompt: a triage table the main model was supposed to remember. v2 makes routing active: a hook classifies every prompt and injects a one-line hint, pinned subagents enforce the actual model swap, and /router-stats shows the real dispatches, tokens, and estimated dollars saved.
| Task | Tier | Agent |
|---|---|---|
| broad search / grep / "where is X" | Haiku · low | Explore |
| 1-file edit, rename, format, known command | Haiku · low | quick-haiku |
| standard feature / refactor / tests | Sonnet · medium | worker-sonnet (parallel-friendly) |
| subtle bug / architecture / perf, well-posed | Opus · high | inline or deep-opus |
| complex / novel / ambiguous, substantial plans & design decisions, or a failed tier | Fable · xhigh | deep-fable |
Default main model is Opus 4.8 / high effort (quality-first). Three postures are available (below); the ladder and its agents are identical across all of them — only the starting rung moves.
Active routing, not a memorized table. A UserPromptSubmit hook (bin/classify.sh) reads every prompt, scores it against local heuristics in config/rules.json (pattern match, zero API cost), and — if the result is ambiguous — can optionally fall back to one tiny Haiku call. It then injects a one-line routing hint into context: "this looks like standard → consider dispatching worker-sonnet." The main model doesn't have to remember the ladder anymore; it gets reminded every turn. The hook is fail-open by design: any error, missing dependency, or disabled classifier means no hint and a clean exit — it never blocks or slows a turn.
Fable participates in planning. Substantial plans and architecture/design decisions — "make a plan for X", "what's the best approach", "weigh the options", "how should we structure this" — now route to the complex tier and dispatch deep-fable, instead of being absorbed inline on Opus. Quick, well-posed plans stay on Opus; the point is that the strongest reasoning participates in the hard planning, not that every plan escalates.
Proof, not a promise — who actually ran. A PreToolUse(Task) hook (bin/log-dispatch.sh) logs every subagent dispatch to a local JSONL ledger. Run /router-stats and it leads with a per-tier, per-model delegation table straight from that ledger — the honest answer to "was that Haiku, Sonnet, Opus, or Fable?" It then shows main-loop token usage from the session transcript (labeled as such — a subagent's tokens live in its own context and never enter the main transcript) plus an estimated dollar amount kept off the baseline model.
Ships as a plugin. Install from a marketplace in two commands, or keep using the manual script if you don't want the plugin system. Ships with an English stub by default; a French locale is available in locales/.
Configurable end to end. Every tier's agent/model/effort, three ready-made postures, classifier knobs, and per-model pricing live in config/router.config.json; the classifier's pattern rules live in config/rules.json. Model IDs live only in that one config file, so there are no dated model IDs scattered through prompts to rot.
Two mechanisms, not just "a cheaper model":
- Cheaper tier per task — searches and mechanical edits run on Haiku instead of the flagship, standard work runs on Sonnet, and Opus/Fable are spent only where they earn it.
- Context isolation (the big one) — a subagent reads files in its own context and returns only a short conclusion. Those file contents never enter the main thread, so every later turn stays cheap. File-heavy work is pushed into disposable contexts.
v2 adds a third lever: active enforcement. In v1, savings depended on the main model remembering to delegate. In v2, the classifier hint nudges it every turn, and /router-stats shows whether the nudge is actually being followed. The ledger makes it concrete: you can see exactly how many tasks each tier absorbed, and estimate what that delegation kept off the baseline model — see the sample report below.
Fixed overhead is kept deliberately small: the always-loaded stub is about 15 lines per turn, and the classifier hook is a fast local shell script per prompt (no network call unless the prompt is ambiguous and the Haiku fallback is enabled).
/plugin marketplace add nobodyohm-web/claude-code-model-router
/plugin install model-router
That's it — the agents, skill, hooks, and config load automatically. Restart Claude Code if prompted.
git clone https://github.com/nobodyohm-web/claude-code-model-router && cd claude-code-model-router
./install.sh # idempotent; supports --dry-run to preview changes
# then restart Claude Code./uninstall.sh reverses it cleanly. Use this path if you're not on a Claude Code build with plugin support, or prefer to vendor the files directly into ~/.claude.
Either way, work normally afterwards — the router routes on its own. Force a tier by naming its agent ("use deep-fable for this").
$ /router-stats
Model Router — who did the work
================================
Delegations by tier (from ledger — this is who actually ran):
tier model count share
standard claude-sonnet-5 3 43%
search claude-haiku-4-5 2 29%
complex claude-fable-5 1 14%
mechanical claude-haiku-4-5 1 14%
total 7
Main loop this session (from transcript — main model only, NOT subagents):
claude-opus-4-8 in 210,400 out 12,800 $1.37
Estimated tokens kept off claude-opus-4-8 (rough proxy):
~$0.53 saved by delegating 7 task(s) to cheaper tiers instead of the baseline.
Note: delegation counts are exact (from the ledger). Transcript tokens cover the main loop
only — subagent tokens live elsewhere — so the "saved" figure is an ESTIMATE from the per-tier
averages in config/router.config.json (stats.est_tokens_per_dispatch), priced vs the baseline.
The delegation counts are exact — every dispatch is logged, so the "who ran" table is ground truth. The transcript section covers the main loop only: in real Claude Code a subagent's tokens live in its own context and never enter the main transcript, so they can't be summed there. The "saved" figure is therefore an honest estimate — ledger dispatch counts × the rough per-tier token averages in config/router.config.json (stats.est_tokens_per_dispatch), priced against the baseline model. Edit those averages, the prices, or the baseline and the estimate moves with them — it's not a billed number from Anthropic.
/router-stats also prints an "Effort distribution" section and an effort column on the delegation table, since effort now varies independently of tier (see effort_variants below) — the ledgers log the effort of every classification and dispatch, so you can see not just which tier ran but at what effort.
Everything lives in config/router.config.json (tiers → agent/model/effort, classifier knobs, pricing) and config/rules.json (the classifier's pattern rules, one weighted rule set per tier).
| Profile | model |
effortLevel |
Note |
|---|---|---|---|
quality-first (default) |
opus[1m] |
high |
Opus orchestrates, delegates down, escalates to Fable. |
balanced |
sonnet[1m] |
medium |
Sonnet orchestrates, Opus is the hard tier, Fable for complex. |
max-savings |
haiku |
low |
Haiku pilots and delegates almost everything upward. |
Switch profiles by setting "profile" in config/router.config.json, or hand-edit model/effortLevel in settings.json directly — the agents and rules are identical across postures, only the starting rung moves.
Other useful knobs, all in config/router.config.json:
classifier.enabled/classifier.inject_hint— turn the active hook on/off without uninstalling.classifier.haiku_fallback— whether ambiguous prompts get one small Haiku call (needsANTHROPIC_API_KEY).classifier.confidence_margin/classifier.min_prompt_chars— how decisive a match must be, and how short a prompt can be before it's skipped entirely.telemetry.enabled/telemetry.ledger_path— where dispatches are logged for/router-stats.telemetry.log_classifications/telemetry.classifications_path— whetherclassify.shalso records the routed tier of every prompt (defaults on; path defaults toclassifications.jsonlbeside the dispatch ledger), so/router-statscan show what the router suggested per request even when nothing was delegated.stats.baseline_model/stats.est_tokens_per_dispatch— the baseline the "saved" estimate is measured against, and the rough per-tier token averages it multiplies dispatch counts by (subagent tokens aren't in the transcript, so this is the honest proxy).pricing_usd_per_mtok— per-model input/output prices used for the cost estimate; edit these to match your actual plan.effort_variants/stats.effort_output_multiplier— the tier×effort agents the classifier can route to, and the per-effort output-cost weights/router-statsuses to estimate effort-aware savings.effort_signals(inrules.json) — quick/thorough phrases that modulate effort ±1 independently of the tier.
- Stub (
locales/stub.en.md, appended intoCLAUDE.mdbyinstall.sh, or auto-loaded as a plugin) — the always-on ladder table, so routing is visible every turn without a lookup. - Agents (
agents/*.md) — five tiered subagents, each pinned to amodel+effort. Loaded only when dispatched, so the rich guardrails in their bodies cost nothing until they run. - Skill (
skills/model-router/SKILL.md) — the full policy: decision procedure, escalation protocol, parallelism and supervision guidance, anti-patterns. Lazy-loaded, zero cost until consulted. - Hooks (
hooks/hooks.json) —UserPromptSubmitrunsbin/classify.sh(the active classifier);PreToolUse(Task)runsbin/log-dispatch.sh(the dispatch ledger). bin/scripts —classify.sh(heuristics + optional Haiku fallback, fail-open; also logs each prompt's routed tier to the classifications ledger),log-dispatch.sh(append-only JSONL logger),router-stats.sh(opens with a per-request "router suggestions" table from the classifications ledger, then the dispatch ledger's per-tier/per-model delegation table, main-loop transcript tokens, and an estimate of what delegation kept off the baseline).config/—router.config.json(tiers, profiles, classifier knobs, pricing) andrules.json(classifier pattern rules). The single source of truth for every model ID, so nothing rots when a model is renamed.skills/router-stats/SKILL.md— wires the/router-statscommand tobin/router-stats.shand adds the "these are estimates" caveat automatically..claude-plugin/—plugin.jsonandmarketplace.json, so the whole thing installs as a single Claude Code plugin.- Quality gates:
tests/*.batsexercise the classifier's routing decisions against a labeled prompt corpus, and GitHub Actions runs them (plus shellcheck) on every push.
- No mid-session model swap exists in Claude Code. A hook cannot change the model the main conversation is running on — that's an API-level constraint, not a bug here. Enforcement in v2 is therefore two-layered: pinned-model subagents (the only place the model actually changes) plus the injected hint (the only lever that influences the main model's own behavior). The router steers; it doesn't override.
- The dollar figure is an estimate. The delegation counts in
/router-statsare exact (from the ledger), but the "saved" figure multiplies those counts by the rough per-tier token averages inconfig/router.config.json(stats.est_tokens_per_dispatch) and prices them against the baseline — because a subagent's real tokens never land in the main transcript. Treat "saved" as directionally accurate, not a reconciled invoice. - The Haiku fallback needs
ANTHROPIC_API_KEY. Without it, the classifier still runs — it just relies on the local heuristics inrules.jsonand abstains (no hint) on genuinely ambiguous prompts instead of calling out to Haiku. - Fixed overhead per turn: the always-loaded stub is roughly 15 lines, and the classifier hook runs one fast local script per prompt. Both are designed to cost less than the first delegated search repays.
- The plugin ships with an English stub by default (
locales/stub.en.md); a French version is inlocales/stub.fr.mdif you prefer to route in French — routing behavior itself is language-independent. - No secrets or project data are included — this is pure editor configuration plus local, append-only telemetry ledgers under
~/.claude/router/(dispatches.jsonlandclassifications.jsonl).