Blocking Stop hook reprints the entire corrected answer on top of the original (no pre-emit event / no way to retract streamed output)

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Aug 2, 2026

What happened

A blocking Stop hook that returns {"decision":"block","reason":"..."} fires after the assistant's reply has already fully streamed to the terminal. The block then makes the model regenerate a corrected reply, which streams as a second message. The user therefore sees the entire answer twice: the original (rejected) draft, then the corrected rewrite on top of it. For long answers this is a lot of duplicated scrollback on every block.

There is no way to fix this from the hook side, because:

  • The earliest hook event that can inspect/reject the assistant's reply is Stop, which is post-display. (Confirmed against the 2.1.220 bundle — the hook event set has no pre-response/pre-emit event.)
  • A Stop block result carries no field to retract, clear, or replace the already-streamed message. suppressOutput only governs the hook's own success stdout, not the assistant message.

Repro

  1. Register a Stop hook that inspects the last assistant message and returns {"decision":"block","reason":"rewrite it this way..."} when it dislikes something (e.g. a lint/style rule).
  2. Ask a question that produces a long answer containing the disliked pattern.
  3. Observe: the full answer streams, the hook blocks, and the full corrected answer streams again below it. Both remain in scrollback.

Feature request

Give hook authors a way to correct a reply without the user seeing the rejected draft. Either of these would close it:

  1. A pre-emit / PreResponse hook event that fires after the model finishes generating but before the text is shown, supporting decision: "block". The guard inspects the drafted reply, rejects it, and triggers a regenerate — so only the corrected answer is ever displayed. (Existing Stop guard bodies would move to this event unchanged.)
  2. OR a field on the Stop block result — e.g. replaceOutput: true / retractDraft: true — instructing the harness to clear the just-streamed reply and show only the regenerated one.

Related, smaller display bug

Separately, a single decision:block from a Stop/PostToolUse hook renders its reason twice in the terminal — once as ... hook feedback: and once as ... hook blocking error from command: "<cmd>": <reason>. In the 2.1.220 bundle the one blockingError object is rendered by both ppr(blockingError) (the "blocking error from command" line) and via iud(...) (the "feedback" line). De-duplicating that render for an intentional decision:block would remove the second, redundant surface. (Matches earlier reports for PostToolUse.)

Environment

  • Claude Code 2.1.220, macOS (arm64)

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗