[BUG] Headless session exits "success" seconds after dispatching subagents — work orphaned, no error surfaced

Status Open
Reported on v2.1.218
Maintainer reply None cached
Activity 4 comments · opened Aug 8, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

In a headless SDK session (anthropics/claude-code-action@v1 running the code-review@claude-code-plugins plugin), the model dispatched a multi-agent review fan-out, announced it, and the session terminated 6–15 seconds later with "subtype": "success", "is_error": false. No review was ever produced. The GitHub Action reported success, so nothing signalled that the review had silently not happened — the PR comment simply stayed frozen at an unfinished todo list.

This is the headless counterpart of #81254 (interactive: Claude presents a plan before backgrounded subagents finish). Interactively the session survives, so the user can notice and the subagent result still lands. Headless, ending the turn ends the process — the pending work is killed and the exit code says everything is fine.

Relevant context: 2.1.218 changed /code-review to run as a background subagent (see #80385). Our workflow invokes the plugin's namespaced /code-review:code-review; I haven't verified whether the plugin command shares the built-in's backgrounding, so the mechanism below is the best-supported explanation rather than a proven one.

Two runs on 2026-08-08, same repo, unrelated PRs. Timestamps from the run logs plus the GitHub comment edit history (userContentEdits), which is the only way to see the model's actions since the transcript is hidden in CI:

Run A (PR "350"):

| Time (UTC) | Event |
|---|---|
| 14:27:08 | SDK init |
| 14:27:17 | model's first tracking-comment update (todo list) |
| 14:33:35 | model updates comment: "7 review agents dispatched, in progress" |
| 14:33:50 | result: success, duration_ms: 401422, num_turns: 12, total_cost_usd: 5.2874, permission_denials_count: 50 |

Run B (PR "355"):

| Time (UTC) | Event |
|---|---|
| 15:18:53 | model's first tracking-comment update |
| 15:23:42 | model updates comment: "multi-agent review running" |
| 15:23:48 | result: success, duration_ms: 303082, num_turns: 13, total_cost_usd: 5.2872, permission_denials_count: 23 |

In both cases the session died 6–15 s after the dispatch edit — far too fast for any review agent to have finished, and the model's own todo list still had "Post review feedback" unchecked. Plain reruns of both (same CLI version, same plugin) reviewed in the main conversation instead of fanning out and completed normally, which is why this reads as nondeterministic agent behaviour that the harness should be catching.

Two details that may help narrow the mechanism:

  1. Near-identical total cost across unrelated PRs: $5.2874 vs $5.2872 (0.003% apart), despite different durations, turn counts and diffs. That looks less like coincidence and more like both runs hitting the same hard ceiling (context window or an internal budget) during the pre-dispatch phase — which may be why the model pivoted to a subagent fan-out immediately before dying. If such a ceiling can end a query while work is pending, that's worth checking independently of the background-subagent path.
  2. Permission-denial storms (50 and 23): the action's prompt instructs the model to run git diff origin/dev...HEAD, but the default allowlist has no read-only git commands, so on large PRs it grinds through the changeset with Read() instead. Healthy runs on the same repo logged 0–3 denials and cost $0.36–2.85. The denials are invisible without diffing the result JSON.

I can't distinguish with certainty between (a) background-dispatched subagents orphaned at turn end, (b) subagent dispatch denied and the model giving up with a hidden final message, or (c) a context/budget ceiling ending the loop right after dispatch — the hidden transcript caps what's diagnosable from outside. All three share the same fix surface: a headless query should not exit success with work pending or with the task demonstrably unfinished.

What Should Happen?

In headless/--print/SDK mode, one of:

  1. Await pending background subagents and re-invoke the model with their results before emitting result (the interactive re-invocation behaviour, applied headlessly); or
  2. Refuse background dispatch in headless mode so the model receives an error it can react to and falls back to reviewing inline.

Either is fine. Silently exiting with success while killing pending work is the worst of the options, because it converts a recoverable failure into an invisible one. Compare #74685, where ScheduleWakeup is a silent no-op in --print mode — same shape of problem: a mechanism that assumes a future re-invocation which headless never delivers.

Secondary asks:

  • Surface permission_denials_count above some threshold as a log warning naming the denied tools. 50 hidden denials in one run made this substantially harder to diagnose.
  • (For claude-code-action, if it's the better home: with track_progress: true, a run ending while the tracking comment still shows unchecked todos and an in-progress spinner could be a step failure or at least a warning annotation. Both of our stalls were caught only by a human noticing the frozen comment.)

Error Messages/Logs

# Run A, final SDK result — emitted 15 seconds after the model announced
# "7 review agents dispatched, in progress". No error anywhere in the log.
{
  "type": "result",
  "subtype": "success",
  "is_error": false,
  "duration_ms": 401422,
  "num_turns": 12,
  "total_cost_usd": 5.287379350000001,
  "permission_denials_count": 50
}

# Run B, same shape:
{
  "type": "result",
  "subtype": "success",
  "is_error": false,
  "duration_ms": 303082,
  "num_turns": 13,
  "total_cost_usd": 5.287212800000002,
  "permission_denials_count": 23
}

Steps to Reproduce

Reproduces intermittently — 2 of ~5 review runs on the same day. It needs a diff large enough to push the model toward a subagent fan-out.

  1. Configure a GitHub Actions workflow using anthropics/claude-code-action@v1:

```yaml

  • uses: anthropics/claude-code-action@v1

with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
track_progress: true
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review'
```

  1. Open a large PR (ours were 59 files / +611 −1874, and 29 files) so the model has a lot of diff to process. Do not add read-only git tools to the allowlist, so it burns context via Read().
  2. Watch the tracking comment. When the model writes "N review agents dispatched, in progress", check the run result.
  3. Failure case: the session emits result: success seconds later, no findings are posted, and the workflow goes green with the comment frozen mid-todo.

Because the transcript is hidden by default in the action, userContentEdits on the bot comment (GraphQL) is the most reliable way to timestamp what the model actually did:

query { repository(owner:"OWNER", name:"REPO") { issueOrPullRequest(number: N) {
  ... on PullRequest { comments(first: 5) { nodes { databaseId
    userContentEdits(first: 20) { nodes { editedAt diff } } } } } } } }

Claude Model

claude-sonnet-5

Is this a regression?

Yes, likely — /code-review moved to a background subagent in 2.1.218 (#80385). We have no confirmed pre-2.1.218 baseline for this workflow, so I can't state the last good version with confidence.

Last Working Version

Unknown; suspect pre-2.1.218.

Claude Code Version

2.1.226 (native build), @anthropic-ai/claude-agent-sdk@0.3.226

Platform

GitHub Actions (anthropics/claude-code-action@v1), headless SDK

Operating System

ubuntu-latest (GitHub-hosted runner)

Terminal/Shell

bash (GitHub Actions runner, non-interactive)

Additional Information

Related: #81254 (interactive analog — plan presented before backgrounded subagents finish), #80385 (docs: /code-review backgrounding in 2.1.218), #74685 (ScheduleWakeup silent no-op in headless).

Workaround we've deployed, in case it helps others hitting this: append constraints to the prompt telling the model not to end its turn while subagents are running or todos are unchecked, to run review subagents in the foreground, and to fall back to reviewing inline if dispatch fails; plus allowlisting read-only git diff/log/blame/show and gh pr diff/view so it stops burning context on Read(). That's a prompt-level patch over a harness-level problem, which is why I'm filing this.

View original on GitHub ↗

4 Comments

konard · 19 days ago

Additional full claude -p stream-json trace from Claude Code 2.1.282 (Linux container): Hive Mind issue #2301, full sanitized failure log.

Reproduction pattern: Run a noninteractive claude -p --output-format stream-json --verbose --dangerously-skip-permissions task that starts background Agent subagents and then ends its main turn saying it is waiting for them. In this run, five port agents were outstanding. At 18:53:51 UTC the CLI emitted type: result, subtype: success, is_error: false, terminal_reason: completed, permission_denials: [], and subagent_stats: { spawned: 5, completed: 0, killed: { system: 5, user: 0, parent: 0 } }. Its result text explicitly said the remaining version/test/PR work depended on those agents.

Immediately after that result, the CLI emitted task_notification status: stopped and synthetic subagent tool_result errors with tool_use_result: "User rejected tool use" and text The user doesn't want to proceed with this tool use.... No user made a permission decision; the subprocess itself exited 0. A host that treats the last tool_result as the final verdict then reports a false user rejection, while a host that trusts only result.success silently loses the unfinished work. This is the same orphaned-work failure as the issue, with an additional misleading post-result error surface.

Small repro: The deterministic auto-background Bash reproduction in #89495 shows the same print-mode lifecycle without requiring five agents. For this Agent variant, request several background Agent tasks in one claude -p prompt and have the main model return a waiting/status message before collecting their results; inspect the final result's subagent_stats and the events after it. This variant is timing dependent.

Workaround: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 for one-shot print-mode runs (documented by Claude Code) makes Bash and Agent work stay in the turn. Prompt the agent to await all dependent work before its final response. The host can detect killed.system > 0 and resume the same session once, or fail clearly if the work remains incomplete.

Suggested CLI fix: Do not emit a successful terminal result while required background work is pending; await/drain it or disallow background dispatch in print mode. If the CLI must cancel pending children, report that as cancellation rather than a user permission rejection, and emit one authoritative terminal result after cancellation accounting is complete. Include the unfinished-task count in the result for hosts to handle.

kurodaritsu · 18 days ago

This might also be related to #73453. Reports were also from claude-code-action on a Linux runner and describe the same thing i.e. the session ends its turn with subagents still running and exits 0.

We're seeing this on the same setup (claude-code-action@v1 + code-review@claude-code-plugins). As mentioned here by others, CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 does its job for us as well (for now), but the subagents then run sequentially, so the review takes noticeably longer.

konard · 15 days ago

Follow-up with the exact mechanism and a deterministic repro, now on Claude Code 2.1.284 (second production incident: a 2.5 h claude -p run that spawned 15 background subagents).

Mechanism. In print mode the CLI waits for background tasks only up to CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS (default 600000). Exactly 600 s after the main thread's last message (21:53:55.139 → 22:03:56.680) the CLI:

  1. writes only to stderr: Background tasks still running after 600s; terminating. Set CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=0 to wait indefinitely.
  2. emits task_notification status: stopped for every task (10 before the result, 1 after it)
  3. gives each killed subagent synthetic [Request interrupted by user] and The user doesn't want to proceed with this tool use… (tool_use_result: "User rejected tool use"), although the run uses --dangerously-skip-permissions and no user exists
  4. emits result subtype: success, terminal_reason: completed, permission_denials: [], subagent_stats.killed.system: 9, and exits 0

Deterministic repro (about a minute): repro-ceiling.sh, with captures in captured/.

  • Agent, ceiling lowered to 8 s: CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS=8000 claude -p "<launch one background Agent that runs a 16 s Bash loop, then end the turn>" --output-format stream-json --verbose --dangerously-skip-permissions produces the stderr line, then stopped, killed.system=1 and the synthetic "User rejected tool use" inside the subagent.
  • Background Bash: sleep 90 with run_in_background: true is not waited for at all and has no stderr line. It is stopped about 4 s after result, and killed.system stays 0, so hosts cannot see from subagent_stats that work was cancelled.
  • Workaround: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1 CLAUDE_CODE_DISABLE_WORKFLOWS=1 CLAUDE_CODE_DISABLE_MCP_TASK_BACKGROUND=1. The model still requests run_in_background: true, but the agent runs in the foreground, completed, started_in_background=0, and its answer is in the result. Setting the ceiling to 0 also avoids the cancellation, but lets a stuck task hang a headless run forever.

Suggested fixes:

  • Put the ceiling sweep into the terminal result: a non-success subtype, or terminal_reason: "background_tasks_terminated", plus the count of cancelled tasks, including local Bash tasks.
  • Use a cancellation message that does not say the user rejected the tool. It is indistinguishable from a real permission denial.
  • In one-shot print mode, run run_in_background work in the foreground, or reject it, instead of starting work that the process is guaranteed to kill.

Host-side handling (exact detection from the stream, ignoring subagent-scoped events, one bounded --resume): link-assistant/hive-mind#2302. Case study: docs/case-studies/issue-2301.

konard · 15 days ago

Follow-up: I filed a focused report as #98170. Two more production runs hit the same sweep (Claude Code 2.1.284). The ceiling counts from the main thread's last message, not from task progress: subagents that were still running pytest 2 s earlier were killed, and the run still ended with result: success and exit 0. It also covers results being held back until exit and --resume replaying the stale kill. Repro, logs and host-side workaround (raise the ceiling to 4x and resume the same session): https://github.com/link-assistant/hive-mind/pull/2302