anthropics / anthropics/claude-code
Headless `claude -p` exits with its own background subagents still running; every in-flight Task is reported `stopped` and the run still ends `result.subtype=success`, exit code 0
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
### Environment
- Claude Code CLI 2.1.270 (also reproduced on 2.1.267–2.1.269), Linux x64, `claude -p --output-format stream-json --verbose`, launched unattended from a cron-driven shell script (no TTY).
- Model: a local OpenAI-compatible vLLM server reached through the Anthropic-compatible base URL; served model name `qwen-local` = `Qwen/Qwen3.8-27B-FP8`, vLLM `0.27.2rc1.dev110+gacb0f1dcd` (image `vllm/vllm-openai:nightly-acb0f1dcdb668d90bbbf50e57552d2f6f0987c87`), `--max-num-seqs 8`.
- Prompt shape: an orchestrator system prompt (`--append-system-prompt` from a custom agent file) that dispatches 5–11 custom subagents (`subagent_type` = per-ticker analysts) in one turn via the Agent/Task tool, then waits for their returns. Subagent runs take 12–100 minutes each because they queue on the local model server.
### Failure case (model omits `run_in_background`)
When the local model's Agent/Task `tool_use` input carries only `description`, `prompt`, `subagent_type` (no `run_in_background` key), the CLI backgrounds the subagent: `system/task_started` has `is_backgrounded: true` and the Task `tool_result` is the placeholder "Async agent launched successfully". The main turn then ends ("awaiting returns"). If no completion lands within roughly 600 s of the previous turn, the `-p` process exits: every in-flight subagent gets `system/task_notification` `status: "stopped"`, the parent never sees a result, the final `result` record is `subtype: "success", is_error: false`, and the process exit code is 0.
Measured on 2026-09-14 (UTC), one jsonl per session, counts from `task_started` / `task_notification` records:
| session | tasks started | backgrounded | completed | stopped | `result` | wall |
|---|---:|---:|---:|---:|---|---:|
| b6b7af2c | 5 | 5 | 3 | 2 | success, is_error=false, num_turns=2 | 1.9 min |
| 60294b47 | 11 | 11 | 0 | 11 | success, is_error=false, num_turns=20 | 17.4 min |
| 42c89edc | 11 | 11 | 0 | 11 | success, is_error=false, num_turns=19 | 17.6 min |
| d1f7b7dc | 11 | 11 | 0 | 11 | success, is_error=false, num_turns=20 | 21.7 min |
The same pattern appears in our logs on 2026-09-01, 09-02, 09-04, 09-10 and 09-11. In the three 11/11 sessions the last completion-to-exit gap was 602–603 s, and an offline control (two subagents that each backgrounded a long `sleep`, no other activity) exited 651 s after the previous turn with the subagents still alive — so the exit looks like an idle window of ~600 s with no completion arriving, not an immediate abandonment.
### Control case (explicit `run_in_background: false`)
Sessions where the model passed `run_in_background: false` ran every subagent in the foreground (`is_backgrounded: false`), returned every result in the same turn, and lost nothing:
| session | tasks started | backgrounded | completed | failed | stopped | wall |
|---|---:|---:|---:|---:|---:|---:|
| 61f8e81c | 18 | 0 | 15 | 3 (stall watchdog, "Agent stalled: no progress for 600s") | 0 | 200.9 min |
| 9ba482a8 | 1 | 0 | 1 | 0 | 0 | 26.9 min |
### Workaround that works
`CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` in the launcher environment. Under it the Agent/Task tool schema no longer offers `run_in_background`, every subagent runs in the foreground, and an offline A/B (two subagents dispatched in one message, each ~5 min) returned both in-turn with the switch and lost both without it. It is now our production setting for every unattended `-p` wave.
### Minimal reproduction
```
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS= claude -p \
"Dispatch two general-purpose subagents in ONE message; each must run 'sleep 900; echo DONE' with Bash. Do not wait for them; reply 'dispatched' and end your turn." \
--output-format stream-json --verbose > run.jsonl
```
With a model that omits `run_in_background` (or when it passes `true`), `run.jsonl` shows two `task_started` with `is_backgrounded: true`, the process exits after ~10 minutes of no completions, both tasks end with `task_notification status=stopped`, `result.subtype=success`, exit code 0. With `run_in_background: false` (or the env switch set to 1) both return in-turn.
### Redacted record shapes from the failing runs
```
{"type":"system","subtype":"task_started","task_type":"local_agent","is_backgrounded":true,"spawn_depth":1,"task_id":"…","tool_use_id":"…","session_id":"60294b47-…"}
{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"…","content":"Async agent launched successfully. …"}]}}
{"type":"system","subtype":"task_notification","status":"stopped","task_id":"…","tool_use_id":"…","output_file":"…","session_id":"60294b47-…"}
{"type":"result","subtype":"success","is_error":false,"num_turns":20,"duration_ms":1044000,"stop_reason":"end_turn","session_id":"60294b47-…"}
```
### Asks
1. A per-command foreground-only flag for `-p` (the equivalent of the env switch), so unattended runs are deterministic regardless of what the model passes.
2. In `-p` mode, refuse to exit while the session still owns unresolved background tasks: either wait for them (bounded by a configurable deadline) or end with a non-success `result` (`subtype` other than `success`, non-zero exit code) that names the stopped tasks. Today a lost wave is indistinguishable from a clean run without parsing `task_notification` records.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with the minimal `claude -p --output-format stream-json --verbose` reproduction using Agent/Task and compare it with `run_in_background: false` or `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1`. Trace how `task_started`, `task_notification`, and the final `result` are handled in headless mode; done means unresolved owned tasks are waited for or produce a non-success result and non-zero exit rather than `success`.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100