The tool has already run
If you need to stop a call before it happens, you want PreToolUse. This event is the one that fires after the fact. PostToolUse runs after each successful tool call, including calls that ran at the same time inside a batch. Whatever the tool did is done.
What the payload adds after the fact
The matcher filters tool names and the if filter applies, the same as on PreToolUse. The payload carries permission_mode, tool_name, tool_input, a structured tool_response, tool_use_id, and an optional duration_ms. File paths in the input are absolute and use the native separator for the platform, so on Windows they carry backslashes. The hooks reference documents the Agent and ExitPlanMode response shapes in its shared tool schema.
Change what Claude reads, not what happened
A top-level decision: "block" with reason adds feedback beside the result and leaves the original in place. Under hookSpecificOutput, the reference lists additionalContext, classifierContext from Claude Code v2.1.236, updatedToolOutput, and the older MCP-only updatedMCPToolOutput. A replacement through updatedToolOutput has to match the shape the built-in tool’s own output would have.
Exit 2 is a note, not a wall
Nothing this hook returns can undo the tool. The file is written, the command has run. What the exit code changes is what Claude reads afterwards. Exit 0 with valid JSON applies that JSON, and exit 0 with nothing passes the result through untouched. Exit 1 passes the result through as well, and the transcript notes that the hook failed. Exit 2 adds your stderr next to the result, as a note, and still reads any valid JSON on top. That is the whole difference from PreToolUse, where exit 2 is a wall.
You wire a hook that scans every Write for a hard-coded password and exits 2 when it finds one. You test it. The hook fires, your message reaches Claude, and the file is on disk with the password in it.
Claude may fix it on the next turn, or may not. Nothing prevented the write, because the event fires after the write. The message arrived as a note beside a result that already existed.
Move the check to PreToolUse and deny there. Keep a PostToolUse copy only if what you want is the record. The lab’s audit_log is that pattern, and module 09 wires it with async because it has nothing to decide.
Module 02 first names this event, as the moment that comes too late to stop anything.