anthropics / anthropics/claude-code
CLAUDE_CODE_FORK_SUBAGENT=1 silently overrides the documented teammate foreground-only rule — teammate's Agent dispatch launches async (no error) and the teammate strands forever
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## Environment
- Claude Code 2.1.220, macOS (darwin 25.5.0)
- `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1`, `CLAUDE_CODE_FORK_SUBAGENT=1`, default `in-process` teammate mode
- Live-probed 2026-07-25; mechanism confirmed by static analysis of the 2.1.220 bundle on 2026-07-26
## Summary
The agent-teams docs promise that an in-process teammate's subagents run in the **foreground**, and that asking for a background one "returns an error." With fork mode enabled (`CLAUDE_CODE_FORK_SUBAGENT=1`), a teammate's Agent-tool dispatch is instead **silently launched async** — `Async agent launched successfully`, no error, no warning. Because a teammate never receives task-notifications (its workers' completions route to the top-level session), the teammate then waits forever on a child it structurally cannot hear finish. The foreground-inline result was the only delivery mechanism a teammate had; fork mode removes it and substitutes a channel the teammate is deaf to.
With `CLAUDE_CODE_FORK_SUBAGENT` off (teams flag still on), the same dispatch runs foreground-inline exactly as documented and the stall disappears.
Note this is not gated on users setting the env var: the fork-mode resolver falls through to a staged-rollout gate when the variable is unset, so any agent-teams user can be opted into this behavior remotely. Setting `CLAUDE_CODE_FORK_SUBAGENT=0` explicitly is currently the only way to pin it off.
## The docs contradict each other
[agent-teams § Limitations](https://code.claude.com/docs/en/agent-teams#limitations):
> **No background subagents from in-process teammates**: an in-process teammate's own subagents run in the foreground. Asking for a background one, whether with `run_in_background` or a subagent definition that sets `background: true`, returns an error, because a teammate's background work can't outlive the lead's process.
[sub-agents § Run subagents in foreground or background](https://code.claude.com/docs/en/sub-agents#run-subagents-in-foreground-or-background):
> When `CLAUDE_CODE_FORK_SUBAGENT` is set to `1`, every subagent runs in the background and the frontmatter `background` field has no effect, because fork mode removes the `run_in_background` parameter from the `Agent` tool.
Neither page cross-references the other. In practice the fork rule wins, silently.
## Repro
1. Set `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` and `CLAUDE_CODE_FORK_SUBAGENT=1`; default `in-process` teammate mode.
2. Lead spawns a teammate.
3. Teammate dispatches a subagent via the Agent tool (no `run_in_background`, no `background: true` in the definition).
**Expected** (per agent-teams docs): foreground-inline run, result in the tool result; a background request would error.
**Actual:** `Async agent launched successfully`. The child's completion notification is delivered to the top-level session; the teammate is never woken and stalls indefinitely. Observed live across multiple dispatches (the teammate yielded to wait and no notification ever arrived; only a manual SendMessage from the lead recovered it).
## Mechanism (static analysis of the 2.1.220 bundle)
The documented teammate guard **exists and runs** — there are two precondition checks (string anchor: `subagent_teammate_background_denied`) that throw exactly the documented error for `run_in_background: true` and for a definition with `background: true`. The defect is a coverage gap between the guard and the async decision that runs after it:
```
async = isRemote
|| ( runInBackgroundParam === true
|| agentDef.background === true
|| coordinatorMode
|| forkSubagentsEnabled // <-- the culprit
|| (!isTeammate && runInBackgroundParam !== false) // the ONLY disjunct that respects teammate-ness
) && !disableBackgroundTasks
```
The guard tests the **request** (`run_in_background === true || def.background === true`); the decision tests the **environment**. The fork disjunct (and the coordinator-mode one) force async for teammates without ever consulting teammate-ness, so a teammate dispatch that requests nothing — or even explicitly passes `run_in_background: false` — goes async with no error.
Aggravations found in the same code path:
- **`run_in_background: false` does not help.** The explicit-false escape lives only inside the `(!isTeammate && …)` disjunct, which the fork disjunct short-circuits past. The guard's own error message ("Use run_in_background=false for synchronous subagents") is therefore incorrect advice under fork mode: explicit `true` errors, explicit `false` and omitted both launch async.
- **The async branch's owner stamp cannot name a teammate.** It computes `owner = deriveOwner(spawnerAgentId) ?? mainSessionId()`, and the derivation fails twice for teammates (registry record keyed by task id rather than agent id, and type `in_process_teammate` fails the `local_agent` gate) — so the owner is stamped as the main session and the completion notification routes there. Either half alone would be survivable (async + correct owner would notify the teammate; foreground + wrong owner never emits a notification at all); together they produce a silent permanent stall.
- **Rollout exposure.** The fork-mode resolver is: coordinator mode → off; env truthy → on; env explicitly falsy → off; non-interactive → off; else a statsig rollout gate. So the interaction can activate for teams users who never set the flag. Explicit `CLAUDE_CODE_FORK_SUBAGENT=0` pins it off ahead of the rollout check.
- **Same shape, untested:** `coordinatorMode` and `isolation: "remote"` also skip the teammate guard (code-read inference; we only probed the fork flag).
- A fork-agent permission deny does not switch the disjunct off — it reads the raw flag state, not fork-agent availability.
## Suggested fix shapes
1. Scope the fork/coordinator disjuncts to non-teammates (`&& !isTeammate`), preserving the documented foreground rule — or have the guard test the same inputs the async decision does, so the documented error actually fires.
2. Alternatively, if async-from-teammates is intended under fork mode, stamp the spawning teammate as the owner and route its completion notification to it.
3. Docs: cross-reference agent-teams § Limitations and sub-agents § fork mode so the precedence is stated; correct the guard error message's `run_in_background=false` advice if the behavior stays.
## Workarounds (verified against the code path)
- `CLAUDE_CODE_FORK_SUBAGENT=0` (explicit falsy — also pins off the staged rollout) when teams are in use.
- `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` collapses the whole term and restores foreground for everyone — but kills all background tasks session-wide.
- No call-parameter workaround exists: with fork mode on, a teammate cannot obtain a foreground subagent by any Agent-tool input.
## Related
- #77300 — Monitor/background-Bash notifications never wake an idle teammate (the only prior teammate notification-blindness report; this issue is the Agent-subagent + fork-flag composition of it)
- #69212 — subagent results route to root teammate instead of spawning teammate (implies teammates were getting async dispatches earlier; reports the misrouting half only)
- #74614 — `run_in_background: false` ignored when the dispatch includes a `name` (a different hidden input silently forcing async, stranding the caller)
- #75043 / #77950 / #81438 — completion notifications bypassing the spawning parent at subagent depth
- #73578 / #69691 / #62633 — undocumented async escalation family
- #69621 — teammate background-task lifetime docs gap
Contributor guide
No contributing guide indexed for this repository
Research direction
Reproduce the interaction with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 and CLAUDE_CODE_FORK_SUBAGENT=1, then inspect the 2.1.220 bundle around the teammate background guard and async decision. Verify whether a teammate's Agent dispatch remains foreground and whether its completion reaches the teammate. Compare the agent-teams Limitations and sub-agents fork-mode documentation if documentation changes are included.
Written by the indexing model from the issue text.
Assessment
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100