Assistant text before/between tool calls is not rendered in the CLI, while hooks and the transcript still see it

Status Open
Reported on v2.1.170
Maintainer reply None cached
Activity 6 comments · opened Jun 10, 2026

Summary

Assistant text blocks emitted before or between tool calls in the same turn are silently dropped from the rendered CLI transcript. The text still exists in the underlying transcript — the model counts it as said, and PreToolUse/supervisory hooks read it and judge the model on it — but the human never sees it. The model, the hooks, and the user are looking at three different conversations, and two of them agree the third is wrong.

Reproduction

  1. Send the model a message mid-task (e.g. a correction).
  2. The model responds with a text block (acknowledgment/answer) followed by tool calls (Read, Write, Edit) in the same assistant turn.
  3. The CLI renders only the tool-call lines. The text block is not displayed.

What the user sees:

❯ Claude, individual edits are more costly for me, as each is a hook call.

  Read 1 file (ctrl+o to expand)
  ⎿  Loaded .claude/worktrees/exit-types/CLAUDE.md

⏺ Write(.claude/worktrees/exit-types/pkg/script/read_next_test.go)

What the transcript contains at that position: a full paragraph acknowledging the correction and describing the consolidation strategy. The user has no indication it exists.

Why this is worse than a cosmetic rendering gap

  1. The system prompt solicits exactly this text. It instructs the model: "Before your first tool call, say in a sentence what you're about to do; while working, give brief updates when you find something load-bearing." The same prompt elsewhere warns "text you write between tool calls may not be shown to the user." The harness asks the model to write into a channel it then doesn't render.
  1. Hooks validate against the transcript, not the rendering. In our setup a PreToolUse reasoning-audit hook reviewed a blocked edit and wrote, verbatim, "The agent acknowledged this" — about the invisible paragraph. Supervisory tooling confirmed communication had happened; the human it was addressed to saw nothing. Any hook- or transcript-based oversight (audits, "did the agent reply" checks, post-hoc review) inherits this false positive.
  1. The failure is silent on both sides. The model has no signal its text was dropped; the user has no signal text existed. In our case this compounded into a day of escalating conflict: the user repeatedly demanded answers the model believed it had already given — because it had, into the void.

Expected behavior

Render assistant text blocks wherever they occur in the turn (collapsed/dimmed is fine), or make the drop consistent end-to-end: if pre-tool-call text is not rendered, it should not be solicited by the system prompt, and it should be flagged or excluded for hooks so transcript-based review and the human are judging the same artifact.

Environment

  • Claude Code 2.1.170, macOS (darwin), CLI terminal UI
  • Observed with text-then-tool-call turns of both single and batched tool calls

View original on GitHub ↗

4 Comments

mdickinson · 2 months ago

See also #66960, #65662

yurukusa · 2 months ago

A verified data point and a way to disambiguate the "three views" you describe — because the evidence in the sibling reports actually conflicts, and it's worth pinning down before this gets chased in the wrong layer.
The conflict: this report (and the hooks-see-it observation) says the text exists in the transcript and reaches the client. But over in #67166, @Luunae reports API-level evidence that the text block never reaches the client at all. Those can't both be true for a single bug — so either there are two failures being merged, or "transcript" means different things (the persisted .jsonl vs. the live API stream).
What I can verify locally (2.1.172, WSL2 — caveat: I'm on Opus 4.8, not Fable 5): I scanned 23,493 assistant messages in my own ~/.claude/projects/*/*.jsonl. Assistant messages that contain both a text block and a tool_use block in the same turn are rare for me — only 6 — but in all 6 the text block comes first and is fully preserved in the persisted transcript (I can read the text back). So at least on the persistence side, when text precedes a tool call in one turn, it does reach the client and survive. That's consistent with your "the text still exists in the transcript / hooks read it" observation.
The rarity is probably the why-Fable-5 part: Opus almost never emits a prose block and an AskUserQuestion/tool call in the same turn, so it rarely trips this. Fable 5 does it constantly (text deliverable → AskUserQuestion to confirm it), which is why #67112 / #67166 / #66960 all surface it through Fable 5 and it feels model-specific.
A clean disambiguating test: 2.1.172 made the fullscreen (alternate-screen) renderer the default, and it's already behind several display regressions (e.g. #67300, scroll wheel sending arrow keys). Try /tui default to drop back to the inline renderer, then reproduce:

  • if the missing text block now appears, it's the fullscreen renderer dropping it after it reached the client (a rendering bug), and
  • if it's still missing, that points upstream and supports @Luunae's "never reaches the client" reading.

Either outcome narrows it to the right layer, and /tui default is at least a usable stopgap for anyone blocked right now if it turns out to be the renderer.

alexeyv · 1 month ago

Two additions: a reliable way to reproduce this, and evidence for the question raised above about whether the dropped text is persisted.

Reproduction: we use an output style that tells the model to end every response with a Bash call (it runs say to read a summary aloud). This means every turn ends with text followed by a tool call — and that final text, which contains the actual answer, was dropped every time (4 out of 4 turns in one session). Claude Code 2.1.200, macOS, claude-fable-5.

The dropped text is not in the session .jsonl file. I searched the file for phrases unique to each dropped message: no matches for any of the four. Phrases from messages that did display are all there. Listing every assistant text block in the file confirms it: everything that displayed is in the file, nothing that was dropped is. (Search patterns were written as character classes like commen[t] so they couldn't match their own persisted tool-call entry.)

So in our case this is not a display bug — the text never reaches the transcript at all. That matches #67166 rather than the report here that hooks and the transcript still see the text; possibly these are two different bugs. It also means the /tui default test suggested above can't settle it for this variant, since there is nothing in the transcript for the renderer to drop.

One more detail: text at the start of a turn, before the first tool call, almost always survived. What got dropped was text later in the turn, between tool calls or right before the final one.

alexeyv · 1 month ago

Follow-up with a controlled test isolating the model as the variable.

I reran the exact same setup — same output style (ends every response with a Bash say call), same CLI (2.1.200), same macOS — changing only the model:

  • claude-fable-5: the final text before the trailing tool call is dropped from both the screen and the persisted .jsonl (as reported above).
  • claude-opus-4-8: identical setup, and the final text both renders on screen and persists to the transcript normally. Confirmed visually by the user and by grepping the session .jsonl (the Opus test message is present; logged under claude-opus-4-8).

So with everything else held constant, this reproduces on Fable 5 and not on Opus 4.8 — consistent with the other Fable-5-centric reports in this thread.

One caveat worth flagging separately: on the Opus turn, the harness injected an automated [Your previous response had no visible output] notice even though the text was in fact visible and persisted. That looks like a false positive in that check, distinct from the actual drop bug — but if the two share code, it may be a useful thread to pull.

Showing cached comments. Read the full discussion on GitHub ↗