Blank stdout tool call at end of turn triggers false 'no visible output' retry loop
Summary
When a response ends with a Bash tool call that produces no stdout (e.g. afplay /System/Library/Sounds/Hero.aiff -v 0.7 &), Claude Code's rendering system incorrectly detects 'no visible output' for the turn and auto-injects a retry prompt: [Your previous response had no visible output. Please continue and produce a user-visible response.]
This causes the model to regenerate the same response, resulting in the user seeing identical responses 2-3 times in a row.
Steps to Reproduce
- In any Claude Code session, end a response with a Bash tool call that produces no stdout output
- Example: last action is
afplay /System/Library/Sounds/Hero.aiff -v 0.7 &(background process, no stdout) - Observe: the system injects a 'no visible output' retry prompt
- The model regenerates, producing a duplicate response
- If the regenerated response also ends with the same no-stdout Bash call, it loops again (confirmed up to 3 duplicates)
Expected Behavior
A turn that contains substantive text output followed by a tool call with no stdout should be considered to have visible output. The 'no visible output' detection should check whether the turn contained any text content, not just whether the last tool call produced stdout.
Actual Behavior
The turn is flagged as having no visible output, triggering a retry. The user sees the same response 2-3 times.
Context
- Platform: macOS (darwin 25.3.0)
- Shell: zsh
- The text content of the response IS visible to the user — only the final tool call produces no stdout
- The issue is specifically triggered by tool calls with empty stdout, not by missing text output
Workaround
None available when the CLAUDE.md rules require a specific command as the last action of every response.
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Root cause from string-inspecting the compiled bundle (claude-code 2.1.202, native build): the reporter's mechanism guess ("checks only the last tool call's stdout") isn't what fires. The actual detector keys on thinking-only turns, not tool stdout.
The injected string lives in a branch guarded by this condition (de-minified, variable names as-found):
if ( (stop_reason === "end_turn" || stop_reason === "stop_sequence")
&& !isApiError && mode !== "compact" && !<guardA> && !<guardB> && !<guardC>
&& !responseMessages.some(m =>
m.content.some(b => b.type === "text" && b.text.trim().length > 0))
&& !alreadyNudged ) {
telemetry("query_thinking_only_response", "nudged");
inject("[Your previous response had no visible output. Please continue and produce a user-visible response.]");
}
Key points:
textblock in the turn. Tool calls are not counted.stop_reason === end_turn/stop_sequence. A turn that ends in a tool call hasstop_reason === "tool_use"and is excluded — so the nudge does not fire on the tool-call turn itself. It fires on a subsequent thinking-only turn that ends with no text block. The telemetry name (query_thinking_only_response) and state flag (thinkingOnlyNudged) confirm this.Same root cause, second manifestation — Dispatch / Cowork: there the user-facing channel is the
SendUserMessagetool, which produces notextblock. A legitimate "reply via SendUserMessage, then end" exchange leaves a thinking-only end-turn with no text → false nudge → the Dispatch system prompt re-interprets "produce visible output" as "call SendUserMessage again" → the same message is resent 3–5×.Suggested fix: treat a turn as having visible output when it delivered user-facing content via a tool (notably
SendUserMessage, and side-effecting bash), not only when a non-emptytextblock is present — i.e. suppressquery_thinking_only_responsewhen the current exchange already reached the user through a tool.Repro (Dispatch): any turn whose only user-facing output is a
SendUserMessagecall, followed by an empty/thinking-only turn.Same bug via a different trigger: a turn ending on an MCP tool call with no visible output
We hit this via the Claude Agent SDK (
claude-agent-sdk==0.2.111, bundled CLI), and the trigger is broader than empty Bash stdout — it's any turn whose final block is a tool call that produces no user-visible output, including custom MCP tools.Setup: streaming input,
include_partial_messages=True, a custom MCP tool (suggest_prompts) that sets suggested-reply chips and intentionally returns no user-facing text. Our system prompt instructs the model to call it as the last action of a turn (after it has already produced its full answer).What happens: the model produces a complete, substantive text answer, then calls the MCP tool as the final block. The CLI flags the whole turn as "no visible output" and injects:
The model then regenerates its answer, so the user sees the response 2–3×. This fires on essentially every turn for us, because ending a turn on a silent side-channel tool is intentional in our agent design.
Root of it: the "did this turn produce visible output?" check appears to consider only the final block, ignoring the text (and any visible tool output) emitted earlier in the same turn.
Two asks:
nudge_on_empty_turn=false) for agents that intentionally end turns on silent tool calls — there's currently no setting/env/SDK flag to disable this.Environment: macOS (darwin arm64),
claude-agent-sdk0.2.111.Dispatch/Cowork: this fires on every single turn
In Dispatch mode, the agent's only output channel is the
SendUserMessagetool — plain text replies aren't rendered. So every legitimate response triggers the false nudge, since there's notextblock in the response. The nudge looks like a new user turn, the agent responds, and the loop produces 5-15 duplicate messages before the agent catches on.Messages deliver fine to the user's phone — confirmed via screenshots. The nudge is purely a false positive.
Suggested fix
The
thinkingOnlyNudgeddetector should recognizeSendUserMessagecalls as visible output, or expose a setting likenudge_on_empty_turn = falseso Dispatch sessions can opt out.Workaround
Instruct the agent (via memory or CLAUDE.md) to stop responding after 1-2 phantom prompts. Can't prevent the nudge, but breaks the loop.
Please bump, still active and resending response 2-5x again and again. Repro and scenario as described multiple times above from various perspectives.