{"slug": "i-stopped-my-claude-code-subagents-from-running-on-fable-instead-of-sonnet", "title": "I stopped my Claude Code subagents from running on Fable instead of Sonnet", "summary": "A developer discovered that Claude Code subagents pinned to run on the cheaper Sonnet model were silently falling back to the session model, Fable, due to a frontmatter pin bug, causing unexpected cost increases. The pin in the agent's frontmatter is rank 3 of 4 in model resolution precedence, and across several releases it has dropped out, leaving no error or warning. The recommended workaround is to pass the model explicitly on the dispatch, which is rank 2 and has never been the layer that breaks.", "body_md": "I run Claude Code with a big session model, Fable or Opus 5, plus a small zoo of subagents doing the boring parts. Gateway agents, formatters, checkers. The kind of mechanical stuff you pin to a small model once, in the agent’s frontmatter, and then never think about again.\n\nAt some point the token consumption stopped matching my gut feeling of what I’d actually been doing that week. Nothing was broken. Nothing errored. Everything worked. It just cost more than it should have.\n\nSo I decided to take a deeper look at how Claude Code actually picks the model for a subagent. It turned out the pin I’d been trusting sits in the one layer you shouldn’t trust.\n\nDisclaimer:This is what I run on my own machine, on my own projects. Hooks can block your own dispatches, that’s the whole point of this one, so if you wire it up wrong you’ll be staring at a blocked Task call and wondering why. Also, precedence behaviour in Claude Code changes between releases. Verify against the version you’re on.\n\nBefore the complaining starts, let me be clear about one thing: subagents are good. This post is not an argument against them, it’s an argument for pinning them properly.\n\nThe reason to use one isn’t that it’s a smaller model. It’s that it runs in its **own context window**. It goes off, does something loud and messy, and hands back a short answer. The noise never lands in your main session. You get the three lines that matter instead of the four megabytes they came from.\n\nWhich means the ideal subagent job looks like this: *fetch a lot, filter, return a little*. And that job needs a model that is obedient, not brilliant. Something like Sonnet does it all day. Running it on Fable or Opus 5 is paying frontier prices for grep with good manners.\n\nMy list of agents that should never touch a frontier model:\n\nPinning one looks like this, in `.claude/agents/cloudwatch-digger.md`\n\n:\n\n```\n---\nname: cloudwatch-digger\ndescription: Queries CloudWatch Logs and returns only the relevant lines\nmodel: sonnet\ntools: Bash, Read\n---\n```\n\nOne line. `model: sonnet`\n\n. That’s the whole pin, and that’s exactly why it hurts when it silently stops working. The agents you bother to pin are, by definition, the ones you dispatch most often and look at least.\n\nClaude Code resolves which model a subagent runs on in this order:\n\n| rank | layer | how you set it |\n|---|---|---|\n| 1 | environment variable | shell / launch config |\n| 2 | per-invocation parameter |\n`model` on the dispatch itself |\n| 3 | agent frontmatter |\n`model:` in `.claude/agents/<name>.md`\n|\n| 4 | session model | whatever you started the session with |\n\nFour layers. And the one that everybody actually uses, the frontmatter pin, because it’s the one that’s documented, obvious and writable once, is rank 3 of 4.\n\nThat would be fine if rank 3 always held. It doesn’t. Across several releases the frontmatter layer has silently dropped out, and pinned agents fell straight through to rank 4: the session model. Which in my case is Fable.\n\nSo the cheap little agent you dispatch two hundred times a day quietly runs on the most expensive thing you have. And here’s the part I find genuinely annoying: there is no signal. No error, no warning, nothing in the transcript that looks different. The agent does its job. It just does it at a multiple of the price.\n\nA crash is polite, it tells you. This doesn’t tell you anything. It shows up four weeks later as a number.\n\nI wasn’t the first one to run into this. There’s a whole class of upstream reports about frontmatter pins being ignored after an update, and the workaround people keep confirming is always the same: pass the model explicitly on the dispatch. That’s rank 2, one layer *above* frontmatter, and rank 2 has never been the layer that breaks.\n\nBig shoutout to everyone who bothered to file those issues with reproductions. Silent cost regressions are exactly the kind of bug nobody files, because nobody notices.\n\nWhich leaves an obvious problem: “just always pass the model explicitly” means the orchestrator has to remember it, every single time, forever. An orchestrator that reliably remembers a thing forever is not something I’ve met.\n\nSo don’t remember. Enforce.\n\n`PreToolUse`\n\nruns before a tool call goes through and can block it with exit code 2. So: if a subagent is pinned in its frontmatter, and the dispatch carries no explicit `model`\n\n, refuse the dispatch and say exactly what to re-send.\n\nThese few lines of code can save you a lot of tokens, because they remind Claude Code to use the model you actually chose for your subagents.\n\n`.claude/hooks/enforce-subagent-model.sh`\n\n:\n\n``` bash\n#!/bin/bash\n{ read -r T; read -r S; read -r M; } < <(jq -r '.tool_name//\"\",.tool_input.subagent_type//\"\",.tool_input.model//\"\"')\ncase $T in Task|Agent) ;; *) exit 0;; esac\n[ -n \"$S\" ] && [ -z \"$M\" ] || exit 0\nP=$(awk '{sub(/\\r$/,\"\")} NR==1&&$0==\"---\"{f=1;next} f&&$0==\"---\"{exit} f&&/^model:[ \\t]/{gsub(/[\"'\"'\"']/,\"\",$2);print $2;exit}' \\\n     \"${CLAUDE_PROJECT_DIR:-.}/.claude/agents/$S.md\" 2>/dev/null)\ncase $P in \"\"|inherit) exit 0;; esac\necho \"BLOCKED: '$S' is pinned (model: $P) but this dispatch has no explicit 'model'. Re-dispatch with model: \\\"$P\\\". A deliberate different model also passes, but it must be explicit.\" >&2\nexit 2\n```\n\nNine lines. `jq`\n\nreads the hook payload from stdin, `awk`\n\nreads the pin out of the agent’s frontmatter, and the message on stderr goes back to Claude Code, which then re-dispatches correctly on its own.\n\nDon’t forget:\n\n```\nchmod +x .claude/hooks/enforce-subagent-model.sh\n```\n\nIn `.claude/settings.json`\n\n:\n\n```\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"Task\",\n        \"hooks\": [\n          { \"type\": \"command\", \"command\": \"$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-subagent-model.sh\" }\n        ]\n      }\n    ]\n  }\n}\n```\n\nTest it by dispatching a pinned agent without a model. You should get the BLOCKED message, and the very next dispatch should carry the pin.\n\nThe interesting part of a guardrail is its exceptions:\n\n`model: inherit`\n\nor no `model`\n\nkey at all means inheritance is the intent. Fine, pass.`.claude/agents`\n\n, nothing pinned, nothing to enforce.`jq`\n\ncan’t read the payload, all three variables come back empty and the hook exits 0.That last one is deliberate: this is a cost guardrail, not a security boundary. A hook that fails closed on a payload it doesn’t understand will eventually wedge a session at the worst possible moment, and then I’ll disable it, and then I’ll be back where I started.\n\nAn empty or `null`\n\nmodel, by the way, is *not* treated as a choice. It falls straight through to frontmatter, the exact layer under suspicion, so it gets gated like an absent one.\n\n**Workflow-tool internal agent() spawns don’t go through PreToolUse.** The hook covers\n\n`Task`\n\nand `Agent`\n\ndispatches only. Anything spawned inside a workflow tool sails right past it. I cover those with prose in my reference docs instead, which is a nice way of saying they’re not covered. Know where your gate ends.If you’re running Claude Code on Fable or Opus 5 and your token graph looks steeper than your week felt, check your pinned agents before you check anything else. Everything that’s supposed to be cheap is worth verifying, because the failure mode here doesn’t announce itself. It just quietly bills you.\n\nAnd if you’re not using subagents at all yet, that’s a whole other topic, and honestly a bigger one than this post. Let me know if you’d like to read it and I’ll write it up.\n\nIf you find a cleaner way to close the workflow-tool gap, let me know as well!", "url": "https://wpnews.pro/news/i-stopped-my-claude-code-subagents-from-running-on-fable-instead-of-sonnet", "canonical_source": "https://thomas-witt.com/blog/blog-subagent-model-pin/", "published_at": "2026-08-16 15:29:22+00:00", "updated_at": "2026-08-16 15:40:41.345110+00:00", "lang": "en", "topics": ["ai-tools", "ai-agents", "developer-tools"], "entities": ["Claude Code", "Sonnet", "Fable", "Opus 5"], "alternates": {"html": "https://wpnews.pro/news/i-stopped-my-claude-code-subagents-from-running-on-fable-instead-of-sonnet", "markdown": "https://wpnews.pro/news/i-stopped-my-claude-code-subagents-from-running-on-fable-instead-of-sonnet.md", "text": "https://wpnews.pro/news/i-stopped-my-claude-code-subagents-from-running-on-fable-instead-of-sonnet.txt", "jsonld": "https://wpnews.pro/news/i-stopped-my-claude-code-subagents-from-running-on-fable-instead-of-sonnet.jsonld"}}