Working Code
Let's add a Hook to .claude/settings.json. This Hook blocks file modifications on the main branch:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "[ \"$(git branch --show-current)\" != \"main\" ] || { echo '{\"block\":true,\"message\":\"Cannot modify files on the main branch. Create a feature branch first.\"}' >&2; exit 2; }",
"timeout": 5
}
]
}
]
}
}
Now ask Claude to modify a file on the main branch:
> Add a line to README.md
The Hook triggers and blocks the modification. Switch to a different branch and it works normally:
> Create a branch called feature/test with git checkout -b, then modify README.md
Try It Yourself
Now let's use a PostToolUse Hook to auto-lint after file saves:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx eslint --fix $CLAUDE_FILE_PATHS 2>/dev/null || true",
"timeout": 10
}
]
}
]
}
}
Every time Claude modifies a file, ESLint automatically runs and fixes the style.
"Why?" — Enforcing Rules with Code
Even if you write "don't work on the main branch" in CLAUDE.md, Claude might ignore it. Hooks enforce rules with code. It's not a request — it's a block.
Key Hook Events
| Event | When | Purpose |
| ------------------ | -------------------------- | ------------------------------------------------ |
| PreToolUse | Before tool execution | Block dangerous operations, conditional approval |
| PostToolUse | After tool execution | Auto-formatting, linting, logging |
| Notification | When a notification fires | Custom notifications (Slack, desktop, etc.) |
| Stop | When response completes | Result validation, cleanup |
| UserPromptSubmit | When user submits input | Input preprocessing |
| SubagentStop | When sub-agent completes | Sub-agent result post-processing |
| PreCompact | Before context compression | Preserve data before compression |
| SessionStart | When session starts | Environment validation, dependency checks |
| SessionEnd | When session ends | Cleanup, logging |
The most commonly used are PreToolUse, PostToolUse, and Notification.
Hook Configuration Structure
{
"hooks": {
"EventName": [
{
"matcher": "tool name pattern (regex)",
"hooks": [
{
"type": "command",
"command": "shell command to execute",
"timeout": 5
}
]
}
]
}
}
Matcher Patterns
The matcher filters which tools the Hook applies to:
| Pattern | Matches |
| ------------- | ---------------------------- |
| Edit\|Write | File edit/create tools |
| Bash | Shell command execution |
| Read | File reading |
| .* | All tools (use with caution) |
Exit Codes Determine Behavior
The Hook's exit code determines what happens:
| Exit Code | Behavior |
| --------- | ---------------------------------- |
| 0 | Success — continue the operation |
| 2 | Block — prevent tool execution |
| Other | Treated as error |
Returning exit 2 from PreToolUse blocks the operation:
exit 2
Send a message via stderr to tell Claude why it was blocked:
echo "Cannot modify files on the main branch." >&2
exit 2
Deep Dive
What environment variables are available in Hooks?
You can use these environment variables inside Hook commands:
$CLAUDE_FILE_PATHS— file paths related to the current tool call (space-separated)$CLAUDE_PROJECT_DIR— absolute path to the project root directory
For more detailed information, parse the JSON passed via stdin with jq:
jq -r '.tool_input.command' >> ~/.claude/bash-log.txt
- Add a main branch protection Hook to
.claude/settings.jsonand test it. - Create a PostToolUse Hook that auto-runs
prettier --writeafter file modifications. - Create a SessionStart Hook that prints "Current branch: {branch name}" when a session starts.
Q1. Which Hook event runs before Claude Code modifies a file?
- A) PostToolUse
- B) SessionStart
- C) PreToolUse
- D) Stop