cd /news/ai-tools/i-stopped-my-claude-code-subagents-f… · home topics ai-tools article
[ARTICLE · art-98847] src=thomas-witt.com ↗ pub= topic=ai-tools verified=true sentiment=· neutral

I stopped my Claude Code subagents from running on Fable instead of Sonnet

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.

read6 min views1 publishedAug 16, 2026
I stopped my Claude Code subagents from running on Fable instead of Sonnet
Image: source

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.

At 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.

So 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.

Disclaimer: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.

Before 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.

The 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.

Which 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.

My list of agents that should never touch a frontier model:

Pinning one looks like this, in .claude/agents/cloudwatch-digger.md

:

---
name: cloudwatch-digger
description: Queries CloudWatch Logs and returns only the relevant lines
model: sonnet
tools: Bash, Read
---

One line. model: sonnet

. 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.

Claude Code resolves which model a subagent runs on in this order:

rank layer how you set it
1 environment variable shell / launch config
2 per-invocation parameter
model on the dispatch itself
3 agent frontmatter
model: in .claude/agents/<name>.md
4 session model whatever you started the session with

Four 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.

That 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.

So 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.

A crash is polite, it tells you. This doesn’t tell you anything. It shows up four weeks later as a number.

I 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.

Big 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.

Which 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.

So don’t remember. Enforce.

PreToolUse

runs 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

, refuse the dispatch and say exactly what to re-send.

These 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.

.claude/hooks/enforce-subagent-model.sh

:

#!/bin/bash
{ read -r T; read -r S; read -r M; } < <(jq -r '.tool_name//"",.tool_input.subagent_type//"",.tool_input.model//""')
case $T in Task|Agent) ;; *) exit 0;; esac
[ -n "$S" ] && [ -z "$M" ] || exit 0
P=$(awk '{sub(/\r$/,"")} NR==1&&$0=="---"{f=1;next} f&&$0=="---"{exit} f&&/^model:[ \t]/{gsub(/["'"'"']/,"",$2);print $2;exit}' \
     "${CLAUDE_PROJECT_DIR:-.}/.claude/agents/$S.md" 2>/dev/null)
case $P in ""|inherit) exit 0;; esac
echo "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
exit 2

Nine lines. jq

reads the hook payload from stdin, awk

reads 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.

Don’t forget:

chmod +x .claude/hooks/enforce-subagent-model.sh

In .claude/settings.json

:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Task",
        "hooks": [
          { "type": "command", "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-subagent-model.sh" }
        ]
      }
    ]
  }
}

Test it by dispatching a pinned agent without a model. You should get the BLOCKED message, and the very next dispatch should carry the pin.

The interesting part of a guardrail is its exceptions:

model: inherit

or no model

key at all means inheritance is the intent. Fine, pass..claude/agents

, nothing pinned, nothing to enforce.jq

can’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.

An empty or null

model, 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.

Workflow-tool internal agent() spawns don’t go through PreToolUse. The hook covers

Task

and Agent

dispatches 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.

And 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.

If you find a cleaner way to close the workflow-tool gap, let me know as well!

── more in #ai-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-stopped-my-claude-…] indexed:0 read:6min 2026-08-16 ·