Updated 09/29/2026. When I first wrote this post in May 2026, I counted eight hook events and found none that could read the agent’s reply. That is not true as of today. As of 09/29/2026 the hooks reference lists 33 events. The Stop hook now receives the final reply as last_assistant_message, and a MessageDisplay event sees reply text as it streams to your screen. The gap is smaller. It has not closed, and the part that is left is the part that matters.

Here is the short version. A hook can now read what the agent said and change what you see. No hook can stop the agent from having said it: the original text stays in the transcript and in the model’s context.

I found it while building the hook layer into a production workflow that runs against a 38,240-line Python codebase. You set up the hooks you know about. The harness feels solid. Then the agent does something you did not expect at a lifecycle point you had no gate on. The question you ask afterward is: was there a hook for that? For tool calls and turn boundaries, usually yes. For the agent’s own output text, when I first wrote this, I found none.

What the hook system actually covers

Claude Code’s hook system fires shell commands at specific points in the agent lifecycle. The events most builders work with are UserPromptSubmit, PreToolUse, PostToolUse, and Stop. The full set as of 09/29/2026 is 33 events; the hooks tutorial has the whole table. The ones that matter for this question fall into four groups.

Session-level (fire around session lifecycle): SessionStart, SessionEnd, PreCompact. SessionStart is where you load context, inject environment state, or run a pre-flight check before the model does anything. PreCompact fires before context compaction, your last chance to persist state that would otherwise be summarized away. Most operators skip everything except SessionStart.

Turn-level (fire per turn): UserPromptSubmit fires before the model sees the prompt and can BLOCK, intercepting, transforming, or rejecting the turn entirely. Stop fires after the model has emitted its text. The docs now give Stop a last_assistant_message field, so a Stop hook can read that final reply without parsing the transcript. It can BLOCK stopping and hand the agent a reason to keep working. It cannot edit or retract the reply that was already emitted. SubagentStop is the same boundary for subagent completions, with the subagent’s final text in the same field.

Display-level: MessageDisplay runs while reply text streams to the screen. It receives each batch of completed lines and can return displayContent to replace them, for example to redact an API key. The docs are explicit about its limit: it is display-only. The transcript and what Claude sees keep the original text, and the hook has no decision control, so it cannot block the message.

Tool-level: PreToolUse can BLOCK the tool call, validate the input, or log it before it fires. PostToolUse sees the tool result before the agent processes it.

The enforcement model has one asymmetry that matters operationally. UserPromptSubmit can stop a turn before it starts. PreToolUse can stop a tool call before it fires. Stop can refuse to stop and force the agent to retry. MessageDisplay can change what you see. None of them can reject the reply itself, so a wrong claim still lands in the conversation the model keeps reasoning from.

The gap that is left: no hook can reject a reply

This is an architectural constraint, not a configuration gap.

When I first wrote this, I found no hook between the reply and you. Today two hooks can touch it: MessageDisplay on the screen, and Stop after the turn. There is still no hook that can refuse a reply the way PreToolUse refuses a tool call. The text is generated, it enters the transcript, and the model builds on it. A hook can hide it from you or send the agent back to work. It cannot make the reply not exist.

The failure mode this enables is the “looks fine” bug class. The agent generates a response. The response is internally coherent. No blocked tool was called. No hook fired. But the output contains a claim, a formatting error, or a factual error that none of the gates were positioned to catch.

I hit this in early May 2026. The agent claimed the UI was “formatted properly” across multiple turns. No hook fired because, at the time, none of the hooks could inspect response text. The markdown table was rendering as a wall of pipe-delimited prose. The agent was not lying in any meaningful sense. It was generating plausible text about a state it could not verify through any of its tool outputs.

In May the fix was not a hook. It was a verification architecture: a PostToolUse hook that runs a separate check against tool outputs, combined with a Stop hook that forces a retry if verification conditions are unmet. You verified the preconditions because you could not verify the output.

With last_assistant_message, the Stop hook can now check the output too. A Stop hook can look for a claim like “formatted properly” in the final reply, check whether a render check actually ran this turn, and block with a reason if it did not. That catches the claim after you have already read it, which is better than never, and worse than a gate that stops it first.

How this compares to other harnesses

The output gating gap is not unique to Claude Code. LangGraph has no native output hook. CrewAI has output guardrails, but at the crew level, not the turn level. The framework with the most comparable coverage is the Pi harness, which defines hooks at tool_call, tool_result, session_*, input, before_provider_request, and before_compact, putting its breadth in the same range as Claude Code’s documented set.

The one surface Claude Code still does not have a native equivalent for: OpenAI Agents has OutputGuardrail, which blocks final responses before they reach the operator. The closest Claude Code gets, as of 09/29/2026, is two hooks working together: MessageDisplay to redact what you see, and Stop to read the reply and force a retry. Neither one rejects the reply itself.

The comparison matters not as a ranking but as a map: where is your enforcement layer actually positioned, and what does the agent do in the gaps between the positions you can defend?

How this changes how I write hooks

The practical implication still holds: anything I need to prevent on the output side has to be enforced on the input side first, or on the tool-call boundary. Stop and MessageDisplay are for detecting and hiding, not for preventing.

If I want the agent to only cite verified statistics, the enforcement point is PreToolUse on the tool that would generate the claim, not a post-hoc output scan. If I want the agent to avoid a specific framing, the enforcement point is UserPromptSubmit injecting a constraint before the prompt reaches the model.

This is the design tension the official docs do not make explicit: hooks are a powerful enforcement layer with a precise shape. That shape is: turn entry, tool boundary, display, turn exit. Output text lives inside the turn, after the model fires. You can now watch that space and redact what it shows. You still cannot gate it.

Building a harness that takes this seriously means treating output verification as a separate architectural problem from hook configuration. The events are load-bearing. The gap between what they cover and what you might assume they cover is where production failures concentrate.

What PostToolUse is actually for

PostToolUse is underused relative to PreToolUse. Most builders use PreToolUse for access control, blocking specific tools from being called. PostToolUse is where you verify results before the model acts on them.

The agent reads a file. PostToolUse fires. A script checks the file content against expected schema. If it fails, the hook returns an error that the agent sees as the tool result. The agent then has correct information about the tool output state before generating its response.

That pattern closes part of the output gating gap indirectly. The agent cannot make a false claim about a tool result it has already been corrected on. It does not close the gap completely.

The part that remains is sycophancy and internal-consistency errors: cases where the model generates plausible text about something it cannot verify through tool outputs at all. That is a model behavior problem sitting above the hook layer. Hooks are not the solution to that class of failure.

The harness is the guardrail. The hooks are how you build the harness. Knowing which events can BLOCK, which can only read or redact, and where the output text boundary sits is the foundation.

For a step-by-step guide to wiring these hooks in practice, including a secret-leak scanner and the full current event list, see the Claude Code hooks tutorial. For when a rule belongs in a hook rather than a skill or a subagent, see Claude Code skills vs subagents.