Requests vs. guarantees
"Never edit .env" in CLAUDE.md is a request: Claude will almost always follow it, but almost isn't good enough for some rules. A PreToolUse hook that blocks edits to .env is a guarantee. Hooks are commands Claude Code runs automatically at specific points in its lifecycle. Claude doesn't decide whether they run. They always do.
They also cost zero context unless they return output.
The events you'll use most
| Event | Fires when | Classic use |
|---|---|---|
SessionStart | A session starts or resumes | Load dynamic context (today's tickets, env info) |
UserPromptSubmit | You submit a prompt | Add context, or block certain prompts |
PreToolUse | Before a tool runs | Block dangerous actions |
PostToolUse | After a tool succeeds | Format, lint or run quick checks |
Notification | Claude needs your attention | Desktop or phone alerts |
Stop | Claude finishes its turn | Verify work before it can stop |
SubagentStop | A subagent finishes | Check subagent results |
PreCompact / PostCompact | Around compaction | Save or restore key context |
SessionEnd | The session ends | Cleanup and logging |
There are many more (permission requests, file changes, worktrees, config changes), but these cover most real-world needs.
The configuration shape
Hooks live in settings files under a hooks key: event → matcher groups → handlers.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "npx prettier --write --ignore-unknown \"$(jq -r '.tool_input.file_path')\"" }
]
}
]
}
}- **
matcher** filters which occurrences fire. For tool events it's the tool name:Bash,Edit|Writeormcp__github__.*. An empty matcher matches everything. - **
type** is usuallycommand(a shell command). Others includehttp(POST the event to a URL),mcp_tool,prompt(a quick model check) andagent.
Input: JSON on stdin
Your command receives event details as JSON on stdin, including session_id, cwd, hook_event_name, tool_name and tool_input. Use jq to pull out what you need:
jq -r '.tool_input.file_path' # the file being edited
jq -r '.tool_input.command' # the shell command about to runOutput: exit codes and decisions
| Exit code | Meaning |
|---|---|
0 | Success, no objection. For some events, stdout is added as context. |
2 | Block. stderr is sent to Claude as the reason, and it adapts. |
| Other | A non-blocking error. The action usually proceeds. |
For PreToolUse, exiting 0 means "no objection". The normal permission flow still applies. Hooks can deny, but a silent success doesn't grant permission.
For finer control, exit 0 and print JSON. For example, a PreToolUse decision:
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Production config is read-only" } }Where hooks can be defined
User settings, project settings, local settings, managed policy, plugins, and even skill or subagent frontmatter (active while they run). Hooks from every source merge, and all matching hooks run. Use /hooks to browse what's active.
Check your understanding
Why use a hook instead of a CLAUDE.md line for a critical rule?
Which event can block a tool before it runs?
What does exit code 2 mean for a blocking hook?
How does a hook command receive event details?
Does exiting 0 from a PreToolUse hook approve the tool call?