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

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 0 comments · opened Jul 26, 2026

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 asyncAsync 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:

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:

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

View original on GitHub ↗