[BUG] `Monitor` tool's return message advises text-only `end_turn` — deterministically kills the watched task in headless (`claude -p`) mode

Status Open
Maintainer reply None cached
Activity 0 comments · opened Jul 26, 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?

Below is claude-authored, but I think it is a real issue that might be uncommon to detect since it is a headless-mode issue.

Summary

The Monitor tool's return message tells the caller "Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply." In headless (claude -p) mode, there is no "waiting for the user" — the only next thing is process exit. An agent that faithfully follows the return message emits a text-only reply (no further tool calls), which the CLI treats as end_turn. Per the v2.1.163 behavior (documented request in #65498), background shells are stopped ~5s after the final result. The task the caller just armed Monitor on is one of those background shells, so it is killed within ~5s of the very reply that Monitor's return message advised.

Net effect: Monitor in headless mode is a foot-gun whose own return message primes the caller to trigger the foot-gun.

Environment

  • Claude Code version: 2.1.20x (headless claude -p invocation, macOS host, Docker Linux container)
  • Model: claude-sonnet-5
  • Mode: headless (claude -p), non-interactive; no stdin left open

Transcript excerpt (real failure — anonymized paths)

18:43:45  assistant → Bash "timeout 300 standards run-scripts 2>&1 | tail -150"
18:45:45  Bash returns auto-promotion notice:
          "Command did not complete within its 120s timeout and was moved to the background
           (ID: beki1i197). Output is being written to: <file>. You will be notified when it completes."
18:45:47  assistant → ToolSearch "select:Monitor"
18:45:53  assistant → Monitor { command: "until [[ -f ... ]]; do sleep 2; done; tail -f ...;
                                while kill -0 $(pgrep -f 'beki1i197' | head -1); do sleep 5; done" }
          Monitor returns: "Monitor started (task bw1gbr7h4, timeout 600000ms).
                             You will be notified on each event.
                             Keep working — do not poll or sleep.
                             Events may arrive while you are waiting for the user — an event is not their reply."
18:46:02  assistant → text "Kicked off standards run-scripts via Monitor since it exceeds the
                              120s foreground threshold; I'll continue once it reports back."
          CLI:      stop_reason: "end_turn"
18:46:02  (+~2ms) task beki1i197  → status: "killed"
18:46:02  (+~2ms) task bw1gbr7h4  → status: "killed"

Design collision (precise statement)

Two behaviors are in effect and disagree, and the tool's own return message is on the wrong side of the disagreement:

  • Documented (v2.1.163): "Fixed claude -p hanging forever after its final result when a backgrounded command never exits — background shells are now stopped ~5s after the result once stdin closes." (See open documentation issue #65498 requesting this be surfaced in the headless docs.)
  • Monitor tool return message (verbatim, current): "You will be notified on each event. Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply."

The return message is written assuming an interactive REPL where an idle turn is safe (a subsequent turn will arrive). In headless mode, an idle turn is fatal — it emits end_turn, stdin closes, and the ~5s reaper takes both the watched task and the Monitor watcher itself.

Related issues

  • #65498 (OPEN, documentation) — Headless docs omit claude -p background-shell cleanup after final result. This is the docs-side of the same behavior — the ~5s reaper — that is being triggered here. This bug is arguably that issue's tool-side analogue.
  • #77300 (OPEN)"Monitor events and background-task notifications never wake an idle teammate — dropped, not queued (only SendMessage wakes it)." Same failure class (Monitor events don't reach an execution context that has stopped) but scoped to teammates in interactive mode. References #39632 as the closed/stale "same mechanism class root-caused for headless main sessions".
  • #79348, #80235, #80373, #78489, #76508, #80610 — Other live Monitor bugs currently open on 2.1.20x; useful context for how much triage the subsystem is already receiving.

Workaround (in use)

Route long-running commands through a dedicated subagent (Task-tool invocation) whose own turn owns the background+polling lifecycle. The subagent uses run_in_background: true + per-tick BashOutput-style polling in fresh Bash tool calls (avoiding both wait $PGID — returns 127 from a non-parent shell — and single-call polling loops that themselves auto-promote at 120s). The parent turn does not end until the subagent returns, so no reaper can fire.

This works but requires enumerating every long-runner in a routing table maintained on the caller side. Any command not yet enumerated is exposed to the class of bug reported here.

Impact

  • Silent kill: partial worktrees, zero committed artifacts, no diagnostic beyond the terminating text line.
  • Failure is deterministic once a long-runner is not in the caller's enumerated routing table.
  • Recovery costs a full re-run of the operation to root-cause and complete.

What Should Happen?

Two options, listed lightweight → heavyweight:

(a) Mode-aware return message (one-string swap)

When Monitor is invoked in a headless (claude -p) session, its return message becomes something like:

"Monitor started (task <id>, timeout <Nms>). This session has no next turn — do NOT end the turn on this notification. Poll via BashOutput (or a synchronous polling pattern) until the awaited event fires. Ending the turn will cause the ~5s background-shell reaper to kill this task."

Zero runtime behavior change; just aligns the tool's own advice with the mode's constraints. This alone dissolves the specific class of failure our workflow hit.

(b) Mode-aware synchronous execution (durable fix)

In headless mode, Monitor blocks the return until the awaited event fires (or the caller's timeout_ms elapses). No end_turn gap possible between arming and receiving the event, because the arming call itself doesn't return until the event fires. This is the behavior an agent that reads the current return message believes it is getting; making Monitor's behavior match its own message is arguably the durable fix.

Error Messages/Logs

Steps to Reproduce

  1. From a claude -p invocation, run any long-running command via the Bash tool. Any command that exceeds the Bash tool's ~120s foreground threshold and auto-promotes to background will do (in our case, standards run-scripts on a monorepo).
  2. On auto-promotion, the Bash tool returns: "Command did not complete within its 120s timeout and was moved to the background (ID: <task_id>). Output is being written to: <file>. You will be notified when it completes."
  3. ToolSearch: select:Monitor to load the Monitor schema, then invoke Monitor with any liveness-probe body (e.g., while kill -0 …; do sleep 5; done).
  4. Monitor returns: "Monitor started (task <id>, timeout 600000ms). You will be notified on each event. Keep working — do not poll or sleep. Events may arrive while you are waiting for the user — an event is not their reply."
  5. The caller emits a short text explanation (no further tool calls) — a natural response to the "Keep working" instruction when there's nothing else to do until the event fires. CLI treats this as end_turn.
  6. Within ~2 milliseconds of end_turn, both the original backgrounded task and the Monitor's watch process are marked status: killed (see transcript excerpt below).

The standards run-scripts invocation had roughly 3 minutes of work left; it was killed with zero committed artifacts and no post-process diagnostic beyond the final text line.

Claude Model

Sonnet (default)

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.1.20x

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

iTerm2

Additional Information

_No response_

View original on GitHub ↗