Max Narditdata & ai systems
← Articles
6 mindev

Claude Code fork subagents in claude -p: what 2.1.302 changed for headless runs

Offered is not used: a headless model may never pick a fork, and the official docs still describe the old default. Checking which subagent your pipeline got, why a background fork hands a parser two answers, and where switching it off pays.

Contents

Claude Code 2.1.302 shipped on October 16 with one line that reaches every unattended run. Fork subagents, the subagent type that starts with the parent’s whole conversation instead of a blank context, are now on by default in claude -p, the Agent SDK, the desktop app and VS Code. Before this release a headless run got them only if you opted in with CLAUDE_CODE_FORK_SUBAGENT=1. If your CI job, a cron script or an SDK service updated this week, its Agent tool now offers a type it did not offer by default last week, and nothing in your config changed to say so.

The awkward part is where the default moved. Headless runs are where people put the jobs that should look at their input cold: the CI reviewer, the grader, the search pass that should find what is in the code rather than what the caller expected. A fork is the one subagent type built to carry the caller’s assumptions along. So the flip lands on exactly the mode where inheriting context is most often wrong, and the official subagents page still says the old default applies. I ran it locally on 2.1.302 to see what changes for such a job.

What did Claude Code 2.1.302 change for fork subagents?

It made subagent_type: "fork" available by default outside the terminal. The release note reads: fork subagents are “on by default in claude -p, the Agent SDK, the desktop app and VS Code, as they already are in the terminal; CLAUDE_CODE_FORK_SUBAGENT=0 turns them off, and claude mcp serve never offers them.”

Per the docs a fork gets the same instructions, tools, model and full message history as the main session, and shares the parent’s prompt cache, which is why it costs less than a fresh subagent for work that needs the same context. Since 2.1.285 it also runs under the parent’s permission mode. It cannot spawn further forks.

The subagents page has not caught up. Its “Turn fork mode on or off” section still says Claude Code “leaves it off by default in non-interactive mode with -p and in the Agent SDK”, and says it again elsewhere on the page. Decide from the docs and you will conclude your pipeline is unaffected. The env var semantics on that page still hold: 1 turns fork mode on in -p and the SDK too, 0 turns it off in every kind of session.

Does a Claude Code fork subagent in claude -p inherit the conversation?

Yes. Plant a word in the prompt, ask for a fork, and ask the fork for the word without reading anything:

shell
cd "$(mktemp -d)" && git init -q
claude -p 'The secret word for this session is PERSIMMON. Call the Agent tool exactly once with subagent_type set to "fork" and the prompt: "Without reading any file, reply with the secret word for this session, or NONE if you do not know it." Then reply with the subagent result verbatim.' \
  --model haiku --permission-mode bypassPermissions \
  --output-format stream-json --verbose < /dev/null > fork-on.jsonl

The bypass flag is there because the run is unattended and the fork reads nothing; my runs also loaded my own plugins and agents, so treat the agent list you see as yours, not stock. On 2.1.302 the fork answered PERSIMMON. With CLAUDE_CODE_FORK_SUBAGENT=0 in front of the same command, the Agent tool refused the type:

Agent type 'fork' not found. Available agents: ...

The model then reported that error as its answer, the final result carried is_error: false, and the process exited 0. So a prompt or a skill that names the fork type explicitly breaks silently under the opt-out: no failed build, just a wrong answer. If you set the variable, search your prompts for the type name first.

Will Claude Code choose a fork on its own in a headless run?

In my test, no. With fork mode on and a neutral prompt (“use the Agent tool exactly once to delegate this subtask”), Haiku asked for general-purpose by name, the same type it picked with fork mode off. One run on one model proves nothing about yours. “On by default” means the fork type is offered; whether your runs use it depends on the model and the prompt, so check rather than predict.

How do you see which subagent type a claude -p run used?

From the stream-json output, not from the session’s startup event. The init event lists the agents a session has, and on 2.1.302 fork was not in that list even with fork mode on. It shows up only once something asks for it. A quick check:

shell
grep -o '"subagent_type":"[^"]*"' fork-on.jsonl | sort -u
grep -o "Agent type 'fork' not found" fork-on.jsonl | head -1

Treat both as a first look, not proof. The first grep matches the Agent call and also the background-task lifecycle events, and it shows what was requested, so a refused fork still appears in it; the second catches that refusal. To confirm a fork ran, find the task_started event for that tool call and its matching completion notification. Subagent transcripts land under ~/.claude/projects/ in a subagents/ folder for the session if you need to see what the child received.

Why does a Claude Code fork in claude -p return two results?

Because the fork ran in the background, and claude -p already treats background work as a reason to take another turn. In both of my runs the tool result said “Async agent launched successfully”, the model replied that the fork was still running, and that turn closed. The process stayed open, the fork finished, and the session took a second turn to relay PERSIMMON. The headless docs describe this for any background subagent: the run waits for the work, and “when background work starts another turn, the run prints each turn’s result with the default text output and the last turn’s result with json output.”

What I saw per format:

--output-format text         both turns concatenated: "The fork agent is still running..." then "PERSIMMON"
--output-format json         one object, result "PERSIMMON" (the final turn only)
--output-format stream-json  two events with "type":"result", the first one the non-answer

A parser that takes the first result event in stream-json, or stops reading at it, gets “still running” and calls the job done. Text-mode consumers get the interim sentence glued to the answer. Count the events:

shell
grep -c '"type":"result"' fork-on.jsonl

If that prints 2 on a job you assumed was one turn, read the stream to the end and take the last result, which the docs promise is the final line, or switch to json. Turning forks off removes this particular source of background work, not every one.

When should you turn off fork subagents in a Claude Code pipeline?

In any headless job whose value is a clean view, and keep them where the child continues the parent’s work. A reviewer or a grader is worth running because it does not share the author’s framing. A fork of that job’s context hands the child everything the job read before the call, including the guesses it later dropped. I made the longer version of that argument in why a forked subagent already knows too much: an inherited transcript gives you no way to tell its live conclusions from its dead ones. Continuation is where a fork earns its place, when the half-built state is the work itself.

The coarse switch is per job or per CI step:

shell
CLAUDE_CODE_FORK_SUBAGENT=0 claude -p "..."

The finer one is a deny rule, merged into the permissions block of the project’s .claude/settings.json (or .claude/settings.local.json for a local override):

json
{
  "permissions": {
    "deny": ["Agent(fork)"]
  }
}

The docs describe it as keeping fork mode on while stopping Claude from spawning forks. I did not test the rule. The docs also say fork mode runs spawned subagents in the background, yet in my headless run a general-purpose child with fork mode on ran in the foreground, so do not count on either the rule or the env var to settle scheduling: check it in your own stream.

Then pin the Claude Code version in CI, so a default like this arrives when you choose to take it. That is the one defense a model behind an unchanged name does not allow. The CLI around the model is still an ordinary versioned tool, and those you can pin.

What should you check first after Claude Code 2.1.302 lands in CI?

One real run of each job that matters, read in full. Count the result events, look at the subagent_type values, and then check the job’s own acceptance criteria, because two unchanged counts on one run show only that this run did not change, not that the job is fine. If a fork appears in a job that should look at its input fresh, set CLAUDE_CODE_FORK_SUBAGENT=0 on that step, not globally.

Then check your permission mode. Per the release note, claude -p and SDK sessions with no permission mode configured now start in default mode on third-party providers such as Bedrock, Vertex and gateways, “with telemetry off or on a fresh install”; --permission-mode auto keeps auto. Release 2.1.285 had moved such sessions to auto, so a job set up in the last three weeks without an explicit mode may have been relying on auto without knowing it. I did not test this one. Set the mode explicitly and the question goes away.

Offered is not used. The release changed what your headless run is allowed to do, and one read of its stream tells you what it did.

01 / Discussion

No comment section here — all discussions happen on X.

Max Nardit@mnardit

02 / More articles