Skip to content
Mastering Claude

Home / Extending: skills, MCP, subagents, hooks, plugins

Extending: skills, MCP, subagents, hooks, plugins11 minApplication

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.

Figure 1

Timeout by hook type and event

Default timeoutHook type concernedDefault timeoutParticularity
Ordinary command, http, or mcp_tool hookcommand, http, mcp_tool600 secondsApplies to most lifecycle events
UserPromptSubmit, PreModelSwitch, PostModelSwitchcommand, http, mcp_tool30 secondsThese three events lower the timeout normally set at 600 seconds
MessageDisplaycommand, http, mcp_tool10 secondsThe shortest timeout of all documented events
SessionEndAll types combined1.5 seconds for the whole set of hooks on the eventExtendable up to 60 seconds if a hook explicitly declares a longer timeout
Hook of type promptprompt30 secondsTimeout fixed by the type, independent of the triggering event
Hook of type agentagent60 secondsTimeout fixed by the type, independent of the triggering event
The table crosses hook type with its default timeout, and isolates the events that lower this timeout or that follow a separate rule, such as SessionEnd.
Figure 2

From the event to the decision passed to the model

01
Event triggered
A tool is about to run, or has just done so, depending on the registered event.
02
Hook executed
The harness launches the command, HTTP call, MCP tool, prompt, or agent declared for this event.
03
Decision returned in JSON
hookSpecificOutput.permissionDecision carries allow or deny, accompanied by permissionDecisionReason, and hookEventName repeats the event's name.
04
Context added or action blocked
additionalContext enriches what the model sees, or the call is cancelled before happening if the event is PreToolUse.
05
Exit code 2 as a safety net
It blocks the action even if the JSON says allow, except for events that do not honour it, such as PostToolUse where it is only displayed to Claude via stderr.
The sequence places the moment when PreToolUse can still prevent the call, and the one where PostToolUse can only note what has just happened.
Calibrate it yourself

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 to remember
  • 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.
Do this now

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.

Check the source

Every datable claim in this lesson links here to the public text behind it. A source that does not open proves nothing.