Only the calls that would have asked
You already know PreToolUse sees every tool call. PermissionRequest is narrower: it sees the subset Claude Code was about to ask you about, or would auto-deny in a session with nobody there to ask. Sandbox network prompts do not fire it. The matcher filters tool names and the if filter applies, both as on PreToolUse.
The payload carries suggestions
Before you answer, look at what Claude Code was going to offer. Alongside permission_mode, tool_name, and the tool’s own tool_input, the payload may carry permission_suggestions: the always-allow rules Claude Code would have offered you at the prompt. Each entry can hold type, rules, behavior, destination, or whatever its update type requires. There is no tool_use_id on this event.
The decision is a JSON object
Your hook speaks through JSON, not through exit codes. The answer lives under hookSpecificOutput.decision, with behavior set to allow or deny. An allow can carry updatedInput and updatedPermissions. A deny can carry message and interrupt. Permission updates support addRules, replaceRules, removeRules, setMode, addDirectories, and removeDirectories, each with the destination the hooks reference documents for it. So a hook here can answer the prompt, change the arguments, and rewrite the rules for next time, in one object.
The exit code has no vote
On the other blocking events, exit 2 is the strong answer. Here it is nothing. Exit 2 is not honoured, its stderr is discarded, and the prompt appears as if the hook had not run. Exit 0 with no JSON leaves the prompt in place too. Exit 1 without JSON is the same prompt, plus a hook error in the transcript.
What changes the outcome is a valid JSON decision object on stdout, and the reference says that is read whatever the exit code was. PreToolUse blocks with exit 2 first and JSON second. PermissionRequest reads JSON only.
You copy block_destructive.py from module 09 to this event, since it already refuses the right commands. It exits 2 with a stderr sentence.
The permission prompt appears anyway. Your sentence appears nowhere. If you answer yes at the prompt, the command runs. Exit 2 is ignored on this event and stderr is discarded with it, so the copy did nothing and reported nothing.
Rewrite the refusal as a hookSpecificOutput.decision object with behavior: "deny" and your sentence in message, and exit 0. On this event the JSON is the only thing read.
Module 11 covers the rules this event can rewrite through updatedPermissions.