Key Takeaways
- Subagent model pins in Claude Code frontmatter (rank 3 of 4) can silently drop out, sending agents to your expensive session model.
- Use a PreToolUse hook to block Task calls to frontier models unless pinned.
What Changed — The Silent Model Pin Failure
You pin a subagent to model: sonnet in its frontmatter. You dispatch it two hundred times a day. It works. But somewhere between releases, that pin stops being honored — and your cheap, mechanical agents quietly run on your most expensive session model (Fable or Opus 5). No error. No warning. Just a token bill that doesn't match your gut feeling.
Thomas Witt hit this exact issue. He runs Claude Code with a big session model plus a zoo of subagents — gateway agents, formatters, checkers. At some point, token consumption stopped matching what he'd actually done. Nothing broke. Everything worked. It just cost more.
Why It Works — The 4-Layer Resolution Order
Claude Code resolves which model a subagent runs on in four layers. The frontmatter pin — the one that's documented, obvious, and writable once — sits at 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.
The problem is amplified because the agents you bother to pin are, by definition, the ones you dispatch most often and look at least. They're the mechanical ones — fetch logs, filter, format, check. The kind of job that needs "a model that is obedient, not brilliant." Running those on Fable or Opus 5 is "paying frontier prices for grep with good manners."
How To Apply It — Verify Your Pins Now
Here's the fix. Don't trust the frontmatter. Add a PreToolUse hook that blocks Task calls to expensive models unless the subagent has an explicit pin.

Example hook — in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Task",
"hook": "node ~/.claude/hooks/check-subagent-model.mjs"
}
]
}
}
The hook script reads the subagent's frontmatter. If model is missing or set to a frontier model (Fable, Opus), it blocks the dispatch with a clear message:
// check-subagent-model.mjs
import { readFile } from 'fs/promises';
import { join } from 'path';
const agentDir = join(process.env.HOME, '.claude', 'agents');
const subagentName = process.env.CLAUDE_SUBAGENT_NAME; // set by Claude Code
if (!subagentName) {
process.exit(0); // not a subagent call
}
const agentFile = join(agentDir, `${subagentName}.md`);
const content = await readFile(agentFile, 'utf8');
const modelMatch = content.match(/^model:\s*(.+)$/m);
const model = modelMatch ? modelMatch[1].trim() : null;
const expensive = ['fable', 'opus'];
if (!model || expensive.some(m => model.toLowerCase().includes(m))) {
console.error(`BLOCKED: Subagent ${subagentName} has no model pin or is pinned to an expensive model. Add 'model: sonnet' to its frontmatter.`);
process.exit(1); // blocks the Task call
}
process.exit(0);
Verify your version's precedence: Precedence behavior changes between Claude Code releases. Check the current docs or test with a debug subagent that echoes its model name. Add this to your subagent's prompt: "Print the model you are running on." If it says Fable when you pinned Sonnet, your pin is broken.
Audit your existing agents: Run this one-liner to list all agents and their pins:
for f in ~/.claude/agents/*.md; do echo "$f: $(grep '^model:' $f || echo 'NO PIN')"; done
Any agent with NO PIN or a frontier model is a candidate for the hook.
The Takeaway
Subagents are good — they run in their own context window, fetch a lot, filter, and return a little. But if you're paying frontier prices for that, you're wasting money. Pin them properly, verify the pin holds, and add a hook to catch silent failures. The crash is painful; the silent cost is worse.
Source: thomas-witt.com







