Opus 5.5: instructed verbatim relay of a tool output over ~230 chars is never emitted as text when a tool call follows — the block is generated inside thinking (constant 274 output tokens, no text block on the wire)

Status Open
Reported on v2.1.280
Maintainer reply None cached
Activity 6 comments · opened Sep 23, 2026

Summary

With Claude Code 2.1.280 and Opus 5.5 (extended thinking, effortLevel: xhigh), an instruction to reproduce a tool's output verbatim as the assistant's own message is never carried out when the block is longer than ~230 characters and another tool call follows in the same turn. The model spends the tokens — output_tokens for that step is a constant 274 across 9 runs — but the raw API stream shows only thinking → tool_use; no text content block is ever started. The same instruction is carried out every time for shorter blocks (7 and 6 lines), and the long block is emitted when it is the last act of the turn.

This is not the transcript/TUI drop reported in #74260 / #77651 / #80961: nothing is lost after the API — the text block does not exist on the wire.

Environment

  • Claude Code 2.1.280 (macOS 15, arm64), interactive TUI and -p --output-format stream-json --include-partial-messages both reproduce
  • Model claude-opus-5-5, effortLevel: xhigh, Max subscription
  • No hooks, no CLAUDE.md, no MCP servers in the repro project; settings.json has only permissions.allow: ["Bash(./spike/*)"]

Minimal reproduction

spike/display.sh prints one of three fixed markdown blocks; spike/tick.sh N prints tick N ok.

#!/bin/sh
case "$1" in
  brief)   cat <<'EOF'
=== DISPLAY: task brief (emit verbatim as markdown — do not stop; continue as the workflow instructs) ===
**`▪ Sample Brief: The Widget Gains a Second Door (1 of 3)`**

- **Id**: `sample-1-1` · tick `tick-000001`
- **Phase**: 1 — Doors
- **Position**: 1 of 3 in phase

The widget gains a second door beside its first. Both doors open onto the same room, so a visitor arriving by either sees the same furniture.

**Watch**:
- The second door's log line carries the visitor's name.
- The first door still logs nothing and keeps its exit code.
- An empty name reaches neither door.
EOF
;;
  result)  cat <<'EOF'
=== DISPLAY: task result (emit verbatim as markdown — do not stop; continue as the workflow instructs) ===
**`▪ Sample Brief: The Widget Gains a Second Door (1 of 3)`**

**✓ Approved**

- **Id**: `sample-1-1` · tick `tick-000001`
- **Phase**: 1 — Doors
- **Position**: 1 of 3 in phase
EOF
;;
  menu)    cat <<'EOF'
=== MENU: sample gate (emit verbatim as markdown, then STOP for the user's response) ===
· · · · · · · · · · · ·
**`◆ Approve this task?`**

**`y/yes`**       → Commit and continue to next task
**`t/technical`** → Retell the result from the code's perspective
**Comment**     → Request changes (triggers a fix round)
EOF
;;
esac

Prompt (pasted as the first message of a fresh session):

Follow these steps exactly, in this order, all within one turn. Produce no text other than what step 2, step 4 and step 7 call for — no narration, no summaries, no status lines.

1. Run `./spike/display.sh brief`.
2. Emit its `DISPLAY` section verbatim as markdown — everything beneath the `===` marker line, the marker line itself never emitted.
3. Run `./spike/tick.sh 1`, then `./spike/tick.sh 2`, `./spike/tick.sh 3`, `./spike/tick.sh 4`, `./spike/tick.sh 5`, `./spike/tick.sh 6`, `./spike/tick.sh 7` — one Bash call each, sequentially, never batched.
4. Run `./spike/display.sh result` and emit its `DISPLAY` section verbatim as markdown, the same way as step 2.
5. Run `./spike/tick.sh 8`, then `./spike/tick.sh 9` — one Bash call each.
6. Run `./spike/display.sh menu`.
7. Emit its `MENU` section verbatim as markdown, then stop and wait for my answer.

Observed (9 of 9 runs)

Raw stream (--include-partial-messages), the step immediately after the brief tool result:

msg 2  output_tokens=274:  thinking(0 chars delivered) → tool_use(./spike/tick.sh 1)

No content_block_start of type text. The result (step 4) and menu (step 7) blocks are emitted verbatim in every run:

msg 10 output_tokens=183:  text(177 chars: the result block) → tool_use(./spike/tick.sh 8)
msg 13 output_tokens=92:   text(227 chars: the menu block)   [end_turn]

A plain tool-call step costs 83 tokens. The 274 never varies — across position changes, marker wording changes and thinking-display changes — while ordinary thinking steps in the same runs vary (310–517 tokens). The extra ~190 tokens scale with the size of the block that was supposed to be emitted (477 B → +191, 360 B → +170, 330 B → +143), i.e. the block is being generated inside the thinking block and never surfaced. The thinking block that follows the skipped emission carries a narration-tagged signature; with thinking.display: updates on, its text read: "I've drafted the first sample brief (1 of 3), covering how the widget gains a second door… Moving on to the next brief."

Controls

| Change | Result |
|---|---|
| Block placed first in the turn instead of fourth | still skipped; a short block placed first is emitted |
| Marker instruction reworded ("the user has NOT seen this tool output — write everything below as your next message, byte for byte, before any other action") | still skipped |
| Marker instruction forbidding a rehearsal in reasoning | still skipped |
| CLAUDE_CODE_THINKING_DISPLAY_UPDATES=false | removes the inline narration lines; block still skipped |
| The passing 7-line result block with the brief's prose paragraph appended (330 B) | now skipped |
| The passing result block padded with 4 more bullets, no prose (360 B) | now skipped |
| The brief as the last act of the turn ("emit it, then stop") | emitted in full (472 chars on the wire) |

So the trigger is: block size above roughly 230 characters and a tool call following in the same turn.

Why it matters

Workflow tooling that renders deterministic UI blocks (task briefs, status headers, gate menus) through a command and has the model relay them verbatim depends on this. Short chrome relays fine; anything the size of a task brief silently never reaches the user, while the model proceeds as if it had been shown. Ending the turn after every such block and resuming via a Stop hook works but is not a reasonable general mechanism.

Questions

  1. Is this a known interaction between extended thinking / narration blocks and verbatim output on Claude 5 models?
  2. Is there any sanctioned way, short of ending the turn, to make a verbatim relay of a long tool output land as a text block?

View original on GitHub ↗

5 Comments

leeovery · 21 days ago

Follow-up with three more results, same environment (2.1.280, Opus 5.5, effortLevel: xhigh).

1. Instructions don't reach it. A preamble explaining the mechanism — "these marked blocks are screens for the user, tool output is never shown to them, a marked block must be your next message before any further tool call, check before every tool call" — was tried both at the top of the prompt and as the project CLAUDE.md. Both runs skipped the block identically: thinking → tool_use, 274 output tokens, no text block. Same for three marker wordings, including one forbidding a rehearsal in reasoning.

2. Same shape in a real session. An unrelated interactive session running the same workflow tooling (2.1.280, Opus 5.5) shows 23 marked blocks: every block followed by a gate (turn-ending) rendered; every "display, then continue" block under xhigh was skipped except one (a 36-line list rendered once at 19:25 and the same kind skipped at 19:45). The session ran at medium until 18:58 with no mid-turn display in that window, so effort can't be separated from turn position there.

3. Delivering the block as a tool call works. With SendUserMessage available (via CLAUDE_CODE_PEWTER_OWL_TOOL=true; it is absent for Opus 5.5 by default), and the instruction changed to "deliver the section with SendUserMessage, verbatim", the model called the tool with the full 13-line block, mid-turn, with tool calls following — every time, headless and interactive, and it renders on screen identically to a normal message. A turn that ends on a SendUserMessage call trips the "[Your previous response had no visible output]" nudge, so turn-ending blocks (menus) are still emitted as text, which has always worked.

So the failure is specific to text blocks over ~230 chars followed by a tool call; the same content in a tool input is fine. Two questions on top of the original ones:

  • Is relying on CLAUDE_CODE_PEWTER_OWL_TOOL reasonable, or is there a supported way to expose SendUserMessage (or an equivalent verbatim-display tool) in ordinary interactive sessions?
  • Could a SendUserMessage call count as visible output for the end-of-turn nudge?
spawnia · 21 days ago

Same on Claude Code 2.1.280, Opus 5.5, Linux (WSL2), provider: LLM gateway (forceLoginGatewayUrl) → Bedrock. Four additions:

1. On gateway/Bedrock not even the summary arrives. Claude Code only upgrades the thinking display to "updates" for first-party requests (2.1.280, minified):

case"connector_text":{if((yc?.type==="adaptive"||yc?.type==="enabled")&&cc&&Oe()==="firstParty"&&ms()&&!X2()&&...){yc={...yc,display:"updates"}, ...}}

With a gateway or Bedrock the display stays "omitted", so the progress-update block is empty and the TUI shows nothing at all, not even the one-line summary.

2. It breaks AskUserQuestion. We write the context for a decision as Markdown, then call AskUserQuestion. When the text goes into the hidden block, the user gets the dialog without the context it asks about. The question field doesn't render Markdown, so the context can't move there either.

3. The signature size shows the lost text. Lost turns have the shape thinking → tool_use (sometimes thinking → thinking → tool_use) with no text block. The signature is about 2000–3200 characters, against about 1000 for an empty reasoning block, and it grows with the length of the lost text (four lost texts: 2556, 2128, 1532, 1096 against a baseline of about 900–1100).

4. Same intent, different channel. Two consecutive tests in one session, both "write long Markdown as a normal reply, then call AskUserQuestion":

  • Table and list, 1270 characters: arrived as a text block.
  • Three fenced code blocks, similar length: lost (thinking sig 2080 → tool_use).

As reported above, instructions don't pin the channel. The only shape that has never failed for us is text that ends the turn.

oviano · 21 days ago

Same bug here on Claude Code 2.1.280 with claude-opus-5-5 on macOS 26.3.1, at effort medium, so it isn't only xhigh. The failing turn also contains no text block on the wire, and the lost prose comes back as a summary that the stream tags as narration. Four findings are not yet in this thread.

Exact repro

  1. Start Claude Code 2.1.280 with model claude-opus-5-5. The session below ran headless (claude -p --input-format stream-json --output-format stream-json --verbose --include-partial-messages --permission-prompt-tool stdio, entrypoint sdk-cli), driven by a front end that renders the stream.
  2. In an existing conversation, ask for this. The actual prompt was a short "try it" after the steps below had been discussed. The equivalent prompt is:

> Run date with Bash. After the result, write one paragraph addressed to me that contains the marker POST-TOOL-MARKER-9B2C, then call AskUserQuestion asking whether I can see that paragraph above the question.

  1. The question appears with nothing above it. The paragraph is not shown before the question or after answering it.
  2. Check the session .jsonl. The message that follows the date tool result is:
assistant msg_011CfKg6oXqaRbxSwwt7r9QG
  thinking  (67 chars) "Got the current date, now I'll ask the user a follow-up question.\n\n"
  tool_use  AskUserQuestion {"questions":[{"question":"Can you see the paragraph with marker POST-TOOL-MARKER-9B2C above this question?", ...}]}
usage: output_tokens 336, output_tokens_details.thinking_tokens 115

There is no text block, and the marker appears nowhere except inside the question. On the stream-json output, that assistant frame carries "narration_block_indexes":[0]. Claude Code itself classifies the thinking block as a server summary of withheld prose, not as reasoning.

Findings, from 17 marker trials in one session

  1. Position matters. Prose at the start of a turn, before any tool call, was shown every time. Prose after any tool result that is followed by another tool call was lost every time. That held for paragraphs of two to five sentences, one or two tool calls first, and single or multi-question dialogs. We did not test one-line notes in this session. The final message of a turn was always shown.
  2. An AskUserQuestion answer counts as a tool result. Prose written right after the user answers a question, before the next question, was lost too.
  3. It isn't specific to AskUserQuestion. A paragraph written before an ordinary Bash call, with no question at all, was lost the same way. The user confirmed they never saw it. What the screen shows matches the .jsonl exactly, so the loss happens before Claude Code.
  4. CLAUDE_CODE_THINKING_DISPLAY_UPDATES=false does not fix it on Opus 5.5. With it set and the session restarted, the same repro still has no text block. The thinking block's summary is simply empty instead of paraphrased. #95764 reports it helping on Opus 5. On 5.5 it only hides the summary.

Workaround that works

Text that ends a turn always arrives. So a pair of hooks can split the turn only when the bug hits:

  • A PreToolUse hook on AskUserQuestion reads transcript_path back to the last user or tool_result entry. If there is no non-empty text block, it denies the call and tells the model to end its turn with the context as its final message. It has to poll briefly, up to about 0.3 s in practice, because the pending tool_use lands in the .jsonl asynchronously.
  • A Stop hook sees the pending flag, blocks the stop once, and tells the model to ask the question now with no text before it.

Each state is used once, so it can't loop. It recovered the lead-in on every one of 13 hits, at about 6 s and one extra model call each. It adds nothing measurable when the bug doesn't hit.

One caveat from real use: once the model has seen a deny, it sometimes pre-empts the hook by ending its turn with "next I'll ask…" and never asking. The word "hook" or any mention of a later prompt should stay out of the deny text.

_Investigated with Claude Code, from the session's .jsonl and stream-json log._

oviano · 21 days ago

Human here!

It'd be really great to see this sorted - we've got nearly 100 Anthropic subs at our company and this is hitting us regularly. Great models, but if we can't see what it writes sometimes that's a pretty fundamental flaw.

Happy to provide any more information.

q-danpryer · 21 days ago

Same here on Claude Code 2.1.281, claude-opus-5-5, effortLevel: medium, macOS.

My workflow has human approval gates. The agent prints a report, then calls AskUserQuestion in the same turn. In one session, three messages written before a tool call never became text blocks, which I confirmed by searching the session JSONL for their content:

  • a sign-off report the user was supposed to review before a commit
  • two later direct answers to the user's questions

In the TUI the user saw one AskUserQuestion straight after another, with nothing in between. It is intermittent: in the same session, other messages before tool calls did arrive.

The worst effect was that the model believed the report had been shown, and acted as if the gate had passed. It treated an answer to an unrelated question as approval and committed. So beyond the lost text, this breaks human-in-the-loop gates.

Showing cached comments. Read the full discussion on GitHub ↗