Home / Extending: skills, MCP, subagents, hooks, plugins
Hook events, from trigger to message
PreToolUse can prevent a call from happening, PostToolUse can only react after the fact, and a hook's decision travels through precise JSON fields, never a sentence written to standard output.
A PreToolUse hook runs before the tool activates: its decision can prevent the call from happening. A PostToolUse hook runs afterwards, once the tool has already launched: its decision can no longer prevent anything, only react. This difference in timing determines what a hook can reasonably do at each event, and it explains why the same blocking mechanism does not behave the same way depending on the event that carries it.
About thirty events, from startup to end of session
The official documentation lists around thirty lifecycle events, from SessionStart and UserPromptSubmit through to PreCompact, WorktreeCreate, or SessionEnd. Each covers a precise moment: a file changing, a model switch, a subagent starting or ending, a prompt being expanded. An intended automatism, presented in the previous lesson as the mechanical translation of a rule one hoped would be followed, is always declared on one of these precise events, never on a general intention.
The timeout depends on the hook type, not just the event
A hook of type command, http, or mcp_tool has six hundred seconds by default to respond. Three events lower this timeout to thirty seconds, UserPromptSubmit, PreModelSwitch, and PostModelSwitch, and just one reduces it to ten seconds, MessageDisplay. A hook of type prompt keeps a fixed timeout of thirty seconds, a hook of type agent a fixed timeout of sixty seconds, whatever event triggers it. SessionEnd works differently: the whole set of its hooks shares a budget of one and a half seconds, extendable up to sixty seconds only if a hook explicitly declares a longer timeout.
What actually reaches the model: JSON fields, not free text
A hook does not communicate its decision by writing a sentence to standard output. It returns a JSON object whose hookSpecificOutput.permissionDecision field carries the value allow or deny, accompanied by a permissionDecisionReason that explains the choice, and the hookEventName field repeats the exact name of the triggering event. To add information without blocking anything, hookSpecificOutput.additionalContext inserts text that the model will read as though it came from elsewhere in the conversation.
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Commande destructive detectee dans le motif teste"
}
}
The script's exit code also matters, independently of the JSON: an exit code of 2 blocks the action even if the JSON body said allow. This rule has named exceptions, PermissionRequest, StopFailure outside a terminal sequence, PermissionDenied, and above all PostToolUse and PostToolUseFailure, where exit code 2 becomes non blocking and merely gets displayed to Claude via standard error: logical, since the tool has already finished running by the time this hook fires.
Deferring rather than deciding
The permissionDecision field documents two values, allow and deny. To hand the decision back to the usual permission flow rather than forcing it, a hook writes no third value into this field: it exits with code 0 without reporting a decision, and the call continues along its normal approval path, as though this particular hook had had nothing to say about it.
Timeout by hook type and event
| Default timeout | Hook type concerned | Default timeout | Particularity |
|---|---|---|---|
| Ordinary command, http, or mcp_tool hook | command, http, mcp_tool | 600 seconds | Applies to most lifecycle events |
| UserPromptSubmit, PreModelSwitch, PostModelSwitch | command, http, mcp_tool | 30 seconds | These three events lower the timeout normally set at 600 seconds |
| MessageDisplay | command, http, mcp_tool | 10 seconds | The shortest timeout of all documented events |
| SessionEnd | All types combined | 1.5 seconds for the whole set of hooks on the event | Extendable up to 60 seconds if a hook explicitly declares a longer timeout |
| Hook of type prompt | prompt | 30 seconds | Timeout fixed by the type, independent of the triggering event |
| Hook of type agent | agent | 60 seconds | Timeout fixed by the type, independent of the triggering event |
From the event to the decision passed to the model
A developer configures a PostToolUse hook that returns an exit code of 2 after every call to the Bash tool. He then runs a Bash command from Claude Code and observes that the command runs to completion.
Write in one sentence what this situation establishes, and in one sentence what it does not establish.
What this establishes: This observation establishes that, for the PostToolUse event, an exit code of 2 does not block the action and is only treated as a message displayed to Claude via standard error.
What this does not establish: It does not establish that exit code 2 behaves the same way across every hook event, since PreToolUse honours this same code by blocking the call before it happens.
The three most common miscalibrations
- Too broad This observation proves that exit code 2 never blocks anything, whatever the hook event concerned.
- Too narrow This observation proves nothing about the hook's behaviour, since only one call to the Bash tool was tested.
- Beside the point This observation shows that the hook took longer to run than the Bash command itself.
- PreToolUse can prevent a tool call from happening, PostToolUse can no longer do anything but react once the tool has already launched.
- The default timeout of six hundred seconds for a command, http, or mcp_tool hook drops to thirty seconds on three events and to ten seconds on just one.
- A hook's decision travels through the hookSpecificOutput.permissionDecision field, with the confirmed values allow and deny, never through a sentence written to standard output.
- An exit code of 2 blocks the action even when the JSON asserts allow, except on a named list of events where it becomes a simple message displayed to Claude.
- To defer to the usual permission flow rather than forcing allow or deny, a hook exits with code 0 without reporting a decision, there is no third value written into permissionDecision.
Write a minimal PreToolUse hook that exits with code 0 without writing anything to its standard output, declare it on an innocuous event, and confirm that the call continues along its normal approval path rather than being forced.
Every datable claim in this lesson links here to the public text behind it. A source that does not open proves nothing.
- Claude Code, hooks, permissionDecision values and exit code 2 by event consultée le 2026-09-02