Fork mode in claude -p: what it does to an unattended script, and the one line that pins it
The Claude Code 2.1.302 changelog says forked subagents are now on by default in headless runs. The docs say off. With fork mode on, a script's stdout changes shape.

A script that calls claude -p and parses its stdout can break without a single line of the script changing. Claude Code 2.1.302, released on 16 October, lists forked subagents as on by default in claude -p and the Agent SDK
. Turn fork mode on and the same prompt prints different text.
This piece covers what a fork is, what changes in a headless run when fork mode is on, and a small wrapper that makes the result the same on any version. One thing to know first: the changelog and the sub-agents docs disagree about the default, and a plain run on 2.1.302 matched the docs.
What a fork is
A normal subagent starts with a fresh context and a prompt Claude writes for it. A fork is a subagent that inherits the whole conversation so far. The docs say it sees the same system prompt, tools, model and message history as the main session. Its tool calls stay out of the parent’s context and only the final result comes back.
Because the system prompt and tool definitions match the parent’s, a fork’s first request reuses the parent’s prompt cache. The docs say that makes it cheaper than a fresh subagent for work that needs the same context. It’s the right tool when briefing a new subagent would take as long as the task, or when several approaches should start from the same point.
Claude starts one by calling the Agent tool with subagent_type: "fork". In an interactive session, /subtask does the same on request, and fork mode has been on by default there since v2.1.232.
Why a headless script cares
Fork mode changes more than which subagent types exist. The docs describe two side effects. Claude Code runs every subagent Claude spawns in the background, forks and non-fork alike, and it removes the Agent tool’s run_in_background parameter, so Claude can’t ask for the foreground.
In an interactive session that’s pleasant: work continues while the subagents run. In claude -p there’s no one to keep working, and the parent turn ends while subagents are still running. The parent then answers from whatever it has.
Here’s a small project to see it:
mkdir forkdemo && cd forkdemo && git init -q
printf 'alpha\nbeta\ngamma\n' > a.txt
printf 'one\ntwo\n' > b.txt
And a prompt that wants two subagents:
P='Use the Agent tool to start two subagents, one counting the lines in a.txt and one in b.txt. Then reply with the two counts in one line.'
CLAUDE_CODE_FORK_SUBAGENT=1 claude -p "$P" --allowedTools "Agent Read Bash"
On 2.1.302 that prints this (the blank lines are in the output):
Both subagents are still running, so I don't have the counts yet. I'll reply once they finish.
a.txt has 3 lines. The agent for b.txt is still running, so I'll send both counts once it finishes.
a.txt has 3 lines and b.txt has 2 lines.
Three messages, two of them progress reports. The exit code is 0. A script that captures stdout and uses it as the answer gets all three paragraphs, and a script that takes the first line gets “still running”. Set CLAUDE_CODE_FORK_SUBAGENT=0 and the same command prints one line:
a.txt has 3 lines and b.txt has 2 lines.
With the stream format, the result event appears three times in the fork-on run and once in the fork-off run. --output-format json is different: with fork mode on, it returned a single object whose result was the final line, the same as with fork mode off.
The changelog and the docs disagree
The 2.1.302 changelog entry says fork subagents are on by default in claude -p, the Agent SDK, and the desktop app and VS Code. The sub-agents page and the environment variable reference, read on the same day, still say fork mode is off by default in -p and the SDK, and that CLAUDE_CODE_FORK_SUBAGENT=1 turns it on.
A run on 2.1.302 with the variable unset followed the docs. The Agent calls still carried run_in_background: false, the parameter fork mode removes, and the output was the single clean line. The default may depend on account type, provider or a staged rollout. The changelog also notes that claude -p and SDK sessions on third-party providers now start in default permission mode, so headless behaviour is visibly in motion in this release.
The practical reading: don’t depend on either. Whatever the default is on the machine that runs the script today, it can differ on the next version or on a colleague’s laptop.
Pin it in the script
A wrapper that sets the variable explicitly and reads the JSON result keeps the output predictable either way:
#!/usr/bin/env bash
# usage: agent-step.sh "<prompt>"
set -euo pipefail
# Pin fork mode so the result doesn't depend on the CLI version's default.
export CLAUDE_CODE_FORK_SUBAGENT="${CLAUDE_CODE_FORK_SUBAGENT:-0}"
claude -p "$1" \
--output-format json \
--allowedTools "Agent Read Bash" \
< /dev/null \
| jq -r '.result'
Two details. The < /dev/null redirect stops Claude Code waiting three seconds for stdin data when the command is part of a pipe; without it, a warning about no stdin data lands in the output. And the :-0 default lets a caller opt in with CLAUDE_CODE_FORK_SUBAGENT=1 ./agent-step.sh ... for a one-off.
The wrapper printed a.txt has 3 lines and b.txt has 2 lines. with the variable pinned to 0, and the same line with it set to 1.
Why pin to 0 rather than 1? A pipeline step usually wants deterministic, foreground work with the result in hand when the process exits. Background subagents in a process that ends with the parent turn are the opposite of that. Keep fork mode off by default and turn it on for the jobs that benefit.
When a fork is worth it in a headless run
Forks pay off when the task needs the parent’s context and the parent has a lot of it. A script that loads a large spec, a diff and a set of review rules, then wants three independent checks against all of it, is the case. Each fork starts with that context already cached, where three fresh subagents would each need it written out in a prompt.
In the line-count run above, Claude chose general-purpose subagents, not forks, so a script that wants a fork has to ask for one:
The secret word is "heron". Use the Agent tool with subagent_type "fork" to start a fork that tells you the secret word from our conversation without being told it in the prompt. Report what the fork returned.
With fork mode on, the Agent call went out with subagent_type: "fork", a prompt that never contained the word, and the fork returned heron. With fork mode off, the same prompt failed with Agent type 'fork' not found. Available agents: claude, claude-code-guide, Explore, general-purpose, Plan, statusline-setup. So if a prompt in a script asks for a fork, the pinned setting is what decides whether it works.
Two features from the docs make forks safer in automation. The Agent tool accepts isolation: "worktree", so a fork that edits files writes to a separate git worktree rather than the checkout. And a fork can’t spawn further forks.
Keep fork mode on, forbid forks
The reverse case: fork mode needs to stay on (for example, an SDK host that depends on background subagents) but a given job shouldn’t fork. The docs give a permission rule for it, Agent(fork):
CLAUDE_CODE_FORK_SUBAGENT=1 claude -p "$P" --disallowedTools "Agent(fork)"
A prompt that asked for a fork then got Agent type 'fork' has been denied by permission rule 'Agent(fork)' from cliArg. and Claude reported that it hadn’t run. Note the docs’ caveat: the rule stops forks, but subagents still run in the background while fork mode is on.
What goes wrong
- Parsing text output. This is the failure above. If a script must use text, switch to
--output-format jsonand read.result. - Cost surprises. Forks share the parent’s cache for the first request, which is cheap. A fork that then reads thirty files still adds thirty files of tokens to its own context. No cost figures here: the runs above were too small to say anything honest about savings.
- Background work outliving the turn. If the parent’s answer depends on a subagent result, check that the answer actually contains it. Nothing in the exit code tells you.
- Version drift. The interactive default needs v2.1.232 or later, and the headless default is the open question above. Pin the variable and record
claude --versionin the job’s log.
First 15 minutes
Find every claude -p call in your scripts and CI workflows. For each one, add CLAUDE_CODE_FORK_SUBAGENT=0 to its environment, or wrap the calls in a function like agent-step.sh. Then switch any step that parses stdout to --output-format json | jq -r '.result'. Leave fork mode on only for the step where a fork’s inherited context saves a long briefing, and say so in a comment next to it.