[FEATURE] PreResponse/PostResponse hook for assistant text output (MessageDisplay is display-only)
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet (the prior request, #37243, was auto-closed as stale on 2026-06-07 with no maintainer engagement; this re-files it with the current hook landscape baked in)
- [x] This is a single feature request (not multiple features)
Problem Statement
There is no hook that fires on the assistant's text output. Hooks
can observe and gate user input (UserPromptSubmit) and tool calls
(PreToolUse/PostToolUse), and lifecycle boundaries
(SessionStart/Stop), but the assistant's prose response — the one
channel the model produces that is NOT a tool call — has no hook
attached. This is the asymmetry: every other channel by which content
moves between the user and the model is hookable; the response text is
not.
The practical consequence is that behavioural rules about output text
("don't assert a fact without citing a source", "don't end a turn with
a prose decision-prompt when a picker is the contract", "don't emit
banned vocabulary", "don't use relative-time phrasing against an
injected absolute time") cannot be mechanically enforced. They can
only be placed in CLAUDE.md / rules / memory as text and re-injected
as reminders — which the model follows most of the time and drops under
task pressure. Tool-call rules already have a mechanical backstop (aPreToolUse hook can deny a malformed call at invocation time); the
prose channel has no equivalent.
This was previously requested in #37243 (closed stale 2026-06-07). That
thread accumulated five independent use cases before it was closed
(blocking speculative answers, enforcing word limits, linkifying
un-linked references, rewriting relative-time phrasing, chunk-level
firing). The gap is real and still unmet.
Proposed Solution
Add a PreResponse (and optionally PostResponse) hook event that
fires on the assistant's drafted response text before it is delivered.
Mirror the established PreToolUse output shape so hook contracts stay
consistent:
{
"hookSpecificOutput": {
"hookEventName": "PreResponse",
"permissionDecision": "allow" | "deny" | "modify",
"permissionDecisionReason": "<surfaced when deny>",
"modifiedResponse": "<replacement text when modify>"
}
}
allow(default) — deliver as drafted.denywith reason — block delivery; surface the reason to the user,
or return it to the model for a redraft, depending on configuration.
modifywith replacement text — substitute the drafted text before
delivery. Critically, the substitution must reach the transcript
and the model's own context, not just the on-screen render — see
the MessageDisplay note under Alternatives for why that distinction
is the whole point.
Input payload follows existing conventions
(session_id, cwd, transcript_path, permission_mode,hook_event_name) plus a response_text field carrying the drafted
prose. For long responses, a response_text_path pointing to a temp
file (and/or a configurable max-bytes truncation parameter) avoids any
payload-size concern.
A streaming variant (PreResponseChunk) firing per output chunk could
follow as a separate event if the whole-draft PreResponse lands
first; it need not block the core design.
Alternative Solutions
Each existing surface falls short:
MessageDisplay(shipped v2.1.152) — the closest existing hook,
and the reason this re-file is necessary. Its changelog entry reads
*"Added a MessageDisplay hook event that lets hooks transform or
hide assistant message text as it is displayed."* But per the current
hooks reference it is display-layer only: the exit-code-2 table
lists MessageDisplay → Can block? No → "The original text is
displayed", and the decision-control table states *"displayContent
replaces the displayed text on screen. Display-only: the transcript
and what Claude sees keep the original."* So it cannot block, and its
rewrite is cosmetic — the original text still lands in the transcript
and in the model's context. That breaks every enforcement use case:
you can mask an un-linked reference or a speculative claim on screen,
but the unenforced text persists in the record the model and
downstream tooling read.
Stop— fires after the turn ends, too late to rewrite text the
user has already read.
PostToolUse— only sees text inside tool outputs, not the final
response prose.
UserPromptSubmit— gates input, not output; the wrong direction.
Priority
Medium - Would be very helpful
Feature Category
Developer tools/SDK
Use Case Example
Mechanical enforcement of a response-behaviour rule framework.
Consider a setup that enforces output-quality rules: cite a source or
mark a claim as inference; use a structured picker for any decision
rather than a prose option-list; use a specific English spelling
variant; never end a turn with an un-actioned decision-prompt. Today,
the tool-call subset of these is enforceable — a PreToolUse hook can
inspect a structured-question tool call and deny it if its options
are malformed, at invocation time. The prose subset is not: a drafted
paragraph that asserts an uncited fact, or that ends with "should I do
X or Y?" instead of raising the picker, reaches the user with nothing
between the draft and delivery.
With PreResponse:
- The model drafts a response.
- The
PreResponsehook receivesresponse_text, scans it (regex /
classifier / LLM judge) for the banned pattern — an uncited
assertion, a prose decision-prompt, a banned term.
- On a hit, it returns
denywith a reason (bounce for redraft) or
modify with corrected text (e.g. inject the citation marker, or
rewrite the prose prompt into the structured form).
- The corrected text — not the original — is what the transcript and
the model carry forward.
This is the same enforcement guarantee tool-call hooks already provide,
extended to the one output channel that currently lacks it. Reminders
re-injected each turn are not equivalent: they degrade under load (the
slip happens in the very session the rule is loaded), and they correct
after the user has read the broken text.
Additional Context
Lifecycle symmetry. The proposed event completes the set — the
prose channel is the only model output without a hook:
| Lifecycle stage | Hook |
|---|---|
| User message arriving | UserPromptSubmit |
| Tool call about to fire | PreToolUse |
| Tool call completed | PostToolUse |
| Response drafted, not yet delivered | PreResponse (missing) |
| Turn ended | Stop |
Related closed requests. This re-files #37243 (the originalPreResponse/PostResponse request, with its five use cases). It is
also adjacent to #57519 (OnSystemReminder, also closed stale
2026-06-07): both describe gaps where a content channel reaching or
leaving the model has no observation/gate point. OnSystemReminder
covers harness-injected context into the model; PreResponse covers
prose out of it.
Compatibility. Purely additive — existing hooks and configurations
are unchanged. Adoption is opt-in: users who don't configurePreResponse see no behaviour change.
---
🤖 Generated with Claude Code