claude-code - 💡(How to fix) Fix [FEATURE] `updatedAssistantMessage` field on Stop hook (or new `PreRender` hook) — let hooks deterministically transform the assistant's final text before terminal display

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Code Example

{
  "continue": true,
  "stopReason": "",
  "suppressOutput": false,
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "updatedAssistantMessage": "<text that replaces what the user sees>"
  }
}
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing requests and this feature hasn't been requested yet
  • This is a single feature request (not multiple features)

Problem Statement

There is currently no mechanism in Claude Code for a hook to transform the assistant's final text before it is rendered to the terminal. Hooks can transform tool calls (updatedInput), tool outputs (updatedToolOutput), and the user's prompt (additionalContext), but the assistant's final response is read-only across the entire hook taxonomy.

This blocks a class of skill patterns where the model emits structured data and the harness deterministically renders it to user-visible text. Concretely: a skill where the model emits a YAML/JSON payload and a template engine in a hook produces the rendered output, with no model involvement in the rendering step.

Today, implementing this pattern in the interactive REPL forces one of four trade-offs, all of which are bad:

  1. Tool-call-then-final-message pattern — model emits payload as tool_input, tool returns rendered text, model copies it to its final message. Double-emits the content (doubling output tokens), and the copy step is non-deterministic — the model can deviate between the tool input and the final message.
  2. Render-tool + PostToolUse short-circuit (continue: false) — model emits payload as tool_input once, tool returns rendered text as its output, PostToolUse hook returns continue: false to skip the next model call. Achieves single-emit and determinism, but the rendered text now routes through tool-output rendering, which collapses to ~3 lines by default (#12589, #25776, #27577). For outputs of 50–200 lines, the user sees +N lines (ctrl+o to expand) with most of the content hidden behind a manual expand action. Also depends on continue: false honoring, which has reliability issues (#29991).
  3. systemMessage — single-line notice channel. Not designed for multi-line content bodies; rendered as a single <Line> element.
  4. Hooks writing directly to the terminal — explicitly prohibited since v2.1.139: "On macOS and Linux, command hooks run in their own session without a controlling terminal as of v2.1.139. The hook process and any child processes cannot open /dev/tty or send escape sequences directly to the Claude Code interface." The architectural gap: the model's prose response is the only REPL channel that renders multi-line content without collapsing — but it's also the only channel with no harness-side transformation hook.

Proposed Solution

Add an updatedAssistantMessage field to the Stop hook's JSON output schema, analogous to updatedToolOutput on PostToolUse. When a Stop hook returns this field, the harness replaces the assistant's text in the rendered transcript before terminal display. (Alternatively, introduce a dedicated PreRender hook event that fires after generation and before terminal render. Same semantics, cleaner naming.)

Input schema (Stop hook, unchanged)

Already includes last_assistant_message as read-only input since the CHANGELOG entry: "Added last_assistant_message field to Stop and SubagentStop hook inputs, providing the final assistant response text so hooks can access it without parsing transcript files."

Output schema (Stop hook, new field)

{
  "continue": true,
  "stopReason": "",
  "suppressOutput": false,
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "updatedAssistantMessage": "<text that replaces what the user sees>"
  }
}

Semantics:

  • If updatedAssistantMessage is present and non-null, the harness renders this string to the terminal in place of the assistant's generated text.
  • If absent or null, current behavior (render the generated text unchanged).
  • The transcript file should record both the original generated text and the replacement, so debugging and audit remain possible.

The streaming question

The honest cost of this feature is that it requires buffering the assistant's text until Stop fires, rather than streaming it to the terminal as it generates. That breaks the default UX, which is real-time streaming.

A weaker, opt-in form that contains the cost:

  • Add a session-scoped flag, e.g., declared in plugin/skill frontmatter as buffer_assistant_message: true, or auto-detected when any registered Stop hook declares a mutates_assistant_message: true capability.
  • When the flag is set, the harness buffers the assistant text for that turn and renders it only after Stop returns (showing a spinner during generation, same as during tool use).
  • When the flag is not set, current streaming behavior is unchanged. Zero penalty for sessions that don't use the feature. This is contained, opt-in, and preserves the default streaming UX for everyone else.

Alternative Solutions

Alternatives Considered

  1. Make tool-output collapse threshold configurable (the path of #12589, #25776, #27577). This is a simpler change but doesn't solve the underlying problem. It would still require routing rendered content through the tool-output channel, which is semantically wrong (the rendered text isn't a tool result), and it would leave the cosmetic mcp__server__tool(…) line above every rendered output. Users have requested both threshold configuration and the ability to hide tool-call lines (#21394, #14256) — separately. This proposal addresses both by giving rendered content its own first-class channel: the assistant message.
  2. Allow hooks to open /dev/tty (revert v2.1.139). Would let hooks write content directly. Rejected by Anthropic for good security reasons (escape sequence injection, terminal hijacking). Not a path forward.
  3. Slash command + all-bash skill body. Works when the rendered content doesn't depend on model reasoning, but defeats the purpose of having a skill in the first place — there's no reason to involve Claude.
  4. SDK wrapper around claude -p --output-format json. The cleanest current solution for deterministic rendering, but moves the UX out of the interactive REPL into a separate CLI, which doesn't compose with the rest of a Claude Code session. Not viable for skills intended to be invoked mid-session alongside other tools.

Related Issues

  • #12589 — Configurable output collapse threshold (Nov 2025)
  • #25776 — Auto-expand tool output / default verbose mode (Feb 2026)
  • #27577 — Configurable maxCollapsedLines (Feb 2026)
  • #26954 — Bash output truncated, ctrl+o doesn't fully expand (Feb 2026)
  • #8214 — ctrl+o doesn't expand Read output (Sep 2025)
  • #21394 — Hide MCP tools from slash command preview (Dec 2025)
  • #14256 — Hide tool-call lines from transcript
  • #29991 — PostToolUse continue: false silently ignored via Agent SDK control protocol (March 2026)
  • #25543 — Display messages from startup hooks before first user turn (Feb 2026)
  • #15344 — Display SessionStart systemMessage in VS Code extension (Dec 2025)
  • #50542 — Stop hook systemMessage not rendering as visible line (April 2026)
  • #9090 — SessionEnd systemMessage not appearing in terminal (Oct 2025)
  • #42286 — UserInputRequested hook (closest existing request for a new hook event) Together these issues describe a consistent gap: hooks can observe and react to the agent loop, but they have severely limited ability to produce user-visible content. This proposal addresses the largest single piece of that gap.

Why Existing Mechanisms Don't Solve This

MechanismWhy it doesn't work
PostToolUse.updatedToolOutputOnly modifies what Claude sees from the tool. Has no effect on the user-visible assistant text.
Stop.last_assistant_messageRead-only input field. No companion output field exists.
Stop.decision: "block"Forces Claude to continue generating, which appends more text. Cannot replace the already-streamed text.
Stop.continue: false + stopReasonstopReason is shown as a system notice in addition to the streamed assistant text, not in place of it. Also has reliability issues per #29991.
systemMessage (universal hook field)Single-line notice rendering. Not a multi-line content channel. Behavior also varies across event types (per #50542, #9090, #15344).
terminalSequence (universal hook field)Window titles, bell, OSC sequences. Not for content.
Tool-output renderingCollapses to ~3 lines by default. Threshold not configurable (#12589, #25776, #27577). Even when users press Ctrl+O, expansion is buggy for outputs >30 lines (#26954, #8214).
Output stylesModify only the system prompt. Cannot transform the model's response post-generation. Per the docs: "Output styles directly affect the main agent loop and only affect the system prompt."
Hooks writing to /dev/ttyProhibited since v2.1.139 per the official hooks reference.
SubagentsInherit the same constraints. SubagentStop has the same read-only last_assistant_message.
Headless mode (claude -p)Solves the problem, but exits the interactive REPL. Not viable for skills meant to compose with the rest of an interactive session.

Priority

High - Significant impact on productivity

Feature Category

Developer tools/SDK

Use Case Example

This isn't a one-off pattern — it unlocks a class of skill-authoring patterns that are currently impossible in the interactive REPL:

  1. Deterministic templated outputs — reports, structured artifacts, status pages, build summaries, dashboards. Model gathers data and emits structured payload; harness applies a template. No model rendering, no token deviation, no copy step.
  2. Localized output — model reasons in English (where it's strongest), emits a structured intent, hook localizes to the user's language via a translation table. Currently the model has to do localization itself, with quality varying by language pair and content type.
  3. Brand/format compliance — corporate tooling that needs every output to match a strict formatting spec (specific section headers, mandatory disclaimers, audit fields). Today this is enforced via system prompt instructions that the model sometimes deviates from. With this hook, compliance is enforced harness-side.
  4. Privacy / redaction layer — hook redacts secrets, PII, or internal identifiers from the assistant's response before display, deterministically. Today this is best-effort via system prompt and post-hoc transcript scrubbing.
  5. Token-economic deterministic rendering — current pattern (tool call + final message) emits the same content twice, doubling output tokens for large structured payloads. Single-emit with harness rendering halves output cost for these patterns.

Additional Context

Implementation Notes

  • The transcript JSONL file should record both the original generated assistant text and the post-hook replacement (e.g., text and text_rendered), so --resume, debugging, and audit work correctly.
  • If multiple Stop hooks are registered and more than one returns updatedAssistantMessage, define a clear precedence (last-write-wins, or explicit ordering by registration source: settings.local.json > settings.json > plugin > skill).
  • Interaction with decision: "block" (which forces Claude to continue): if both decision: "block" and updatedAssistantMessage are returned, the block should take precedence — the replacement only renders when the turn actually ends.
  • The feature should be gated behind a session-scoped capability flag (per the "streaming question" section above) so default streaming UX is preserved for users who don't opt in.
  • Cost to the team: roughly one new output field on Stop, one buffer-vs-stream branch in the rendering pipeline, and transcript schema additions. No new event lifecycle needed if implemented as an extension to Stop.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix [FEATURE] `updatedAssistantMessage` field on Stop hook (or new `PreRender` hook) — let hooks deterministically transform the assistant's final text before terminal display