[FEATURE] PreResponse hook: let a hook send feedback so the model revises its reply before it is displayed

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

Background

We want to "loop engineer" replies: run a check on the assistant's final message and, if it fails, have the model fix it before the user reads it.

Real example: the model told the user a migration "would use sponge if sponge is installed". command -v sponge would have settled that in 5 ms, and sponge was installed. A regex over the final message catches this wording, but today the only place the check can run is after the user has already read the hedge.

What exists in 2.1.280, read from the installed binary:

  • Stop gets last_assistant_message, and decision:"block" feeds text back to the model. But the reply has already streamed to the terminal, so the fix appears as a second message under the wrong one. This is the behaviour reported in #95465 and #95461, which are related bugs, not this feature.
  • MessageDisplay is "Display-only: the stored message and what the model sees are untouched." It can hide text, but the model never revises, and the transcript no longer matches the screen.
  • There is no PreResponse, PreSend or BeforeResponse event among the 33 hook events.

Goals

  • A hook can inspect the final assistant message of a turn before it is rendered.
  • The hook can return feedback that makes the model continue the turn, including calling tools, e.g. running command -v sponge, and produce a revised reply.
  • The user sees only the reply that passed.
  • The same event covers subagent final reports before the parent receives them.

Non-goals

  • Not rewriting text mechanically: that's MessageDisplay's job.
  • No new latency for sessions that don't register this hook.
  • No unbounded loops: revisions must be capped.
  • Not a replacement for Stop, which stays for end-of-turn side effects.

Solution

Add a PreResponse event. It fires when the model has produced its final message for the turn, with no tool calls pending, and before that message is displayed.

  • Input: the same payload as Stop, plus revision_count.
  • Output:
  • Exit 0 or no decision: display the message as usual.
  • {"decision":"revise","reason":"…"}: hold the draft, feed reason back to the model, let the turn continue, and run the hook again on the next final message.
  • Guard: a per-turn revision cap (e.g. 3). After the cap, show the last draft with a one-line note.
  • While held: show a "checking reply…" status. Hooks opt in to holding the display ("holdDisplay": true).

Why this over the alternatives. Stop fixes the reply only after the user has read it, so it adds a second message. MessageDisplay hides text without the model learning anything. Buffering in MessageDisplay while Stop blocks has no turn boundary, and the corrected reply still arrives as a second message. Only a pre-display event gives "check → revise → show once".

Uses: verifying claims ("if X is installed", "the commit should have landed"), house style and length limits, and redacting secrets before display.

Claude Code 2.1.280, Linux.

View original on GitHub ↗

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