Waiting on background tasks: repeated no-op status probes silently multiply usage, and nothing detects or surfaces it
Summary
While a session waits for a backgrounded Bash task to finish, Claude Code will happily emit dozens or
hundreds of turns whose only content is a trivial status probe (ls DIR | wc -l, wc -l some.log)
followed by a line of text saying it is waiting. Each of those turns is a full model round-trip that
re-reads the entire conversation, so the marginal cost of one "are we there yet?" is proportional to
the size of the conversation, not to the size of the command. Over a long session this dominates
everything else the session did.
Nothing in the product prevents it, warns about it, or makes the cost visible while it is happening.
What happened
A long-running session launched several long background jobs (run_in_background: true) and then
waited for them. Instead of ending the turn and letting the completion notification wake it, it
looped for hours: one cheap read-only shell command, one "waiting" line, end turn — repeated on the
order of a hundred times.
Measured from the local transcripts over one six-hour window on this machine (deduplicated by
message id):
- ~2,640 model requests
- ~630M cache-read input tokens, ~10M cache-write, ~12K uncached input, ~2.6M output
- ~$500 of API-equivalent usage, of which one session's wait loop was the single largest share
The output of the probes was never used for any decision. The information they produced ("23 files",
"15 lines") changed nothing; the loop existed only so that the "waiting" line had something in front
of it.
Why this is a product issue, not only a prompting issue
- Backgrounded work already notifies on completion. Polling is never necessary, so a session
that polls is burning a full context re-read for nothing, every time. The tool description says
the completion notification arrives on its own, but nothing makes that operationally salient at
the moment the model is deciding what to do next in a wait.
- Per-turn cost is invisible during the session. There is a token-usage line, but nothing says
"this turn cost you a full re-read of a 300k-token conversation and produced no new information".
The failure is only visible afterwards, in billing or in the transcripts.
- Nothing detects repetition. An identical or near-identical read-only tool call, issued N times
in a row with no state-changing call in between and no new user input, is a recognisable pattern
and a reliable signal of an idle loop. Today it is treated exactly like productive work.
- Instructions alone do not hold. This environment already carries an explicit, emphatic written
rule against polling, and a local PreToolUse hook that refuses sleep and refuses repeated
reads of the same file. The session in question knew the rule. It still looped, because each
individual probe looked productive in isolation — a directory listing whose count changes is, by
any local test, a command that "does something". Guidance is not the layer that fixes this.
What would fix it
Any one of these would have prevented the whole event; the first two are the valuable ones.
- An idle-loop detector in the harness. When a background task or subagent is in flight, and the
assistant issues a read-only tool call whose normalized form and target it has already issued
several times in that same wait, with no state-changing call in between — inject a short system
reminder, or refuse it, saying the completion notification is coming and the turn should end.
Repetition inside a wait, not the command itself, is the signal.
- Make the cost of a turn visible while it happens. A running per-session cost or cache-read
figure, and ideally a nudge when consecutive turns cost a lot and produce no state change.
- A user-configurable ceiling on consecutive tool calls in a wait, or on session spend, that
produces a warning rather than a silent burn.
- Sharpen the tool-side wording: state in the background-task tool result itself that no polling
is required and that reading a task's own output file before the notification arrives is never
informative.
Environment
- Claude Code in the desktop app, Windows.
- Long sessions with automatic compaction disabled, so conversations are large by design and each
extra turn is correspondingly expensive.
- Several concurrent background Bash tasks and subagents.
What I have done locally
Written a rule against it (did not hold), and a PreToolUse hook that refuses sleep and repeated
reads of one file (did not catch this shape). I am now extending that hook to key on "read-only call
repeated inside a wait, with nothing productive in between" rather than on a list of known status
files. That works, but every user of Claude Code who runs long background jobs has the same hole and
no hook, and the harness is where the signal actually lives.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗