cd /news/ai-agents/three-claude-code-subagent-files-thr… · home › topics › ai-agents › article
[ARTICLE · art-140318] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Three Claude Code subagent files, three models in frontmatter: what each launch cost, and the field the validator waved through

A developer measured how Claude Code 2.1.278 handles the model and effort keys in subagent YAML frontmatter, running seven claude -p invocations against three custom agents (bulk-reader, deep-reviewer, preflight-reviewer) to capture first-request token usage, the model that actually answered, and whether a file-level effort setting overrides a parent session running at a different level. The probe also tested what the harness does with an unrecognized frontmatter key, reading per-subagent JSONL transcripts under ~/.claude/projects that expose message.model, message.usage and a top-level effort field.

by read10 min views3 publishedSep 27, 2026

We keep three custom Claude Code subagents in .claude/agents/: a bulk reader that fetches pages and logs so the main conversation does not have to, an independent reviewer that judges things without seeing the parent's reasoning, and a pre-ship reviewer that runs three checklists in one body. Each file pins a model and an effort level in its YAML frontmatter. Until this week we had never checked whether those two lines do anything. This is the measurement: what one launch of each agent costs on its first API request, which model actually answered, whether effort survived a parent session running at a different level, and what Claude Code does with a frontmatter key it has never heard of.

Everything below was run on Claude Code 2.1.278 on macOS, on 2026-09-22, with seven claude -p invocations in a throwaway directory. The documentation quotes come from https://code.claude.com/docs/en/sub-agents, fetched the same day with trafilatura.

Stripped of their Japanese system prompts, the three definitions are almost identical in shape. Each has four frontmatter keys and nothing else:

---
name: bulk-reader
description: <one sentence on when to delegate reading work here>
model: sonnet
effort: xhigh
---
---
name: deep-reviewer
description: <one sentence on when an independent verdict is needed>
model: opus
effort: xhigh
---
---
name: preflight-reviewer
description: <one sentence on the three-perspective pre-ship review>
model: inherit
effort: high
---

None of the three sets tools, so each inherits the full tool pool. The bodies are short: 5 to 8 bullet rules about quoting primary sources, separating verdicts from evidence, and never writing to anything public. The system prompt the longest one actually received was 1,702 characters, of which 422 were our file and the rest were the harness's own notes for subagents. That number matters later, when we ask where the launch tokens go.

You need a directory that is not your repo, so that your own instruction files do not pollute the number, and one agent file to launch.

D=$(mktemp -d)
mkdir -p "$D/.claude/agents"
cp .claude/agents/bulk-reader.md "$D/.claude/agents/"
cd "$D"
claude plugin validate .claude/agents
claude -p --output-format json --permission-mode default \
  "Use the Agent tool to launch the subagent named bulk-reader (subagent_type: bulk-reader) exactly once, with exactly this prompt: \"This is a measurement probe. Do not read anything, do not call tools. Return 'ok'.\" Do not read any files yourself and do not call any other tool. When it returns, reply with only the subagent's reply verbatim." \
  > run1.json

Two places hold the answer. The JSON on stdout has a modelUsage object keyed by model, so if the subagent ran on a different model than the parent you get its usage as a separate bucket for free. The transcript is more precise: Claude Code writes the subagent's own JSONL at ~/.claude/projects/<cwd with slashes replaced by dashes>/<session_id>/subagents/agent-<id>.jsonl, and each assistant record there carries message.model, message.usage, and, new to us, a top-level effort key. This reads the first request:

SID=$(python3 -c "import json;print(json.load(open('run1.json'))['session_id'])")
P=~/.claude/projects/$(pwd | sed 's#/#-#g')/$SID/subagents
python3 - "$P"/agent-*.jsonl <<'EOF'
import json, sys
for line in open(sys.argv[1]):
    r = json.loads(line)
    if r.get("type") == "assistant":
        u = r["message"]["usage"]
        print(r["message"]["model"], r.get("effort"),
              u["cache_creation_input_tokens"], u["cache_read_input_tokens"],
              u["input_tokens"], u["output_tokens"],
              u.get("output_tokens_details", {}).get("thinking_tokens"))
        break
EOF

Add --effort low to the claude -p line for a second run and you can see whether the file's effort beats the session's. That is the whole experiment; the rest of this post is what came out of it.

The sub-agents page has a table titled "Supported frontmatter fields", introduced by one sentence: "The following fields can be used in the YAML frontmatter. Only name and description are required." The rows we rely on:

model: "Model to use: sonnet, opus, haiku, fable, a full model ID such as claude-opus-5, or inherit. When you omit it, Claude Code picks the model in the subagent model order."effort: "Effort level when this subagent is active. Overrides the session effort level. Default: inherits from session. Options: low, medium, high, xhigh, max; available levels depend on the model"tools: "Tools the subagent can use, as a comma-separated string such as Read, Grep, Bash or a YAML list. Inherits every tool available to subagents if omitted." The resolution order for the model is spelled out too: "The per-invocation model parameter", then "The subagent definition's model frontmatter, where inherit selects the main conversation's model", then "The CLAUDE_CODE_SUBAGENT_MODEL environment variable", then "The main conversation's model". So a model: line in the file loses only to an explicit model argument on the Agent tool call. We checked the parent transcripts: in all seven runs the Agent call carried exactly four keys, description, prompt, run_in_background, subagent_type, and no model. Whatever model the child ran on, the file decided it.

On unknown keys the page says nothing directly. The section "Subagent files Claude Code skips" lists five conditions, all about name, description, the opening ---, and YAML that does not parse. A key the schema does not know is not among them. The validator is described just as narrowly: "Claude Code checks only the directory you name, and doesn't flag a file whose frontmatter parses but has no name." We read that as "an unknown key is silently accepted" and then tested it, because reading is not measuring.

The first request of each agent, taken from its transcript. cache_read was 0 in every cold run, so the whole prefix was written into the prompt cache on that request.

agent model in transcript effort in transcript cache_creation input output thinking
bulk-reader ( model: sonnet ) claude-sonnet-5 xhigh 20,971 2 842 837
deep-reviewer ( model: opus ) claude-opus-5 xhigh 16,262 2 4 0
preflight-reviewer ( model: inherit ) claude-fable-5-1 high 16,222 2 4 0

The parent in every run was claude-fable-5-1, the model set in our user settings, so inherit resolved to the parent as documented, and opus resolved to claude-opus-5 rather than to the parent's family. The stdout JSON priced the two foreign-model buckets at list rates: $0.0609 for the sonnet launch and $0.1017 for the opus one (costBasis: "list" in the same object). The inherited launch is not priced separately because modelUsage merges it into the parent's fable bucket; you can only see it in the transcript.

Two things about those numbers surprised us. First, 16,000 to 21,000 tokens for a subagent whose system prompt is under 2,000 characters. The transcript explains it: before the first request Claude Code attaches a skill_listing of 20,740 characters (every skill installed on this machine, including plugin ones), a deferred_tools_delta naming 67 tools, and the tool schemas themselves. The temporary directory had no CLAUDE.md, and this machine has no user-level one, so none of that is instruction files. It is the harness. Your agent file is a rounding error inside its own launch cost.

Second, the sonnet run at effort: xhigh spent 837 thinking tokens deciding to say "ok", and a repeat launch spent 199. The opus run at the same xhigh and the fable run at high spent 0. We are not going to draw a rule from three samples of a two-character task, but it does mean the effort line is not free on every model: on the reader agent, the one we spawn most often, xhigh bought a few hundred tokens of reasoning about a prompt that said not to reason.

The transcript's effort key answered this more cleanly than we expected. In the first three runs the parent session ran at high (its default here), so the pre-ship reviewer's effort: high was indistinguishable from inheritance. We reran two agents with the parent forced down:

claude -p --effort low ...   # parent transcript: effort low, perTurnEffort low

Under that parent, the pre-ship reviewer's first request still recorded effort: high and the independent reviewer's still recorded effort: xhigh. That is the documented behaviour, "Overrides the session effort level", confirmed at the request level rather than in the /tasks panel. So all three of our files do what they say: the model line is applied, the effort line is applied, and each is applied independently of what the parent is doing.

One detail for anyone reading these transcripts: the parent's records carry both effort and perTurnEffort, while a subagent whose file sets effort carries effort only, with perTurnEffort null, except for the inherit agent, where both were high. We do not know what perTurnEffort means in the harness and did not find it in the docs; we mention it only so nobody mistakes the null for "effort not applied".

For the last probe we wrote a fourth file with two keys the documentation does not list: reasoning: xhigh, a plausible wrong guess at the effort field, and colour: red, the British spelling of the documented color.

claude plugin validate .claude/agents printed "Validation passed" for the directory with all four files in it. The agent launched on claude-haiku-4-5-20251001, so model: haiku was read. Its transcript recorded effort: None, and the parent for that run was at low. reasoning: xhigh did nothing, and nothing told us. colour we could not observe in a headless run, but the documented field is color, and an unknown key that survives validation and has no way to reach the display code.

This is the practical finding of the whole exercise. The documented fields work. The cost of a typo in a documented field is not an error; it is a silent fallback to session defaults. An efort: xhigh in the reviewer file would leave the reviewer running at whatever effort the parent happened to be at, forever, with Validation passed at every check. We have not built a guard for this yet. The obvious one is a commit-time test that reads the frontmatter of every file under .claude/agents/ and rejects any key outside the documented list (name, description, tools, disallowedTools, model, permissionMode, maxTurns, skills, mcpServers, hooks, memory, background, omitClaudeMd, effort, isolation, color, initialPrompt, experimental). Twenty lines of code; we will report when it exists rather than pretend it does.

Not the point of the post, but it fell out of the transcripts and changes how you should read repeated measurements. Every subagent request wrote its prefix with ephemeral_5m_input_tokens, while the parent's own requests in the same session used ephemeral_1h_input_tokens. The timestamps agree with a five-minute window: the pre-ship reviewer launched at 13:06:32 wrote 16,222 tokens, and the same agent launched at 13:10:28 read 16,222 and wrote 0; the reader launched at 13:04:27 wrote 20,971, and the same reader at 13:11:49 wrote all 20,971 again. The documentation has a frontmatter knob for exactly this, experimental with a cacheTtl key: "Set its cacheTtl key to 5m or 1h to choose the prompt cache lifetime for this subagent's requests". We have not turned it on; whether an hour-long cache on a 16,000-token prefix pays for itself depends on how often the same agent is launched, and our launch log does not yet answer that.

The three files stay as they are. model: sonnet on the reader and model: opus on the reviewer are being applied, which is what justifies giving the reviewer a different model from the parent in the first place. effort: xhigh on the reader is the one line we now doubt: it is the cheapest agent by design, it launches most often, and it is the only one that paid thinking tokens on a do-nothing prompt. That decision needs a real workload measurement, not a probe, so it is a follow-up rather than a change.

What changes is the checklist for editing those files. Run the validator, yes, but also launch the agent once with a probe and read the transcript's model and effort back, because the validator will not tell you that your effort line was spelled wrong. The transcript will, in one line, for about 16,000 tokens.

Rulestack designs subagent files, skills and CLAUDE.md conventions for Claude Code, on sale at rulestack.gumroad.com. The three agent definitions measured here are the ones our own repository runs, and their frontmatter now carries a comment naming the field the validator ignores.

The per-launch token counts get remeasured after each Claude Code release that touches subagents; the deltas are posted on @ai-shop.bsky.social.

── more in #ai-agents 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/three-claude-code-su…] indexed:0 read:10min 2026-09-27 · —