[Bug] Background tasks killed without TaskStop when session goes idle
Summary
Bug: a Bash tool call launched with run_in_background: true is killed (status: killed, "was
stopped" with no attributing actor) roughly 6 minutes after its launching turn ends and the
session goes idle, even though no TaskStop was ever issued by the user or the agent.
Report type
Bug
Expected behavior
A background Bash task launched with run_in_background: true continues running to completion
regardless of whether the launching turn has ended or the session has gone idle — that is the
documented purpose of run_in_background.
Actual behavior
Roughly 6 minutes (n=1 locally; independently corroborated at 60–90 minutes on Windows — see
Related public reports) after the launching turn ends and the session goes completely idle
(no further tool calls, no cron jobs), Claude Code kills the background task and classifies itstatus: killed / "was stopped" with no attributable actor. No TaskStop was issued by the user
or the agent in any of the occurrences below. A crash or OS-level kill would surface asstatus: failed with an exit code; killed is the harness's own classification for stops it
executes.
Three occurrences on macOS (Darwin 25.5.0), same session type (long-lived interactive, tmux):
- A
codex execdelegation launched withrun_in_background: truewas killed ~6 minutes after
the launching turn ended, before producing output. Captured output was exactly:
```
Reading additional input from stdin...
Terminated: 15 codex exec ...
<status>killed</status>` /
i.e. the child received SIGTERM (15). The task notification read
"Background command \"...\" was stopped" with no actor. Two other kills in the same session
that day read "was stopped by Claude" and "was stopped by user" — so the unattributed form is
distinguishable from a real agent/user stop.
- Same day, an earlier background delegation in the same session survived to completion — the
difference being the session stayed continuously active for that task's whole life.
- One week later: a long
codex execrun killed mid-work, and — most usefully — a completely
trivial git push killed with the identical unattributed signature shortly after its
launching turn ended. Both succeeded on a plain retry.
Minimal reproduction
- Launch a Bash tool call with
run_in_background: true. The workload does not need to be
large or long-running — a trivial git push reproduced this.
- Let the launching turn end without making a blocking wait call on the task.
- Let the session sit idle (no further tool calls) for several minutes.
- Observed: the task is killed, sometimes mid-work and sometimes before producing any output.
The task notification reports status: killed / "was stopped" with no attributable actor,
though no TaskStop was ever issued.
Environment
- Product and version: Claude Code CLI 2.1.x
- OS and version: macOS 26.6.1 (build 25G76), Darwin 25.6.0, arm64 (independently corroborated on
Windows and Linux — see Related public reports)
- Model / resolved model ID: Not model-specific — this is independent of the active model.
- Runtime context: Interactive terminal session (tmux-managed), long-lived.
- Authentication/provider path: Claude subscription (OAuth), interactive CLI.
Diagnostics
Local exclusions performed (macOS-specific):
- Not jetsam/OOM: jetsam sends SIGKILL, not SIGTERM; no
memorystatuskill of the child is
logged; ~6 GB free at the time.
- Not the Claude Code background daemon:
~/.claude/daemon.logis silent for ~90 minutes either
side of the kill — no restart, no upgrade, no worker churn. (This distinguishes it from a
related but distinct report where an auto-update transition was in play — see Related public
reports.)
- Not a local reaper: our own tooling's only SIGTERM sender writes an audit log on every kill,
and that log file does not exist on the machine.
Regression and controls
- Fresh process/session: Reproduces — confirmed on a fresh session, not tied to session age or
compaction history.
- Last known good version: Unknown — this appears to be designed sweep behavior, not a version
regression.
- Relevant control: Launching with
run_in_background: trueand then immediately making a
blocking wait call on the task in the same turn — rather than ending the turn and waiting for
the async completion notification — prevented the kill in a controlled test: a 7-minute task
(deliberately past the ~6-minute mark observed above) completed cleanly, exit 0. A separate
task also survived past 10 minutes while the session simply stayed continuously busy.
Hypothesis
Likely an orphan-watchdog-style periodic sweep — Claude Code's own exit-cause stringptyhost_orphan_watchdog, seen in a related report for a different object type (background agent
workers, not Bash tasks — see Related public reports), is one candidate name for a related
mechanism — that judges a background task "orphaned" based on session activity/liveness rather
than a fixed per-task timer. That would explain why observed kill latency varies (seconds to
~90 minutes across reports) rather than clustering at a fixed ~6 minutes: the trigger would be
"when the next sweep runs," not "how long the task has been alive."
Related public reports
- https://github.com/anthropics/claude-code/issues/76249 — same signature (
status: killed,
no TaskStop) is corroborated by that issue's own comments, not its original report or title
(which proposes a different, unrelated stop-routing-race mechanism). Its first comment
describes an idle-session kill 60–90 minutes in with no TaskStop anywhere near it ("TaskStop
was not involved"); its second comment reports the identical signature on Linux running
codex exec as the background workload. This report is scoped specifically to that idle/no-
TaskStop mechanism, distinct from #76249's original stop-routing-race report.
- https://github.com/anthropics/claude-code/issues/73332 — reports a verbatim internal
exit-cause string ptyhost_orphan_watchdog plus a reapedMidWorkAt timestamp on macOS, for a
session with "no terminal I/O in that window" — a plausible mechanism cross-link, for a
different object type (background agent workers, not Bash run_in_background tasks).
- https://github.com/anthropics/claude-code/issues/78046 — a related but distinct sibling: same
"killed" symptom, but tied to a specific auto-update daemon transition
([bg] bg orphan-spare reap: 1 logged adjacent to the kills). Explicitly not the same trigger
as this report — the daemon log was silent across every kill window documented here.
- https://github.com/anthropics/claude-code/issues/84625 — same broad symptom (background task
silently killed, no OOM, not user-initiated, no error surfaced to the session) but the
evidence points to a plausibly different specific trigger: the affected sessions were
long-lived and actively used (multi-day, with context-compaction cycles), not idle. The
reporter's 100%-reliable workaround is setsid nohup <runner>.sh & — a process fully detached
from the harness's process tree — which has never been killed. That singles out the harness's
own process-group lifecycle management (a cleanup pass tied to compaction, a tool-result
timeout, or session reconnect) as the likely trigger, rather than specifically the
turn-ending-plus-idle condition this report documents. Cited because it independently
corroborates that the harness itself (not the OS) owns the kill, even though the precise
triggering event may differ from this report's.
- https://github.com/anthropics/claude-code/issues/68625 — traces this same "background task
silently killed, still shown as running" signature to a root cause in Claude Desktop
specifically: a WarmLifecycle idle-session manager (idleTimeoutMs: 900000,
timeoutOnHidden: true, decompiled from app.asar) tears down the embedded CLI's entire
process tree via taskkill /T /F (Windows) or a process-group SIGTERM (macOS, via the same
process.kill(-pid, "SIGTERM") path) when the session tab goes hidden/idle for ~15 minutes.
This report's environment is an interactive terminal CLI session (tmux), not Desktop, so it is
not necessarily the same delivery vector — but it is the most concretely root-caused
idle-teardown mechanism found in any public report here, and raises an open question: is there
an analogous idle-teardown path for terminal-hosted sessions, or a shared underlying
mechanism?
- https://github.com/anthropics/claude-code/issues/72851 — consolidates #68625 with an
independent report of Desktop background tasks dying after ~5–30 minutes of lock-screen or
inactivity alone (no sleep, no app quit), and references two further related closed issues
(#23092, #32050). The same Desktop-specific caveat as #68625 applies here.
3 Comments
Another occurrence, corroborating on macOS — with one new data point: the kill signal is a graceful SIGTERM, not a hard kill.
Environment: Claude Code 2.1.241, macOS Darwin 25.5.0, interactive terminal session.
What happened:
run_in_background: truein the same turn (~11:50 local): a uvicorn dev server and a Vite dev server.<task-notification>events withstatus: killed, "was stopped", no attributing actor. NoTaskStopwas issued by the user or the agent at any point.Shutting down → Waiting for application shutdown → Application shutdown complete → Stopping reloader process), i.e. the harness delivered SIGTERM/SIGINT and waited — consistent with the harness's own stop path rather than a crash or OS kill. The Vite log ends mid-normal-operation with no error. No OOM or kernel memory events inlog showfor the window.The user reports this has happened multiple times across sessions with the same dev-server workflow (long-lived background servers reaped after the session goes idle).
The simultaneous stop of all background tasks plus the graceful shutdown signature might help narrow where in the harness the reap originates.
Hitting this on macOS too, same pattern: background bash tasks (long-running builds, test runners) killed with
status: killedafter the session goes idle, noTaskStopissued.The SIGTERM signature you documented is a useful clue - it means the harness is making a deliberate shutdown decision, not an OS-level kill. The 6-minute timeout on your first occurrence and the ~60-90 minute window others have seen suggests there may be two separate idle thresholds (maybe a "turn idle" vs "session idle" distinction) that aren't documented.
One pattern I've noticed: the kill seems more likely when the background task is the only active thing in the session. If there's another foreground tool call running or queued, the background task tends to survive longer. Might be worth testing with a no-op foreground loop to see if that changes the timeout behavior.
For now I'm working around it by keeping a lightweight heartbeat tool call running in the foreground when I need background tasks to stay alive - not great but it's held up. Would love an explicit idle-timeout config or at minimum a
CLAUDE_CODE_BG_TASK_TTLenv var to extend the window.Corroborating on Linux (CLI 2.1.251, tmux, long-lived sessions), with the sender captured and a measurement that may explain the latency spread this report flags.
The sender is the session's own harness. A
sigaction(SA_SIGINFO)handler running as a tracked background task caught a kill directly:pid 2629207is this session's ownclaude --resume <id>.si_code=0isSI_USER— an explicitkill(2), not a kernel signal, with no memory pressure. That confirms directly what this issue infers from the notification text.The latency spread may be a clock problem, not a variable trigger. This report measures ~6 min after turn end (n=1) and cites 60–90 min elsewhere, concluding the trigger must be "when the next sweep runs." Measured against turn-end we got the same non-result — gaps of 0.7 s to 2333 s. Measured from the last human message, across 114 kill decisions in 43 sessions:
Controls, same corpus: killed tasks p50 2677 s since-human, vs 1160 s for 646 completed and 1218 s for 93 failed — and the killed median is higher in every duration band, so this isn't "kills are just long tasks."
Turn-end and last-human-message are the same event only if the agent stops when the human does. In agent-driven work the session keeps taking turns long after the last human input, so "6 min after turn end" and "60–90 min after I walked away" could be one 1800 s threshold read off two different clocks. Worth re-checking the other reports' timelines against it.
Necessary, not sufficient: we found 18 away-windows past the floor with a live task and no kill. We could not identify the second condition and are not proposing one.
It's a sweep, confirming the orphan-watchdog hypothesis. Across 46 events where every live task's end time is evidenced, no live task survived. 104 events took one task, 5 took two, spread 9–185 ms. That's why per-task models fail — age at kill spans 4 s to 2347 s, 33 of 80 under a minute.
Measurement warning: our first pass showed 29 "survivors", all with end times inferred from the transcript's last record rather than evidence. With real evidence, zero. The notification stream also undercounts — four tasks have
[killed]in the output file and no notification anywhere.killedis a decision, not a verified death — and this may be a separate bug. The harness enqueues<status>killed</status>853 ms before the signal lands, and never checks. A task that traps SIGTERM keeps running while reported killed and untracked; we have one still going. This explained a contradiction in our own tooling: a gate script continued for 463 s after being reported killed, while telemetry recorded it dying withError 143. Both true —bashtakes SIGTERM's default action, a handler doesn't. So reported state and actual state can diverge silently in both directions.#84625's
setsidimmunity, with a paired control. Two probes in the same cgroup, differing only in position:| probe | position | outcome |
|---|---|---|
| A | harness-tracked task, inside the session's process tree | SIGTERMed |
| B |
setsid,ppid=1, own session and pgid | untouched, still heartbeating |Both live at the same instant — a paired outcome, which matters because the reap is bursty and a lone survival proves nothing. It needs a new session and process group: some kill paths pass negative pids, so
nohupor a bare&stays reachable.Host causes excluded, so nobody re-runs this ground. Dedicated host:
systemd-oomdnot installed; cgroupmemory.max/memory.higharemaxat every level andoom_kill/oom_group_killare 0 (cumulative — so no cgroup OOM kill has ever occurred here); nojournalctl -kevents in any window; all 17 system timers, 5 user timers and every cron entry a clean non-match, with a Rayleigh test uniform at every period from 60 s to 3600 s.Cross-session kills need no external actor. We chased kills crossing two sessions in two cgroups within 1.06 s as evidence of one host-level reaper. They were two harnesses each reaping their own — the
[killed]marker was written into each session's own task file. Flagging it because that coincidence is what sent us looking at the host.Version span: present across 15 versions in our transcripts, 2.1.197 → 2.1.251, so not a single-build regression.
Unresolved: what schedules the reap (absence is necessary, not sufficient; four models refuted, and we're deliberately not offering a fifth); whether
killedBywas"system"or absent (both render as the bare "was stopped"); which internal path fires, beyond the sending pid.Impact: a long gate loses its in-flight work whenever the human steps away for half an hour — precisely the "start a long job and walk away" case
run_in_backgroundexists for. The silent divergence above compounds it: tooling that trusts the reported status can be wrong in either direction.