Claude Code has three ways to start a subagent. They differ most in what the launching costs the parent.
| Agent tool | Workflow tool | claude -p children (scripts/headless_fanout.py) |
|
|---|---|---|---|
| parent cost | one parent turn per launch and one per completion | one tool call per run | one Bash call per run |
| concurrency | 20, and a 21st launch errors (it does not queue) | min(16, vCPUs − 2): 2 on a 4-vCPU CCotw container | RAM-bound, about 210 MB per child: 40 ran clean on 4 vCPU / 15 GB |
| child context | session prefix (~32–55K tokens), plus parent transcript chunks if a context hook is installed | session prefix | what you pass: about 1.2K tokens with --system-prompt and no tools, 39K with the default prompt |
| tools | agent type's tools, the session's MCP connectors, hooks | as Agent | built-in tools named in --tools, write paths allowlisted; no MCP, hooks or skills |
| effort | session's level, or the effort: in a named agent type's frontmatter |
agent(prompt, {effort}) |
--effort (measured: low gave 0 thinking tokens, high gave 112–197) |
| needs user opt-in | no | yes: an explicit request, ultracode, or a skill that calls it | no |
| cost visibility | none per child | none per child | total_cost_usd and tokens per child, in a ledger |
| output check | yours | in-script | file plus JSON Schema, or CLI-validated --json-schema |
The headless runner is the default for volume. On 2026-10-11 a codex build
dispatched 314 Haiku children through the Agent tool. The children cost
$11.61. The 1,458 Opus parent turns the dispatching took (each re-reading a
context of about 400K tokens) cost roughly $160–170, about 14× the work. Five of
the same tasks through headless_fanout.py cost $0.10, $0.02 each against
$0.033 per Agent-tool child, and one parent call.
Choosing
Answer in order. The first yes picks the mechanism.
- Does the child need what only the session has? That means MCP connectors
(Muninn, Gmail, GitHub MCP), a context hook's transcript chunks, a worktree,
or a custom agent type with guard hooks such as
auditor. If so, use the Agent tool. Keep the count small, or batch many items into each child, because every launch costs a parent turn. - Do you need to read each result before deciding the next launch? That is iterative work where task N+1 depends on N's answer. Use the Agent tool.
- Did the user opt in to a workflow, and does the procedure have phases a
script should enforce (find → verify → fix, with retries and gates)? Use
Workflow, and load
workflow-authoringfirst. Accept 2-way concurrency on a 4-vCPU container, or move the bulk phase to headless children. - Are there ten or more independent tasks whose inputs fit in a prompt or in files, and whose output a script can check? Use headless children. This covers judges, graders, extractors, page writers, verifiers and per-file reviews.
- Fewer than ten small independent tasks: use the Agent tool in one message. The parent-turn cost is still small at that size.
Earned exceptions
| rule | banned when | earned when |
|---|---|---|
| Use headless for volume | the child needs an MCP connector or the parent's transcript | the inputs can be written into the prompt or a file first |
| Agent tool for small counts | the parent context is already past ~300K tokens, so each launch turn is expensive | the parent context is small, or the results feed the next launch |
| Judges in headless children | — | always: a child with no hooks cannot see the parent discussing the items it scores |
Headless fan-out procedure
- Write one task per line to
tasks.jsonl:{"id", "prompt"}plus any ofout(absolute path of a JSON file the child writes),json_schema(structured output, no file),model,tools,allow,deny,system_prompt,timeout.{out}in a prompt expands to the out path. This is the same{id, prompt, out}shape claude-workspace'sscripts/fanout.pydispatches over the Agent tool, so one file runs on either engine. - Put everything the child needs into the prompt or into files it can
Read. The child has no hooks, no CLAUDE.md, no skills, no MCP and no
memory of this session. For no-tool tasks (judges, classifiers) inline the
inputs and pass
"tools": "". Put a large prefix shared by every task insystem_prompt, since that is the cacheable part. - Dry-run. This prints the first three commands:
CheckS=/mnt/skills/user/dispatching-subagents/scripts/headless_fanout.py python3 $S run tasks.jsonl --work RUN --schema schema.json --dry-run--model,--toolsand theEdit(//...)rule against the out path. - Launch as a background Bash command, never as a waiting loop. Run the
job itself in the background and end the turn. The completion notification
brings you back.
(python3 $S run tasks.jsonl --work RUN --schema schema.json \ --model haiku --concurrency 16 --max-usd 5 > RUN.log 2>&1; python3 $S status RUNBash run_in_background: true.) Defaults are--tools Read,Grep,Glob,Write, 1 retry, 900 s per child, and concurrency 16. - Read
status. It reports attempts, ok, failing ids with reasons, total cost, tokens and permission denials. Rerunning the same command skips done tasks and retries the rest.
What the runner enforces per child:
- a fresh
--session-idand--no-session-persistence; - the parent's session-binding variables removed, because without that a child reports the parent's session id;
--setting-sources ""and--strict-mcp-config, so no hooks and no MCP;--permission-mode dontAskwith an allowlist: Read/Grep/Glob when listed,Edit(//<out>)for the out file, and any--allowrules;denyrules (task field or--deny) go to--disallowedToolsand win over the allowlist, so a child can hold Bash withBash(curl:*)refused;- done means verified: the out file parses and passes the schema, or
structured_outputis present. The child's claim of success does not count.
Permission rule syntax under dontAsk
Measured 2026-10-11, Claude Code 2.1.296:
- A write is allowed by
Edit(//abs/path/**)orEdit(rel/**)(relative to the child's cwd).Write(path)and a bareEditare both refused. Bash(grep:*)style prefixes work. A prefix rule matches the start of the command only, soBash(curl -s https://api.github.com/*)still admits-X DELETEappended after it. Never allow Bash for a child that holds a token or that could reach a write API. Fetch live data yourself and put it in the prompt instead.- Denied calls appear in the child's
permission_denials, and the ledger counts them. A child that hits a denial usually reports the step as failed and stops (it does not route around).
Agent tool procedure
- Launch all children of a batch in one message,
run_in_background: true, at most 20 in flight. A launch over 20 errors and is lost. - Batch items per child. The per-spawn prefix (32–55K tokens) is paid once per invocation, however many items it carries.
- In claude-workspace,
scripts/fanout.pykeeps the ledger and validates outputs. Itsnextcan report a slot free before the agent stops. Dispatch one replacement per completion notification instead (diagnosed 2026-10-11, the codex build). - Subagents that must talk to each other use
SendMessage/ListAgents; the measured traps (reply by agentId, never by the envelope'sfrom; subagents have noListAgents) are in references/native-messaging.md. - Put
[no-context]in judge and eval prompts where thedelegating-with-contexthook is installed, or the judge sees the parent discussing what it scores.
Workflow procedure
Load workflow-authoring before writing the script, and run only with the
user's explicit opt-in. Give agent() its effort where it matters. For a
bulk phase on a small container, a Workflow step can shell out to
headless_fanout.py instead of awaiting 100 agent() calls two at a time.
When NOT to use this skill
| situation | use instead |
|---|---|
| which model or effort a child should get, cascades, escalation | agent-routing |
fan-out from claude.ai chat, where no Agent tool or claude binary exists |
orchestrating-chat-agents |
| per-item classification at large volume, where a cheap non-Claude model is enough | invoking-gemini |
| a deterministic multi-step pipeline with no model judgment between steps | flowing |
| work that must outlive this session or container | create_session (claude-workspace docs/delegation.md) |
| hundreds of workers fixing failing tests for hours, each fix merged behind a no-regression gate | swarming-failing-tests (it runs this skill's headless runner per worker) |
| one subagent for one question | the Agent tool directly, with no ledger |
Abandon a headless run mid-way when the first wave's ledger shows the same
failure reason on most tasks (a prompt or permission bug, not bad luck), or
when denials is non-zero on tasks meant to need no tools. Stop it, fix the
task file, and rerun; done tasks are kept.
Common failure modes
- A child's
session_idequals yours. The session variables leaked (CLAUDE_CODE_SESSION_IDand the rest). Run children only through the runner, or stripSESSION_ENVyourself. - The output file is missing and the ledger shows
denials> 0. The write rule did not match: theEdit(//...)path differs from the path in the prompt, or someone usedWrite(...). Dry-run and compare. modelUsagelists claude-haiku-5-5 on a Sonnet or Opus run. This is Claude Code's own side call, about 3% of the cost. It is expected. Read the per-model split before attributing cost.- A judge or extractor reads only part of a large input file. Inline the input,
or split files so each fits one Read. Check what the child read before
trusting the scores (
oaustegard/experimentsMETHODS.md). - Spend climbs past expectation. Every child writes its own cache, since nothing
is shared across
--no-session-persistencechildren. Move the common prefix intosystem_prompt, and cap the run with--max-usd. The runner stops launching once the cap is reached. - Many children fail with "transient" reasons at once. The run hit a rate or
overload limit. Lower
--concurrency. The runner backs off and retries.
Verification
python3 $S status RUN # exit 0 only when every task's last attempt is ok
python3 /mnt/skills/user/dispatching-subagents/scripts/test_headless_fanout.py # offline, fake claude binary
A bad success is a run where every task passes the schema check but the outputs are copies of the example in the prompt, or are empty lists. Read three outputs before using any aggregate.
Diagnosed failures
- 2026-10-11, codex build. 314 Agent-tool launches from an Opus parent cost about 14× the children's own bill in parent turns. The fix is this skill's headless runner. On five of the same verifier tasks it cost $0.10 in one call.
- 2026-10-11, probe.
claude -pchildren with no flags inherited the parent's session id and wrote transcripts under the parent's id. Fixed by the runner's environment strip and--session-id. - 2026-10-11, probe.
--allowedTools "Write(/abs/out/**)"was refused underdontAskand the child wrote no file.Edit(//abs/out/**)works.