Where a hook sits in the tool loop
Bash(rm -rf …)PreToolUse with tool_inputPreToolUse runs between the model asking and the tool running — the only point at which an action can still be stopped.What a hook actually is
A hook is a handler you configure to run at a named point in the agent's lifecycle. It receives JSON describing what is about to happen — the session id, the working directory, the event name, and for tool events the tool_name and the tool_input — and it answers by exit code and by JSON on standard output. Handlers can be a shell command, an HTTP endpoint, a tool on an MCP server, or a prompt or subagent evaluated by a model.
The crucial property is that a hook is ordinary code. It runs whether or not the model is having a good day, its decision does not depend on how the request was phrased, and you can unit-test it. That is what makes it the right instrument for preventing destructive actions, and it is the comparison every exam item about hooks is drawing.
| Control | Runs | Decides by | Good for |
|---|---|---|---|
An instruction in the prompt or CLAUDE.md | When the model attends to it | Model judgement | Style, conventions, preferences |
| A permission allow/deny rule | Every matching tool call | Configured patterns | Broad policy on named tools and commands |
| A hook | Every matching event | Your code, on the actual input | Context-dependent safety checks, audit, blocking |
| Sandboxing | Every command | Filesystem and network isolation | Enforcement that does not depend on command text |
Blocking a destructive action
PreToolUse is the event that can stop something. A hook blocks by exiting with status 2, and it should also return JSON naming the decision and the reason so the model learns why and can try something else. Exit 0 means the hook has no objection and normal permission handling continues; any other exit status is a non-blocking error and the action proceeds.
#!/bin/bash
# .claude/hooks/block-destructive.sh — receives the event JSON on stdin.
COMMAND=$(jq -r '.tool_input.command')
case "$COMMAND" in
*"rm -rf"*|*"DROP TABLE"*|*"git push --force"*)
# Tell the model why, then block. Exit 2 is the blocking signal.
jq -n '{hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason:
"Destructive command blocked by policy hook."}}'
exit 2 ;;
esac
exit 0 # No objection — normal permission flow applies.Hooks are configured in settings, with a matcher selecting which events reach them. A matcher of Bash fires only for the Bash tool, Edit|Write for either of two tools, and anything containing regular-expression characters is treated as a pattern — mcp__.*__.* reaches every tool provided by an MCP server. All matching hooks run in parallel.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-destructive.sh",
"timeout": 10,
"statusMessage": "Checking command safety..."
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/audit.sh" }
]
}
]
}
}| Exit status | Meaning | Effect on the action |
|---|---|---|
0 | Success; JSON on stdout is read | Proceeds under normal permission handling |
2 | Blocking error | Blocked on events that support blocking, with the reason passed back |
| Anything else | Non-blocking error | The action proceeds — the hook failed open |
| Timed out | Cancelled, output discarded | On PreToolUse it does not block |
The other events, and what they are for
Blocking is the headline, but the lifecycle has points for the rest of a safety story too. The documented set is large; a handful carry most of the safety work.
| Event | Fires | Typical safety use |
|---|---|---|
PreToolUse | Before a tool runs | Block destructive actions; require a condition to hold |
PostToolUse | After a tool succeeds | Audit log, secret scanning, running a formatter or test |
UserPromptSubmit | Before Claude processes a prompt | Screen input; add context; block a disallowed request |
SessionStart | When a session starts or resumes | Load policy or environment; record who started it |
ConfigChange | When configuration changes | Audit or block changes to settings during a session |
Stop | When Claude finishes responding | Check the turn's outcome before it is presented |
Where the hook is configured decides who can change it. Project settings are shared through version control; a local settings file is personal and not committed; managed policy settings are administered centrally and cannot be overridden by a user. A safety hook that matters to the organisation belongs in managed settings — otherwise the control is only as strong as the most impatient engineer's local configuration.
Choosing the right control
- A preference or conventionInstructionsguidance the model follows
- A named tool or commandPermission ruleallow, ask or deny
- Needs your logic or data
PreToolUsehookinspect, decide, exit 2 - Must hold whatever the text saysSandboxfilesystem and network isolation
That last branch matters. The security documentation notes that a deny rule matches a command as written, so text-based matching can be evaded by an equivalent command spelled differently; for enforcement that does not depend on the wording, network and filesystem isolation is the stronger instrument. A hook reading tool_input has the same exposure, so treat pattern matching as a filter over an already-restricted environment, not as the wall itself.
Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
Relying on CLAUDE.md or the system prompt to forbid destructive commands | Enforce it with a PreToolUse hook, a deny rule or a sandbox. |
| Returning a non-zero exit code other than 2 to block an action | Exit 2; every other non-zero status is a non-blocking error and the action proceeds. |
| Letting a safety hook crash or time out unhandled | Fail closed: deny when the check cannot complete, and test that path. |
| Blocking silently with no reason | Return permissionDecisionReason so the model can adapt rather than retry. |
| Keeping an organisation-wide safety hook in a personal settings file | Ship it as managed policy settings that users cannot override. |
| Treating string matching on commands as complete protection | Pair it with sandboxing, which does not depend on how the command is spelled. |
You should now be able to
- Explain what a hook is, what input it receives and how it returns a decision.
- Write a
PreToolUsehook that blocks a destructive action and explains why. - Choose the right lifecycle event for a given safety or audit requirement.
- Configure matchers and pick the settings scope that makes a control enforceable.
- Decide whether a hook should fail open or closed, and justify it.