AI-generated illustrationRoute Claude Code Work Across Model Tiers and Prove the Savings
Put search on a small model and edits on a mid-tier one, confirm in telemetry which model ran each request, and keep the route only if checked runs spend less.
- Reading
- 69 min
- Workshop
- 120 min
- Stack
- Claude Code 2.1.308 · Node.js 22 · pnpm 12 · OpenTelemetry
- Version
- 1.0.0
- Last verified
- Oct 26, 2026
01Outcomes
What you will build and be able to do
Each outcome has an observable criterion. The lab and the assessment check exactly these, nothing more and nothing less.
OBJ-001
Measure one Claude Code run by model, query source, agent and effort from telemetry, and state its three cost estimates.
The learner's table for the baseline run names the model, query source, agent and effort of every row, and their note gives the telemetry, client and recomputed cost figures with the size and cause of any difference.
OBJ-002
Predict the effective model and effort of the main session and of each subagent from settings files, flags, environment variables and agent definitions, using four precedence ladders.
Each Lab 2 prediction is written before its run and matches the run's telemetry, or the miss is explained from the ladder; in ASM-001, the predictions for unseen configurations match the reference reading.
OBJ-003
Configure a routed setup with pinned model IDs (a Sonnet main session, a read-only Explore override and a researcher on Haiku, a worker on Sonnet and an Opus advisor) and confirm from telemetry that each tier runs where you intended.
The probe table shows every agent on its intended model and effort; the annotated settings diff explains each key and what it overrides.
OBJ-004
Diagnose routing faults from run evidence, name the rung of the ladder that caused each one, and fix it.
For each Lab 3 fault the learner cites the telemetry or output line that shows it, names the rung that caused it, applies a fix and shows the corrected value in a rerun or a recorded run.
OBJ-005
Write a measurement report that defines the workload, compares only repeated runs that passed the quality check by median and range with n, and states a rollback rule.
The report passes the report check, makes no claim beyond the measured runs, and has none of ASM-001's critical failures.
02Prerequisites and setup
Runs live or replays offline
- Claude Code installed and working with an Anthropic API key in ANTHROPIC_API_KEY. This guide never signs in to a subscription account.Claude Code setup
- A terminal running bash 3.2 or later (macOS ships 3.2) on Linux, macOS or WSL, and Git 2.40 or later. The kit's scripts were tested on Linux with bash 5.3 and 4.3, not on a Mac.Install Git
- Node.js 22.18 or later and pnpm 12 on PATH. On Node.js 22 and 24, corepack enable provides pnpm; otherwise follow pnpm's installation page.Download Node.js
- Reading JSON, and setting and unsetting environment variables for one command.
- Dev Guide 05: Claude Code Workflows You Can Verify, or the same experience: headless -p runs with explicit grants, a verifier the change cannot edit, and workspace trust. The lesson's first section restates what this guide uses.Dev Guide 05
export LABS="$PWD/labs" # the labs/ folder of the extracted downloadexport WS="$HOME/routing-lab" # the workspace the labs build; it must not exist yetnode "$LABS/starter/checks/preflight.mjs"PASS Node.js: 22.23.4 (needs 22.18 or later: the fixture runs TypeScript with Node.js's built-in type stripping)PASS pnpm: 12.15.0PASS git: git version 2.53.0PASS Claude Code: 2.1.308, the version the guide's outputs were recorded onPASS ANTHROPIC_API_KEY: set (value not shown)INFO routing variables in this shell: none setPASS port 4318 on 127.0.0.1: free for the telemetry collector preflight: ready for the live route and the offline route03The system picture
Who runs on which model

Extended description
the main session runs on `claude-sonnet-5-5` at effort `medium`, capped at `high`. It sends codebase search to an `Explore` agent and questions about test data to a `researcher` agent, both on `claude-haiku-5-5` at `medium`, capped at `medium`, and the one allowed edit and its checks to a `worker` agent on `claude-sonnet-5-5` at `medium`, capped at `high`. Beside the main session, an advisor on `claude-opus-5-5` answers when the main session consults it: it receives the conversation and returns advice, and it never calls tools or edits files.
Title: "Who runs on which model". Subtitle: "The reference routing of [Lab 2](#lab)". On the left, a box "Ticket T-4207" with an arrow into the main session box, mint: "Main session", `claude-sonnet-5-5`, "medium, cap high", "plans and decides". To its right, joined by a dashed two-headed amber arrow, a dashed amber box: "Advisor", `claude-opus-5-5`, "answers when consulted". From the bottom of the main session box, arrows lead down to three agent boxes: "Explore", `claude-haiku-5-5`, "medium, cap medium", "search" (blue); "researcher", `claude-haiku-5-5`, "medium, cap medium", "data questions" (blue); "worker", `claude-sonnet-5-5`, "medium, cap high", "the edit and its checks" (mint). No arrow joins the advisor to an agent. Footer: "Reference configuration, 2026-10-25".
04Lesson
Three ideas from Guide 05
This guide reuses Guide 05's refund-rules project and three of its ideas. If you have not read it, these commands are enough to follow along.
Headless runs with explicit grants. A claude -p run with --permission-mode dontAsk performs only the actions you list in --allowedTools and rejects the rest without asking (CLI reference; permissions). Every measured run in this guide uses the same grants:
claude -p "Fix ticket T-4207 as TASK.md describes. Follow the routing rules in CLAUDE.md." \ --permission-mode dontAsk \ --allowedTools "Edit(./src/policy.ts)" "Bash(pnpm test)" "Bash(pnpm test:acceptance)" "Bash(pnpm typecheck)" "Bash(pnpm decide *)" "Bash(git diff *)" "Bash(git diff)" \ --max-turns 40 --max-budget-usd 2.00 --no-session-persistence \ --output-format stream-json --verboseA verifier the change cannot edit. Guide 05's scripts/verify-change.ts checks a change from the tagged commit it started at: scope, protected files, typecheck, both test suites and the new test's failure before the change. This guide runs it from the configuration's own commit, so a change cannot weaken the check that judges it.
Workspace trust. A -p session shows no workspace trust dialog and runs a project's hooks and MCP servers even in a folder you have never trusted (headless docs). In recorded runs on 2026-10-25 with Claude Code 2.1.308, the same folder's committed model, modelSettings, advisorModel and env keys applied too. Read a repository's .claude/ folder before running it headless, and in these labs, run only the workspace the lab builds.
How Claude Code chooses a model and an effort
Extended description
the main session runs on claude-sonnet-5-5 at effort medium, capped at high. It sends codebase search to an Explore agent and questions about test data to a researcher agent, both on claude-haiku-5-5 at medium, capped at medium, and the one allowed edit and its checks to a worker agent on claude-sonnet-5-5 at medium, capped at high. Beside the main session, an advisor on claude-opus-5-5 answers when the main session consults it: it receives the conversation and returns advice, and it never calls tools or edits files.
Title: "Who runs on which model". Subtitle: "The reference routing of Lab 2". On the left, a box "Ticket T-4207" with an arrow into the main session box, mint: "Main session", claude-sonnet-5-5, "medium, cap high", "plans and decides". To its right, joined by a dashed two-headed amber arrow, a dashed amber box: "Advisor", claude-opus-5-5, "answers when consulted". From the bottom of the main session box, arrows lead down to three agent boxes: "Explore", claude-haiku-5-5, "medium, cap medium", "search" (blue); "researcher", claude-haiku-5-5, "medium, cap medium", "data questions" (blue); "worker", claude-sonnet-5-5, "medium, cap high", "the edit and its checks" (mint). No arrow joins the advisor to an agent. Footer: "Reference configuration, 2026-10-25". The animation builds this picture box by box, sends dots from the main session to each agent in that agent's hue, and ends on the finished diagram.
Three separate choices decide what a request costs:
- The model of the main session. It comes from the first of four places that sets it: the
/modelcommand during a session, the--modelflag at startup, theANTHROPIC_MODELenvironment variable, or themodelkey in a settings file (model configuration). With none set, the account's default applies; on the Anthropic API on 2026-10-25 that wasclaude-opus-5-5. - The model of each subagent. A subagent definition in
.claude/agents/can name its own model in itsmodelfield; otherwise a variable or the main session's model decides (subagents). The built-inExploreruns on the main conversation's model unless a user or project definition namedExplorereplaces it. - The effort of each request. Effort levels control adaptive reasoning: lower is faster and cheaper, higher reasons more deeply (model configuration). Effort has its own ladder, per-model defaults (
mediumfor Opus 5.5, Sonnet 5.5 and Haiku 5.5) and caps (maxEffortLevel) that limit every source above them.
An advisor adds a fourth choice. With advisorModel set and the advisor available, the main model can consult a stronger model "at key moments during a task" (advisor). The advisor's tokens are billed at the advisor model's rates and are part of the session's totals.
Settings themselves come from several files, and the most specific one wins: managed settings, then command-line settings, then .claude/settings.local.json, then .claude/settings.json, then ~/.claude/settings.json (settings).
The four precedence ladders, from recorded runs
Each ladder below comes from Anthropic's documentation and from a run recorded on 2026-10-25 with Claude Code 2.1.308, in which a higher rung overrides a lower one. The boxes are excerpts of the table table.mjs prints from each run's telemetry (Read a run from its telemetry), trimmed to the rows that matter. Where a run showed something the documentation does not state, the text says so.
Extended description
settings files, from managed settings, then --settings, then .claude/settings.local.json, then .claude/settings.json, to ~/.claude/settings.json. The main session's model, from /model, then --model, then ANTHROPIC_MODEL, then the model key in settings, to the account default. A subagent's model, from the model named for one invocation, then the definition's model field, then CLAUDE_CODE_SUBAGENT_MODEL, to the main model. Effort, from CLAUDE_CODE_EFFORT_LEVEL, then --effort, then modelSettings, then effortLevel, to the model's default, with the result capped by maxEffortLevel. The quick reference adds what the figure leaves out, including /effort and the order for a subagent's effort.
Title: "Four ladders, highest first". Four boxes, two on the left and two on the right, each a numbered list with rung 1, the highest, in mint. "Settings files": 1 managed, 2 --settings, 3 .claude/settings.local.json, 4 .claude/settings.json, 5 ~/.claude/settings.json. "Main session's model": 1 /model, 2 --model, 3 ANTHROPIC_MODEL, 4 model in settings, 5 account default. "Effort, with its cap": 1 CLAUDE_CODE_EFFORT_LEVEL, 2 --effort, 3 modelSettings, 4 effortLevel, 5 model default, then in amber, "capped by maxEffortLevel". "A subagent's model": 1 model for one invocation, 2 definition's model, 3 CLAUDE_CODE_SUBAGENT_MODEL, 4 main model. Footer: "Documentation and recorded runs, 2026-10-25". The animation builds the boxes one rung at a time and ends on the finished picture.
The main session's model
From highest to lowest: /model in the session, --model at startup, ANTHROPIC_MODEL, the model key in settings, the account default (model configuration).
Recorded run LIVE-04 (2026-10-25, Claude Code 2.1.308, effort medium): project settings said "model": "claude-haiku-5-5" and ANTHROPIC_MODEL=claude-sonnet-5-5 was set. The variable won:
model source agent effort requests input output cache read cache write cost.usageclaude-sonnet-5-5 main - medium 1 2 4 11352 5997 $0.0162total 1 2 4 11352 5997 $0.0162Recorded run LIVE-05 added --model claude-haiku-5-5 to the same setup. The flag won over the variable:
model source agent effort requests input output cache read cache write cost.usageclaude-haiku-5-5 main - medium 1 2 4 11352 6773 $0.0010total 1 2 4 11352 6773 $0.0010Settings scopes
From highest to lowest: managed settings, command-line settings (--settings), .claude/settings.local.json, .claude/settings.json, ~/.claude/settings.json (settings).
Recorded run LIVE-06 (2026-10-25, Claude Code 2.1.308, effort medium): the user file said claude-sonnet-5-5, the project file claude-haiku-5-5 and the local file claude-sonnet-5-5. In the run before it, without the local file, the project file had beaten the user file and the session ran on Haiku. With the local file, it ran on Sonnet:
model source agent effort requests input output cache read cache write cost.usageclaude-sonnet-5-5 main - medium 1 2 4 11352 5995 $0.0162total 1 2 4 11352 5995 $0.0162A subagent's model
From highest to lowest: a model named for one invocation, the definition's model field, CLAUDE_CODE_SUBAGENT_MODEL, the main session's model (subagents). model: inherit counts as set in the definition, so it beats the variable. Setting CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1 (Claude Code 2.1.257 or later) applies the variable's model, or the main model, to every subagent and ignores the definitions (environment variables).
This order changed in Claude Code 2.1.251: before it, CLAUDE_CODE_SUBAGENT_MODEL outranked the definition. Notes and scripts written for older versions often assume the old order.
Recorded run LIVE-07 (2026-10-25, Claude Code 2.1.308, effort medium): the researcher's definition said model: claude-haiku-5-5 while CLAUDE_CODE_SUBAGENT_MODEL=claude-sonnet-5-5 was set. The main session was on Haiku too, so only the variable pointed at Sonnet. The definition won:
model source agent effort requests input output cache read cache write cost.usageclaude-haiku-5-5 main - medium 3 6 249 48264 8047 $0.0016claude-haiku-5-5 subagent researcher medium 2 4 240 3790 4395 $0.0007total 5 10 489 52054 12442 $0.0023The built-in Explore is a different case. Recorded run LIVE-08 (same day and version) had no Explore definition, CLAUDE_CODE_SUBAGENT_MODEL=claude-haiku-5-5 and --model claude-sonnet-5-5. The built-in Explore ran on the main model, not on the variable's. The documentation says the variable alone does not change the built-in Explore or Plan; a definition named Explore does.
model source agent effort requests input output cache read cache write cost.usageclaude-sonnet-5-5 main - medium 2 4 219 28858 6542 $0.0214claude-sonnet-5-5 subagent Explore medium 2 4 91 8596 8742 $0.0236total 4 8 310 37454 15284 $0.0451Effort, with its cap
From highest to lowest: an explicit choice (CLAUDE_CODE_EFFORT_LEVEL, --effort or /effort), then settings (the level saved for the model in modelSettings, or an effortLevel key), then the model's default (model configuration). The documentation groups the three explicit choices into one rung; in a recorded run, the variable beat the flag. For a subagent, the definition's effort field outranks the session's level, an effort Claude passes for one invocation outranks the field, and the variable outranks both (subagents). Then a maxEffortLevel cap limits the result, whatever set it, and the lowest cap across scopes applies. The cap can be a top-level key for every model or a field in one model's modelSettings entry (settings reference).
Recorded run LIVE-09 (2026-10-25, Claude Code 2.1.308, claude-haiku-5-5): project settings set Haiku's level to low, the command passed --effort xhigh, and CLAUDE_CODE_EFFORT_LEVEL=max was set. The variable won:
model source agent effort requests input output cache read cache write cost.usageclaude-haiku-5-5 main - max 1 2 687 0 17961 $0.0026total 1 2 687 0 17961 $0.0026Recorded run LIVE-10 added a top-level "maxEffortLevel": "high" to the project settings. The cap held the variable to high:
model source agent effort requests input output cache read cache write cost.usageclaude-haiku-5-5 main - high 1 2 4 11187 6774 $0.0010total 1 2 4 11187 6774 $0.0010Two behaviors in these runs are not spelled out in the documentation as of 2026-10-25, so treat them as observed on 2.1.308 only:
- A subagent without an
effortfield ran at the session's level when that level was set explicitly (--effort highgave the researcherhigh). - With no explicit session level, a subagent ran at its own model's
modelSettingslevel: a Haiku researcher got Haiku's entry, not the Sonnet main session's.
One more trap sits in the user settings file. A top-level effortLevel in ~/.claude/settings.json does not count for Opus 5.5, and models released after it start at their own default (model configuration). In recorded runs on 2026-10-25 it was ignored on Sonnet 5.5 and Haiku 5.5 and applied on Opus 5. Set effort per model in modelSettings instead.
Read a run from its telemetry
Claude Code exports usage as OpenTelemetry metrics and events when CLAUDE_CODE_ENABLE_TELEMETRY=1 and an exporter are set (monitoring). The labs send them over OTLP HTTP/JSON to a small collector the kit runs on 127.0.0.1, and labs/starter/telemetry/table.mjs turns one run's export into a table. In 2.1.308, the console exporter printed nothing for a one-second -p run and mixed object dumps into the run's own output for longer ones, so the kit does not use it.
The variables the kit's runner sets for every run:
CLAUDE_CODE_ENABLE_TELEMETRY=1OTEL_METRICS_EXPORTER=otlp OTEL_LOGS_EXPORTER=otlpOTEL_EXPORTER_OTLP_PROTOCOL=http/jsonOTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318OTEL_METRIC_EXPORT_INTERVAL=5000 OTEL_LOGS_EXPORT_INTERVAL=2000OTEL_LOG_TOOL_DETAILS=1If port 4318 is already in use, the runner stops with a message saying so; stop whatever listens there, or set ROUTING_LAB_PORT to another port. The runner stops the collector when the run ends and checks that the port closed.
Each row of the table is one combination of four attributes of the api_request events and cost counters:
- model, the model that served the request (some models arrive with a date suffix, such as
claude-haiku-4-5-20251001; the table strips it); - query source,
mainfor the main session andsubagentfor an agent's requests (the events saysdk,agent:customoragent:builtin:Explore); - agent, the subagent's name, which telemetry replaces with
customfor agents you define unlessOTEL_LOG_TOOL_DETAILS=1is set (monitoring); - effort, the level the request ran at, absent on models without effort.
Here is the median run of the five baseline runs of the first measurement (Measure the route), recorded as LIVE-01 (2026-10-25 19:11:27 UTC, Claude Code 2.1.308, every agent inheriting claude-opus-5-5, effort medium):
model source agent effort requests input output cache read cache write cost.usageclaude-opus-5-5 main - medium 7 14 2377 137748 13163 $0.1410claude-opus-5-5 subagent Explore medium 5 10 3500 49592 14114 $0.1505claude-opus-5-5 subagent researcher medium 4 8 1572 20744 5390 $0.0626claude-opus-5-5 subagent worker medium 4 8 1022 19334 3359 $0.0411total 20 40 8471 227418 36026 $0.3952cost estimates (none of these is an invoice) telemetry cost.usage, summed: $0.3952 total_cost_usd from the result line: $0.3952 tokens at list price (2026-10-25): $0.3952The three cost lines under the table are estimates, and none is an invoice:
- the sum of the telemetry's
cost.usagecounter; total_cost_usdfrom the run's lastresultline instream-json(a long run can print several cumulativeresultlines; the last one is the run's);- the tokens recomputed at list prices read on 2026-10-25 (pricing).
In every recorded run of the fixed task, the three agreed to within a fraction of a cent, which says the client computes cost from the same token counts and prices. It does not say what you will be billed; cost management explains where the console and invoices come from.
Two columns explain most of the money. Cache read tokens are context the model has seen recently, charged at a fraction of the input price; a run that starts within a few minutes of a similar one reads most of its context from cache. Cache write tokens are context written to the cache, charged above the input price. Compare runs only when their caches started in the same state; the kit's comparison script reports a run whose first main request read nothing from cache as cold.
Configure the route
The routed configuration is two kinds of file, both committed to the repository: project settings and agent definitions.
.claude/settings.json keeps Guide 05's deny rules and adds three keys:
{ "model": "claude-sonnet-5-5", "modelSettings": { "claude-sonnet-5-5": { "effortLevel": "medium", "maxEffortLevel": "high" }, "claude-haiku-5-5": { "effortLevel": "medium", "maxEffortLevel": "medium" } }, "advisorModel": "claude-opus-5-5"}modelmoves the main session from the account default to Sonnet 5.5. Anything higher on the model ladder still overrides it.modelSettingssets each model's effort and caps it. The caps are what keep aCLAUDE_CODE_EFFORT_LEVEL=maxin someone's shell from running every request atmax: in a recorded run with that variable set (Lab 3 case 3), the main session ran athighand the Haiku agents atmedium. How much an uncapped run would cost was not measured.advisorModelnames the advisor. It must rank at or above the main model, or the session runs without it (advisor).
Each agent definition in .claude/agents/ names a full model ID and only the tools the agent needs. The Explore override, which replaces the built-in agent of that name:
---name: Exploredescription: Read-only search of this repository. Use it first, to find the files, functions and data a ticket involves. It reports paths, function names and line numbers and never changes a file.tools: Read, Grep, GlobdisallowedTools: Write, Editmodel: claude-haiku-5-5maxTurns: 12---The researcher is the same shape on claude-haiku-5-5, with omitClaudeMd: true so it does not carry the project's instructions into every data question. The worker runs on claude-sonnet-5-5 with tools: Read, Edit, Bash. A routing block in CLAUDE.md tells the main session which agent does what.
Route by task, not by price. Anthropic's Haiku 5.5 announcement reports Terminal-Bench 4.0 scores of 39.2% for Haiku 5.5 and 70.6% for Sonnet 5.5, and says "Sonnet 5.5 and Opus 5.5 remain better choices for complex agentic coding tasks like those measured by Terminal-Bench 4.0", while Haiku 5.5 "pairs well with Opus 5.5 and Sonnet 5.5 as a subagent on coding work" (Introducing Claude Haiku 5.5, 2026-10-07). So this route gives Haiku the search and the data questions and keeps planning and the edit on Sonnet. Putting every agent on the cheapest model is a different route, and it needs its own measurement.
Pin full model IDs. An alias such as haiku resolves through ANTHROPIC_DEFAULT_HAIKU_MODEL when that variable is set (model configuration). In a recorded run with that variable set to claude-haiku-4-5, an Explore agent with model: haiku ran on Haiku 4.5, with no effort attribute at all. On 2026-10-25, Haiku 4.5 listed at ten times Haiku 5.5's token prices for prompts up to 100,000 tokens, and at twice them for longer prompts (pricing). With model: claude-haiku-5-5, the same variable changed nothing. The full IDs in this guide are the 2026-10-25 names (models overview).
Allowlist the tools. On macOS, Linux and WSL, Claude Code leaves Glob and Grep out of the default tool set and searches through Bash; a subagent that lists them in tools and leaves out Bash gets them back (tools reference). The recorded runs on Linux showed exactly that. The definitions here list what each agent uses and nothing more.
The baseline the labs measure against is the same repository at its spec commit: the same agents, the same routing block and the same deny rules, with every model field set to inherit and no model keys in settings. Every agent then runs on the account default, so the only difference between the two configurations is the routing.
Watch the advisor
The advisor does not appear as a request of its own. In the stream-json output, a consultation is a server_tool_use block named advisor followed by an advisor_tool_result block whose content is encrypted. In telemetry, the advisor's tokens arrive as cost and token counters under the advisor model with no effort attribute and no api_request event; the table prints them on their own row marked (no request of its own). Its cost is included in total_cost_usd.
The advisor can be configured and still never run, with no error. In recorded runs on 2026-10-25 with Claude Code 2.1.308, it stayed off when:
DISABLE_TELEMETRYwas set, orCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICwas set, even to0; both turn off the feature-flag fetch the advisor depends on (advisor; environment variables);CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1was set;- the advisor model ranked below the main model (a Haiku advisor for a Sonnet session).
When the advisor is off, a prompt that asks for it gets a reply such as "no advisor is available"; telemetry export is unaffected. The kit's advisor probe asks for exactly one consultation and prints whether an advisor row appeared.
Recorded run LIVE-03 (2026-10-25 19:16:46 UTC, Claude Code 2.1.308, main session on claude-sonnet-5-5, advisor claude-opus-5-5) used a prompt that asked for one consultation. The advisor's row has no request of its own:
model source agent effort requests input output cache read cache write cost.usageclaude-sonnet-5-5 main - medium 8 18 1996 195064 16033 $0.0796claude-opus-5-5 main - - 0 27314 2036 0 0 $0.1500 (no request of its own)claude-haiku-5-5 subagent Explore medium 4 8 2695 33067 12945 $0.0033claude-haiku-5-5 subagent researcher medium 4 8 2299 31562 13545 $0.0032claude-sonnet-5-5 subagent worker medium 4 8 890 22997 4854 $0.0234total 20 27356 9916 282690 47377 $0.2594cost estimates (none of these is an invoice) telemetry cost.usage, summed: $0.2594 total_cost_usd from the result line: $0.2594 tokens at list price (2026-10-25): $0.2594Configured is not the same as used. In the 5 routed runs of the first measurement, the main session never consulted the advisor on its own; in the 5 of the second, one main session did, unasked, and the advisor's row cost $0.1133 (Measure the route). A variant prompt that ends "Consult the advisor once before your final message." produced 2, 2 and 1 consultations in its 3 runs, and every one passed the quality check. Each consultation reads the whole conversation again, and that read is not cached (advisor): the advisor's own tokens came to $0.1482 to $0.1716 of each run. Those runs cost $0.2594 at the median (range $0.2571 to $0.2811), against $0.0822 for the first measurement's routed runs, which did not consult it. The documentation says pairing a faster main model with a stronger advisor "typically costs less than running the stronger model throughout"; on this task, the consulted runs' median was below the first measurement's baseline median, $0.3952, from sessions that could not run their checks (Measure the route). The variant changes the prompt, so it is a different workload, and its runs are not part of the comparison in Measure the route.
Measure the route
A cost claim is only as good as the runs behind it, and a run only as good as the work it did. This section reports two measurements of the same task. The first found the routed configuration much cheaper, but none of its baseline sessions could run the task's checks. The second gave both configurations the same way to run them and measured again.
The first measurement
The protocol, fixed before the first measured run:
- Workload. Ticket T-4207 in the
refund-rulesproject: a request under a new policy, POL-DMG-14, that the decision tool rejects as an unknown policy. The fix is one edit insrc/policy.ts, but finding it means reading the ticket data, the decision code and the policy table. Same prompt, same fixture (Guide 05's download, version 1.1.0), same flags, Claude Code 2.1.308. - Configurations.
spec, the baseline, androuted, the configuration in Configure the route. - Repetition and order. Five runs of each, alternating baseline and routed, so time of day and cache state do not favor one side.
- Quality. Every run is checked by
labs/starter/checks/quality.mjs: Guide 05's verifier from the configuration's commit (V1 to V8), then six review checks the tests do not cover, including the new policy's last eligible day and the day after it. A policy copied with the wrong window passes the tests and the verifier, and fails this check. - Statistics. The median and the full range of the client estimate, with n, over passing runs only. A failed run is reported and excluded.
- Rollback rule. Return to the baseline when any routed run fails the quality check, when either configuration has fewer than three passing runs, or when the two ranges overlap.
- Runs that stay open. The runner stops a run that has printed its result with no background task pending, 10 seconds later if a wake-up is still scheduled and 30 seconds later otherwise, and records it in
run.json(F-11, T-12). None of this measurement's runs called a scheduling tool, and each ended within a second of its last telemetry event, so none would have been stopped.

Extended description
Title: "Estimated cost per run, ticket T-4207". Subtitle, two lines: "Claude Code 2.1.308, 5 runs each, 10 of 10 passed the quality check" and "First measurement: no baseline session could run its checks". A horizontal dollar axis labeled from $0.00 to $0.40 and running a little past it, with dotted gridlines every $0.10, and two rows. The upper row, "Baseline, n = 5", in white: five dots at $0.2935, $0.3670, $0.3952, $0.4064 and $0.4157, a gray line spanning the range, and a tall mark at the median labeled "median $0.3952". The lower row, "Routed, n = 5", in mint: five dots at $0.0802, $0.0819, $0.0822, $0.0847 and $0.0907, close together, a short range line and a median mark labeled "median $0.0822". Between the rows, an amber bracket from the routed median to the baseline median: "Difference of the medians: $0.3130 (79.2%)". Within a row, a dot's height only keeps close values apart; it carries no value. Footer: "Client estimates, measured 2026-10-25".
| Configuration | n | Passed | Median | Range |
|---|---|---|---|---|
Baseline (spec) | 5 | 5 | $0.3952 | $0.2935 to $0.4157 |
| Routed | 5 | 5 | $0.0822 | $0.0802 to $0.0907 |
The difference of the medians is $0.3130, 79.2% of the baseline's median. The rollback rule is not triggered: every routed run passed, both configurations have at least three passing runs, and the ranges do not overlap.
The median routed run, recorded as LIVE-02 (2026-10-25 19:13:03 UTC, Claude Code 2.1.308, main and worker on claude-sonnet-5-5, Explore and researcher on claude-haiku-5-5, effort medium):
model source agent effort requests input output cache read cache write cost.usageclaude-sonnet-5-5 main - medium 6 12 1302 117877 12439 $0.0559claude-haiku-5-5 subagent Explore medium 5 10 3075 52028 15420 $0.0040claude-sonnet-5-5 subagent worker medium 4 8 803 23032 4783 $0.0223total 15 30 5180 192937 32642 $0.0822cost estimates (none of these is an invoice) telemetry cost.usage, summed: $0.0822 total_cost_usd from the result line: $0.0822 tokens at list price (2026-10-25): $0.0822What the quality check did not see
The quality check runs pnpm test, pnpm test:acceptance and pnpm typecheck itself, after the session, on the code the session left. It does not look at what the session did. TASK.md and CLAUDE.md both tell the session to run those three checks and report each one's pass and fail counts, and no baseline session of the first measurement did. Claude Code rejected every command the 5 baseline sessions wrote for the checks, 17 in all. The run's grants name the checks as Bash(pnpm test), Bash(pnpm test:acceptance) and Bash(pnpm typecheck), and dontAsk mode denies whatever the grants do not cover (T-09). Every rejected command added $? or ${PIPESTATUS[0]} to print an exit status, or ran pnpm --dir <workspace> test, and no accepted command in the 10 runs had any of those. Each baseline session's final message said that the checks had not run. The routed workers wrote the commands in forms the grants cover and ran all three.
So the first measurement compared a baseline that could not run its checks with a route that did. Its numbers are what its protocol produced, and the protocol stands, but $0.3130 is not the price of the same work. The measurement's pilot baseline run showed the same rejections before the measured runs began, and they were missed: nothing in the protocol read what the sessions did.
Telemetry records every permission decision. Each rejected call is a tool_decision event with decision set to reject, and a safety_event whose reason code says what refused it usually follows; the documentation notes that some refused calls have none (monitoring). All 17 here had one, and each gave the reason code mode:dontAsk. The replays keep these events. In the folder where Step 3.2 wrote the ten measured runs, this counts each run's rejected calls:
for r in .runs/M*; do echo "$r $(grep -o '"stringValue":"reject"}},{"key":"source"' "$r/telemetry/logs.jsonl" | wc -l | tr -d ' ')"; done.runs/M01-baseline 3.runs/M02-routed 0.runs/M03-baseline 5.runs/M04-routed 0.runs/M05-baseline 3.runs/M06-routed 0.runs/M07-baseline 3.runs/M08-routed 0.runs/M09-baseline 3.runs/M10-routed 0On the live route, use your own run ids in place of M*. The pattern matches the tool_decision record, which Claude Code 2.1.308 writes with its source attribute right after decision, so each rejected call is counted once and not again for its safety_event.
The second measurement
The second measurement repeated the first with these changes, all fixed before its first run on 2026-10-26:
- Prompt. One sentence added for both configurations: "Run each check as a plain command, exactly as TASK.md writes it, in its own Bash call, with nothing added before or after it." A different prompt is a different workload, so the two measurements are not compared run for run.
- Session check. A run passes only if it passes the quality check and, in the session itself, each of the three checks ran in a call Claude Code did not reject, and the final message names all three.
- Pilot. One run of each configuration with the new prompt, which had to show the baseline session running its checks before the measured runs started. It did.
The task, the two configurations, the flags, the version, the quality check, the statistics and the rollback rule did not change. The runner was the download's own, which stops a run that stays open after its result (F-11, T-12); the first measurement's runner did not.
| Configuration | n | Passed both checks | Median | Range |
|---|---|---|---|---|
Baseline (spec) | 5 | 5 | $0.3813 | $0.3554 to $0.4279 |
| Routed | 5 | 5 | $0.0882 | $0.0857 to $0.2067 |
The difference of the medians is $0.2931, 76.9% of the baseline's median. The rollback rule is not triggered: every routed run passed both checks, both configurations have at least three passing runs, and the ranges do not overlap. Every session ran all three checks. Claude Code rejected 5 calls, each a git -C <workspace> diff or git -C <workspace> status, which the grants do not cover (T-09); none was a check.

Extended description
Title: "Estimated cost per run, ticket T-4207". Subtitle, two lines: "Claude Code 2.1.308, 5 runs each, 10 of 10 passed both checks" and "Second measurement: each session ran the task's three checks". The same axis as the first measurement's chart, labeled from $0.00 to $0.40 and running a little past it, with dotted gridlines every $0.10, and two rows. The upper row, "Baseline, n = 5", in white: five dots at $0.3554, $0.3582, $0.3813, $0.4077 and $0.4279, a gray line spanning the range, and a tall mark at the median labeled "median $0.3813". The lower row, "Routed, n = 5", in mint: four dots close together at $0.0857, $0.0875, $0.0882 and $0.0901, and one well to the right at $0.2067, the run whose main session consulted the advisor; a range line from $0.0857 to $0.2067 and a median mark labeled "median $0.0882". Between the rows, an amber bracket from the routed median to the baseline median: "Difference of the medians: $0.2931 (76.9%)". Within a row, a dot's height only keeps close values apart; it carries no value. Footer: "Client estimates, measured 2026-10-26".
Two routed runs did something no run of the first measurement did:
- One main session consulted the advisor without being asked. The advisor's row cost $0.1133 and made that run $0.2067, the top of the routed range; the other routed runs cost $0.0857 to $0.0901 (Watch the advisor).
- One main session scheduled a wake-up early in the run. The run printed its result with the wake-up still pending, and the runner stopped it 10 seconds later, before the wake-up could add a turn (F-11). It passed both checks.
What the two measurements show, and what they do not
- They show that on this task, on these two days and this version, the route ran each agent on its intended model and spent less. In the second measurement, both configurations also did the work the task asks, including its checks.
- They do not show a saving on other tasks, other repositories or other versions, and they say nothing about a month of use. A task with more exploration, longer context or harder decisions can move the result either way.
- All runs started with a warm cache, so the figures are for back-to-back work, not a first run of the day.
- In the first measurement, the routed main session skipped the researcher in 4 of its 5 runs, and the baseline's main session in 2 of 5; in the second, the routed main session skipped it in 5 of its 5 runs, and the baseline's in 0 of 5. Routing in
CLAUDE.mdis guidance the model weighs, not a rule it must follow; the telemetry table is how you find out what it did. - A check that runs after the session cannot tell you whether the session did its own work. Count each run's rejected calls before you compare (F-12).
Labs
The labs build the measurement workspace from Guide 05's published download, so every learner works on the same files. Each lab has a live route, which calls the API and costs money, and an offline route, which replays a recorded run and costs nothing.
On the live route the labs cost roughly $2.88 in API usage at 2026-10-25 list prices, from the recorded runs; the offline route replays recorded runs and costs nothing.
- Lab 1: Read a baseline run (about 35 minutes). Build the workspace, run the fixed task on the baseline configuration with telemetry on, and read the table by model, query source and agent. You state the run's three cost estimates and the size and cause of any difference.
- Lab 2: Route in layers, predicting first (about 45 minutes). Apply the routing one layer at a time: the session model, the agents' models, effort settings and caps, the advisor, then full-ID pins. Before each probe you write down the model and effort you expect for every agent, then check the prediction against telemetry.
- Lab 3: Find the faults and write the report (about 40 minutes). Diagnose five seeded misconfigurations, fix each one and rerun its probe, then measure both configurations and write the measurement report with its rollback rule.
The download, model-routing-labs.zip, holds the handout (labs/learner-labs.md), the starter kit with Guide 05's fixture and its notice, the agent templates, the telemetry collector and table, the quality and report checks, the offline replay and the Lab 2 solution files. The code is MIT licensed.
Troubleshooting
Each entry quotes what the kit or Claude Code printed in a recorded run or a kit check on 2026-10-25, unless it gives another date.
T-01: Preflight fails on Node.js
FAIL Node.js: ... (needs 22.18 or later: the fixture runs TypeScript with Node.js's built-in type stripping). The fixture's scripts run .ts files directly. Install Node.js 22.18 or later (download Node.js).
T-02: pnpm is missing or not version 12
FAIL pnpm: not on PATH. On Node.js 22 or 24, run corepack enable (Corepack README); Corepack ships with Node.js only before 25.0.0, so on a later Node.js follow pnpm's installation page. The fixture pins pnpm 12.14.0 in its package.json.
T-03: The runner asks for an API key
claude-run: set ANTHROPIC_API_KEY first (this lab uses an Anthropic API key, not a sign-in). Export the key in the terminal that runs the labs, or take the offline route. The runner passes the key through to Claude Code and prints nothing about it.
T-04: The collector's port is in use
claude-run: port 4318 is in use; stop whatever listens there or set ROUTING_LAB_PORT. Another collector, often one left by an interrupted run, still listens. Stop it and confirm the port closed, or run with ROUTING_LAB_PORT=4319.
T-05: A run id is already taken
claude-run: .runs/<id> exists; pick a new run id. Every run keeps its own folder so nothing is overwritten. Choose a new id, such as lab2-L1b.
T-06: A measured run refuses to start
measure: the working tree has changes; commit, stash or move them first (git status lists them, untracked files included). measure.sh checks out a tag and resets the working tree afterward, so it will not start over uncommitted work. An untracked file counts too, and git stash alone leaves untracked files in place. Commit your routing (Lab 2 step 2.7), and write notes and the report after the measured runs or outside the workspace.
T-07: A run stops with a budget error
The last result line says error_max_budget_usd, and the exit code is 1 (LIVE-11). The run reached its --max-budget-usd limit. Count it as a failed run in any comparison; do not raise the limit inside a measured batch.
T-08: The table is empty or says to set a temporality
table: these counters use cumulative temporality; set OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta (the default) and run again. Something in your --env or --env-file changed the exporter settings. Run without it; the runner's own telemetry variables are listed in Read a run from its telemetry. Claude Code sends metrics and events when it exits, so a run killed early can leave the table empty.
T-09: Commands were rejected
The events line shows safety_event entries, and the result line may list permission_denials. In dontAsk mode, Claude Code denies every call that would otherwise ask for permission (permissions). A grant must match each part of a compound command on its own, read-only commands such as echo and tail need no grant, and a grant matches the command as written: Bash(pnpm typecheck) covers pnpm typecheck 2>&1 | tail -40, but Bash(pnpm test) does not cover pnpm --dir <workspace> test, and Bash(git diff) does not cover git -C <workspace> diff. In the recorded runs of 2026-10-25 and 2026-10-26, Claude Code accepted 57 calls that piped a check into tail and rejected 109 Bash calls. Each rejected call ran a command from another folder with --dir or -C, added $? or ${PIPESTATUS[0]}, ran a command held in a variable, or printed a variable with echo, though a plain echo needs no grant; no accepted call did any of these. Each rejection's safety_event gave the reason code mode:dontAsk (monitoring). A rejection is the grant working. It is a failure when it blocks what the task asks for: a session whose checks were rejected never ran them, even when the quality check passes afterward (F-12). Count each run's rejected calls before you compare (Measure the route).
T-10: A quality check fails on scope
FAIL Q1 verifier from <tag>: verdict FAIL; failing V2 together with FAIL Q2 only src/policy.ts changed. The run edited a file outside the task's scope. Report the run as failed and keep it out of every median.
T-11: Replay will not overwrite a run
replay: .runs/<id> already holds a run that was not replayed; move it before replaying <id>. A live run of yours has the same id as a recorded one. Move your folder; replayed folders carry a REPLAYED.txt.
T-12: The runner stopped a run after its result
claude-run: the run printed its result with a wake-up still scheduled and no background task pending; stopped it 10 s later, before the wake-up could add a turn. See <run-dir>/stopped-after-result.jsonclaude-run: the run printed its result and was still running 30 s later with no background task pending; stopped it. See <run-dir>/stopped-after-result.jsonThe first means the main session scheduled a wake-up (F-11); the second, that the process stayed open after its work for some other reason. The note lists every scheduling call and the rule that applied. run.json keeps Claude Code's own exit code as claude_exit_code and sets exit_code to 0 only when the last result has subtype success and is_error false; an API error prints a result with subtype success and is_error true. The quality check still decides whether the run passed.
Quick reference
| Ladder | Order, highest first | Source |
|---|---|---|
| Main session model | /model in the session; --model; ANTHROPIC_MODEL; model in settings; account default | Model configuration |
| Settings scopes | managed; --settings; .claude/settings.local.json; .claude/settings.json; ~/.claude/settings.json | Settings |
| Subagent model | the model for one invocation; the definition's model (including inherit); CLAUDE_CODE_SUBAGENT_MODEL; the main model. CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1 applies the variable's model, or the main model, to all. Built-in Explore follows the main model unless a definition replaces it | Subagents |
| Effort, main session | CLAUDE_CODE_EFFORT_LEVEL; --effort or /effort; modelSettings for the model; top-level effortLevel (not in user settings for Opus 5.5 and later models); the model default. Then the lowest maxEffortLevel caps the result | Model configuration |
| Effort, a subagent | CLAUDE_CODE_EFFORT_LEVEL; an effort Claude passes for one invocation; the definition's effort field; then, observed on 2.1.308, the session's explicit level, or else the subagent model's own modelSettings level. Then the lowest maxEffortLevel caps the result | Subagents |
The advisor runs only when advisorModel ranks at or above the main model and nothing turns off feature-flag fetching (DISABLE_TELEMETRY, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC set to any value) or the tool (CLAUDE_CODE_DISABLE_ADVISOR_TOOL) (advisor).
Commands from the kit, run from the workspace:
bash <labs>/starter/run/probe.sh <run-id> [--env NAME=value] [--advisor] # which model and effort each agent getsbash <labs>/starter/run/measure.sh <tag> <run-id> # one measured run with the quality checknode <labs>/starter/telemetry/table.mjs .runs/<run-id> # the table for one runnode <labs>/starter/telemetry/compare.mjs --baseline spec .runs/<ids>... # medians, ranges and the rollback rulenode <labs>/starter/checks/report-check.mjs report.md # a report against the runs it citesnode <labs>/starter/replay/replay.mjs [<id>] # the offline routeBefore you compare, count each run's rejected calls (Measure the route, F-12). This counts them for the ten recorded runs; for your own runs, put their ids in place of M*:
for r in .runs/M*; do echo "$r $(grep -o '"stringValue":"reject"}},{"key":"source"' "$r/telemetry/logs.jsonl" | wc -l | tr -d ' ')"; done05Hands-on lab
Lab 2: Route in layers, predicting first
You start from the baseline commit and add the routing one layer at a time: the session's model, each agent's model, effort with caps, the advisor, and then a check that full model IDs hold against a stale pin. Before every probe, write down the model and effort you expect for the main session and each agent. A probe asks each agent one small question, so it costs a few cents; the offline route replays the recorded probe instead. When a prediction misses, name the rung of the ladder that explains it before you move on. Set LABS to the download's labs/ folder and work in the workspace from Lab 1, on a branch from spec: cd "$WS" && git switch -c routing spec. Each probe has an offline twin that replays the recorded run and costs nothing: run node "$LABS/starter/replay/replay.mjs" with the same run id, such as lab2-L0.
Acceptance checks
- Every prediction either matched its probe or has a sentence naming the rung that explains the miss.
- node "$LABS/solutions/check-lab-002.mjs" compares your settings.json, as JSON, and your agent files, line by line, with the reference, and its last line says CHECK-2.7: PASS.
Predict the model and effort of every agent, then probe the baseline (L0, about $0.12 live)
$ bash "$LABS/starter/run/probe.sh" lab2-L0model source agent effort requests input output cache read cache write cost.usage claude-opus-5-5 main - medium 2 4 736 28818 7973 $0.0604 claude-opus-5-5 subagent Explore medium 2 4 478 7440 1426 $0.0182 claude-opus-5-5 subagent researcher medium 2 4 285 6868 1532 $0.0147 claude-opus-5-5 subagent worker medium 3 6 741 13135 2762 $0.0313 total 9 18 2240 56261 13693 $0.1246Add "model": "claude-sonnet-5-5" to .claude/settings.json; the agents still say model: inherit. Predict, then probe (L1, about $0.08)
$ bash "$LABS/starter/run/probe.sh" lab2-L1model source agent effort requests input output cache read cache write cost.usage claude-sonnet-5-5 main - medium 5 10 682 88249 9928 $0.0405 claude-sonnet-5-5 subagent Explore medium 2 4 269 4326 4506 $0.0144 claude-sonnet-5-5 subagent researcher medium 2 4 156 3772 4599 $0.0134 claude-sonnet-5-5 subagent worker medium 2 4 223 4810 5062 $0.0154 total 11 22 1330 101157 24095 $0.0837Set model: claude-haiku-5-5 in Explore.md and researcher.md and model: claude-sonnet-5-5 in worker.md. Predict, then probe (L2, about $0.05)
$ bash "$LABS/starter/run/probe.sh" lab2-L2model source agent effort requests input output cache read cache write cost.usage claude-sonnet-5-5 main - medium 5 10 801 88246 10004 $0.0419 claude-haiku-5-5 subagent Explore medium 2 4 215 4324 4499 $0.0007 claude-haiku-5-5 subagent researcher medium 2 4 167 3814 4641 $0.0007 claude-sonnet-5-5 subagent worker medium 2 4 278 7928 2032 $0.0087 total 11 22 1461 104312 21176 $0.0519Add the modelSettings block from Configure the route, then probe with --effort xhigh, as someone might for a hard ticket (L3, about $0.05)
$ bash "$LABS/starter/run/probe.sh" lab2-L3 -- --effort xhighmodel source agent effort requests input output cache read cache write cost.usage claude-sonnet-5-5 main - high 5 10 733 88251 9914 $0.0410 claude-haiku-5-5 subagent Explore medium 2 4 244 7418 1399 $0.0004 claude-haiku-5-5 subagent researcher medium 2 4 175 6894 1555 $0.0004 claude-sonnet-5-5 subagent worker high 2 4 291 9401 708 $0.0056 total 11 22 1443 111964 13576 $0.0473Add "advisorModel": "claude-opus-5-5", predict whether an advisor row appears, and run the advisor probe (L4, about $0.12)
$ bash "$LABS/starter/run/probe.sh" lab2-L4 --advisormodel source agent effort requests input output cache read cache write cost.usage claude-sonnet-5-5 main - medium 1 4 83 30197 7808 $0.0234 claude-opus-5-5 main - - 0 20427 508 0 0 $0.0919 (no request of its own) total 1 20431 591 30197 7808 $0.1152 final reply Advisor: yes, reading CLAUDE.md first is sound (it's already loaded in context), and the next step is to read TASK.md and then start with the Explore agent.Probe with a stale pin a teammate's shell might set (L5, about $0.05)
$ bash "$LABS/starter/run/probe.sh" lab2-L5 --env ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5model source agent effort requests input output cache read cache write cost.usage claude-sonnet-5-5 main - medium 5 10 759 93571 11284 $0.0452 claude-haiku-5-5 subagent Explore medium 2 4 203 8779 2816 $0.0005 claude-haiku-5-5 subagent researcher medium 2 4 273 8253 2966 $0.0006 claude-sonnet-5-5 subagent worker medium 2 4 263 10759 1915 $0.0085 total 11 22 1498 121362 18981 $0.0548Commit .claude/, tag it routed, annotate git diff spec routed -- .claude/settings.json one line per key, then compare with the reference
$ node "$LABS/solutions/check-lab-002.mjs"SAME .claude/settings.json SAME .claude/agents/Explore.md SAME .claude/agents/researcher.md SAME .claude/agents/worker.md CHECK-2.7: PASSShow the solution discussion
The reference routing is in the lab download, in labs/solutions/lab-002/: .claude/settings.json and the three agent files. node "$LABS/solutions/check-lab-002.mjs" compares yours with it, and bash "$LABS/solutions/apply-lab-002.sh" "$WS" --force applies it if you want to start Lab 3 from the reference.
Each recorded table above answers its prediction. Where your prediction missed, find the rung that decided the cell in The four precedence ladders, from recorded runs: the settings model in L1, each definition's model field in L2, the per-model cap in L3, the advisor row with no request of its own in L4, and the full model IDs that a stale alias pin cannot move in L5.
More labs in this guide
Lab 1: Read a baseline run
Build the workspace, run the fixed task on the baseline configuration with telemetry on, and read the table by model, query source and agent. You state the run's three cost estimates and the size and cause of any difference. About 35 minutes.
Lab 3: Find the faults and write the report
Diagnose five seeded misconfigurations, fix each one and rerun its probe, then measure both configurations and write the measurement report with its rollback rule. About 40 minutes.
06Failure gallery
Eleven ways this breaks, and how you will know
Eight of these reproduce in the lab; the other three were observed in this guide's recorded runs. Learn the symptom first; it is what you will actually see.
- F-01
user-effort-high.jsonReproducibleThe user settings' effort is ignored
- Symptom
- On the spec commit (git switch --detach spec; git switch routing returns), run bash "$LABS/starter/run/probe.sh" f01 --home-settings "$LABS/starter/settings/user-effort-high.json" -- --model claude-sonnet-5-5. The runner copies the file, {"effortLevel": "high"}, into the run's throwaway home as ~/.claude/settings.json. The main session on Sonnet 5.5 runs at medium, not high.
- Diagnosis
- A top-level effortLevel in user settings does not count for Opus 5.5 or later models (model configuration). In recorded runs on 2026-10-25 it was ignored on Sonnet 5.5 and Haiku 5.5 and applied on Opus 5.
- Fix
- Set effort per model in modelSettings, or use --effort for one session.
- Verify
- Probe again with the fix, and check that the main session's row shows the level you set.
- F-02
case-3-hard-ticketsReproducibleAn effort variable outranks every settings file
- Symptom
- Lab 3 case 3, whose env file exports CLAUDE_CODE_EFFORT_LEVEL=max. Runs cost more. In the case's before-probe, the main session and the worker ran at high, though modelSettings sets Sonnet to medium.
- Diagnosis
- The variable is the highest rung of the effort ladder (see Effort, with its cap), so every request runs at the highest level the caps allow, whatever any settings file or agent definition says. The routed configuration's caps held it to high for Sonnet and medium for Haiku; without caps it runs at max.
- Fix
- Unset the variable, and use --effort for the one session that needs more.
- Verify
- The after-probe, without the env file, shows the main session and every agent at medium.
- F-03
case-2-quiet-telemetryReproducibleThe advisor never runs
- Symptom
- Lab 3 case 2, which adds DISABLE_TELEMETRY to the project settings' env. The advisor probe with --env CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=0, or with advisorModel set to claude-haiku-5-5 on the routed commit, does the same. No error appears. The advisor's row is missing, and the reply says no advisor is available.
- Diagnosis
- The advisor depends on a feature-flag fetch that DISABLE_TELEMETRY and CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC turn off, and it runs only when the advisor model ranks at or above the main model (advisor).
- Fix
- Remove the variable, or name an advisor that ranks at or above the main model.
- Verify
- The advisor probe shows a claude-opus-5-5 row marked (no request of its own), as case 2's after-probe did.
- F-04
case-1-short-model-nameReproducibleA stale pin moves Explore to an older model
- Symptom
- Lab 3 case 1: model: haiku in Explore.md, with ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-haiku-4-5 in the case's env file. Explore's row says claude-haiku-4-5, with no effort attribute.
- Diagnosis
- The alias haiku resolves through ANTHROPIC_DEFAULT_HAIKU_MODEL when that variable is set (model configuration).
- Fix
- Use full model IDs in agent definitions.
- Verify
- With model: claude-haiku-5-5 back and the pin still set, the after-probe shows Explore on claude-haiku-5-5 at medium.
- F-05
case-4-one-variableReproducibleThe subagent variable no longer overrides a definition
- Symptom
- Lab 3 case 4, whose env file sets CLAUDE_CODE_SUBAGENT_MODEL=claude-haiku-5-5, and A subagent's model. With the variable set, the worker still runs on claude-sonnet-5-5, the model its definition names. A prediction table or script built on the old order is wrong for every agent that names a model.
- Diagnosis
- Since Claude Code 2.1.251, a definition's model field outranks CLAUDE_CODE_SUBAGENT_MODEL; before it, the variable won (subagents).
- Fix
- Predict with the current order (see A subagent's model), and update any table or script that assumes the old one.
- Verify
- In both of case 4's probes, the worker's row says claude-sonnet-5-5 while the variable names Haiku.
- F-06
case-4-one-variableReproducibleThe built-in Explore runs on the main model
- Symptom
- Lab 3 case 4, which also deletes .claude/agents/Explore.md. Explore's row says claude-sonnet-5-5, the main session's model, though CLAUDE_CODE_SUBAGENT_MODEL names Haiku.
- Diagnosis
- With no Explore definition, the built-in agent runs on the main session's model, and CLAUDE_CODE_SUBAGENT_MODEL alone does not change that. A user-level Explore in ~/.claude/agents/ is outranked by a project one (subagents); observed on 2026-10-25 with Claude Code 2.1.308, a project Explore on Sonnet beat a user Explore on Haiku.
- Fix
- Keep a project definition named Explore with its own model.
- Verify
- With the file restored and the variable still set, the after-probe shows Explore on claude-haiku-5-5 at medium.
- F-07
case-5-which-agentReproducibleEvery agent is called custom
- Symptom
- Lab 3 case 5, whose env file sets OTEL_LOG_TOOL_DETAILS=0. The subagent rows say custom instead of the agents' names. In the case's before-probe, the two Haiku agents share one custom row and the worker has another.
- Diagnosis
- Without OTEL_LOG_TOOL_DETAILS=1, telemetry replaces the names of agents you define with custom (monitoring). The table makes one row for each model, query source, agent and effort, so agents with the same name and model share a row.
- Fix
- Set the variable in the runner that exports telemetry. It also logs tool inputs, so keep the collector on 127.0.0.1 and the workspace synthetic.
- Verify
- The after-probe, without the env file, shows Explore, researcher and worker on rows of their own.
- F-08
report-check.mjsReproducibleA saving from list prices, one run or failed runs
- Symptom
- labs/starter/checks/report-check.mjs fails a report whose medians or ranges do not come from its cited runs, or that projects beyond them. A report states a saving its runs do not show, and report-check.mjs prints a FAIL line for each such value.
- Diagnosis
- A price sheet times an imagined token count is an estimate of a cost, not a measured saving; a single run cannot show its own spread; and a run that failed the quality check bought nothing.
- Fix
- Repeated runs, passing runs only, median and range with n.
- Verify
- node "$LABS/starter/checks/report-check.mjs" report.md prints report: PASS (CHECK-3.3).
- F-10Observed
A budget stop is an error
- Symptom
- Recorded run LIVE-11 (2026-10-25, Claude Code 2.1.308, claude-haiku-5-5), a reading task with --max-budget-usd 0.002. The run stopped with result subtype error_max_budget_usd, is_error true and exit code 1, after spending $0.00218, slightly more than the limit.
- Diagnosis
- The run reached its --max-budget-usd limit. The flag is a stop rather than an exact ceiling.
- Fix
- Treat that exit code as a failed run in any comparison, and do not raise the limit inside a measured batch (T-07).
- Verify
- Before you compare, check that each run's run.json has exit_code 0. compare.mjs decides a pass from the quality check alone, so it does not catch a budget stop by itself.
- F-11Observed
A scheduled wake-up keeps a headless run open
- Symptom
- Observed in the lab walks (2026-10-25, Claude Code 2.1.308). The run printed its result and stayed open until the wake-up fired, and then the model took the turn it had scheduled. In one routed run the wake-up was scheduled at 20:08:49 for 60 seconds, the result came at 20:09:20 and the wake-up fired at 20:10:00; that turn re-ran the tests the run had already reported and raised its estimate from $0.0828 to $0.0953. In one baseline run the wake-up was 1,200 seconds, and the run sat open for almost nine minutes after its result until it was stopped by hand.
- Diagnosis
- Claude Code's tools reference describes ScheduleWakeup as the tool that schedules the next iteration of a self-paced /loop, one minute to one hour out. In these -p runs, which used no loop, the main session sometimes called it to wait for its agents. On 2.1.307 through the Agent SDK, an open issue reports the reverse, a process that exits and a wake-up that never fires (anthropics/claude-code#104161).
- Fix
- The kit's runner now stops such a run 10 seconds after its result while a wake-up is still scheduled (T-12). The delay runs from the call, not from the result, so a wake-up scheduled more than 50 seconds before the result can still come due inside those 10 seconds and add a turn: before comparing runs, search each stream.jsonl for ScheduleWakeup.
- Verify
- In the second measurement (see Measure the route), one routed main session scheduled a wake-up 60 seconds out, and the runner stopped the run 10 seconds after its result, before the wake-up came due. Its stopped-after-result.json lists the call.
- F-12Observed
The baseline never ran its checks
- Symptom
- Observed in the first measurement (2026-10-25, Claude Code 2.1.308). Reproducible offline: replay the ten measured runs and count each run's rejected calls (see Measure the route). The comparison looked clean: every run passed the quality check, the ranges did not overlap, and the rollback rule was not triggered. But Claude Code rejected every command the baseline sessions wrote for the checks, 17 in all, so no baseline session ran them.
- Diagnosis
- The grants cover each check only as the plain command, and dontAsk mode denies the rest (T-09). The quality check could not see it, because it runs the checks itself after the session, so the costs compared sessions that could not finish the task with sessions that did.
- Fix
- Count the rejections before you compare. When they hit the checks, give both configurations a prompt that names the exact check commands, count the session's own checks toward a pass, and measure again.
- Verify
- In the second measurement, every session ran all three checks, and none of its 5 rejected calls was a check (see Measure the route).
07Assessment
Transfer task and rubric
Review a team's routing change: annotate its settings diff, predict the model and effort of every agent under four configurations you have not seen, and write the measurement report.
| Criterion | 0 · Emerging | 1 · Developing | 2 · Proficient | 3 · Exemplary |
|---|---|---|---|---|
| R1 Annotated change | Changes are restated without what overrides them, or at most one change that will not work as expected is named | Each change has what it sets, but some overrides are missing, or only some of the changes that will not work as expected are named | Each change has what it sets and what overrides it; most changes that will not work as expected are named | Every change that will not work as expected is named, with the evidence a reviewer would check |
| R2 Predictions | Fewer than 20 of the 32 model and effort cells are right | At least 20 of the 32 cells are right | At least 28 of the 32 model and effort cells are right, each with a rung | All 32 cells are right, and every rung is the one that decides the cell |
| R3 Workload and runs | The workload or the configurations are not defined, or the report cites no runs | The report leaves out runs the protocol includes, or cites runs it does not | The report defines the workload and both configurations, cites the protocol's runs and only those, and names the quality check | As Proficient, and every run left out is named with the reason |
| R4 Arithmetic | No medians or ranges, or figures from list prices, a single run or failed runs | Medians and ranges from the cited runs, but a value disagrees with the passing runs, or `report-check.mjs` fails | Medians, ranges and the difference come from the passing runs the protocol defines, with n, and `report-check.mjs` passes | As Proficient, at the precision of the runs |
| R5 Decision | No decision, or a rollback answer that contradicts the protocol's rule | The rollback answer follows the rule, but the decision does not, or the limits are missing | The decision follows the protocol's rollback rule, and the limits name what the runs do not show | As Proficient, and the decision says what would have to change before measuring again |
Proficiency standard: Proficient or higher on every criterion. Proposed local standard, not an official certification.
The full brief
You receive a case file: a team's proposed settings.json change with its agent files, four configurations you have not seen, and run records constructed for the assessment. You hand in:
The annotated diff of the proposed settings.json, naming what each key does and what it overrides.
A prediction table of the effective model and effort for the main session and every agent under each unseen configuration, with the ladder rung that decides each cell.
A measurement report written from the template, defining the workload, comparing only passing runs by median and range with n, and stating a rollback rule.
Each criterion is scored Emerging (0), Developing (1), Proficient (2) or Exemplary (3). To pass, every criterion must be Proficient or higher. Any one of these fails the submission outright: a savings claim not backed by passing runs; a prediction table that misplaces CLAUDE_CODE_EFFORT_LEVEL or the subagent order; a report built from a personal configuration. The learner checks (report-check.mjs) catch arithmetic that does not match the cited runs; they cannot tell whether a reading is right, so an instructor grades the predictions and the judgment.
08Instructor kit
Teach it to a room
Facilitation guide
A run of show for a 12-person lab of about 130 minutes, with misconceptions to listen for, three live demonstrations, hint ladders, troubleshooting in the room, a shared spend cap for the live route, and accessibility notes.
A 100-person day
Every learner on the offline route and nobody needing a key, the facilitator running the three demonstrations live on the projector, one helper per 20 to 25 learners, and a checkpoint at the end of each lab.
Dry run and observation
A proposed 70-minute dry run with one candidate and one observer: what the candidate teaches in each segment and what the observer watches for, a readiness rubric, a proposed decision rule and when to observe again. Nobody has been observed with it yet.
Clearance record
A template, one copy per facilitator after a scored dry run: the workshop version, the Claude Code version taught, the audience and route, the evidence, the decision and when to observe again. No facilitator has been cleared yet.
Evaluation plan and pass-rate report
A proposed plan for the diagnostic questions, exit ticket, follow-up and feedback, data handling and second scoring, and a template that reports a cohort by outcome and criterion with its denominators, scorer agreement and limits. The learner pilot is pending.
10Sources and verification
- [1]Model configuration · code.claude.comchecked Oct 25, 2026
- [2]Settings reference · code.claude.comchecked Oct 25, 2026
- [3]Settings · code.claude.comchecked Oct 25, 2026
- [4]Environment variables · code.claude.comchecked Oct 25, 2026
- [5]Create custom subagents · code.claude.comchecked Oct 25, 2026
- [6]Advisor · code.claude.comchecked Oct 25, 2026
- [7]Monitoring · code.claude.comchecked Oct 26, 2026
- [8]Manage costs effectively · code.claude.comchecked Oct 25, 2026
- [9]Run Claude Code programmatically · code.claude.comchecked Oct 25, 2026
- [10]CLI reference · code.claude.comchecked Oct 25, 2026
- [11]Tools reference · code.claude.comchecked Oct 26, 2026
- [12]Advanced setup · code.claude.comchecked Oct 25, 2026
- [13]Pricing · platform.claude.comchecked Oct 26, 2026
- [14]Models overview · platform.claude.comchecked Oct 25, 2026
- [15]Advisor tool · platform.claude.comchecked Oct 25, 2026
- [16]Introducing Claude Haiku 5.5 · anthropic.comchecked Oct 25, 2026
- [17]Changelog · code.claude.comchecked Oct 25, 2026
- [18]@anthropic-ai/claude-code · npmjs.comchecked Oct 25, 2026
- [19]Node.js release index · nodejs.orgchecked Oct 25, 2026
- [20]Corepack README · github.comchecked Oct 25, 2026
- [21]Installation · pnpm.iochecked Oct 25, 2026
- [22]Configure permissions · code.claude.comchecked Oct 26, 2026
- [23]Dev Guide 05: Claude Code Workflows You Can Verify · jordanhudgens.comchecked Oct 25, 2026
- [24]anthropics/claude-code issue #104161 · github.comchecked Oct 26, 2026
Independent work. Not affiliated with or endorsed by any vendor named in this guide.
