Claude Code hooks are shell commands that fire automatically at defined points in Claude’s workflow. Think of them as git hooks, but for your AI coding agent: they let you enforce policies, run formatters, and wire up integrations without any manual steps.
The Four Hook Events
Claude Code exposes four lifecycle points where you can attach scripts:
| Event | When it fires |
|---|---|
PreToolUse | Before Claude calls any tool (Read, Edit, Bash, etc.) |
PostToolUse | After a tool call completes, before Claude’s next action |
Notification | When Claude sends a notification (e.g., task complete) |
Stop | When Claude finishes a full turn |
The most useful pair in practice is PreToolUse (to block bad writes) and PostToolUse (to run formatters or linters after edits).
Configuration Structure
Hooks live in the hooks key of your settings file. You can scope them globally (~/.claude/settings.json) or per-project (.claude/settings.json in the repo root).
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_FILE_PATHS 2>/dev/null || true"
}
]
}
]
}
}
The matcher field
matcher is a regex matched against the tool name. Common values:
"Edit|Write"— fires after any file write"Bash"— fires after every shell command".*"— fires after everything (use carefully)
The command field
Any shell command. Claude Code injects environment variables you can use:
| Variable | Contents |
|---|---|
$CLAUDE_FILE_PATHS | Space-separated list of files affected by the current tool call |
$CLAUDE_TOOL_NAME | The name of the tool that was called |
$CLAUDE_TOOL_INPUT | JSON-encoded input to the tool |
$CLAUDE_TOOL_OUTPUT | JSON-encoded output from the tool (PostToolUse only) |
Practical Examples
Auto-format on every save
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write $CLAUDE_FILE_PATHS 2>/dev/null || true"
}
]
}
]
}
}
Block writes outside the src/ directory
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "echo \"$CLAUDE_FILE_PATHS\" | grep -v '^src/' && exit 1 || exit 0"
}
]
}
]
}
}
A non-zero exit code from a PreToolUse hook cancels the tool call and shows the hook’s stderr output to Claude as a reason. Claude will typically acknowledge the block and try a different approach.
Run ESLint after TypeScript edits
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "echo \"$CLAUDE_FILE_PATHS\" | grep -qE '\\.(ts|tsx)$' && npx eslint $CLAUDE_FILE_PATHS --fix 2>&1 || true"
}
]
}
]
}
}
Log every Bash command Claude runs
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) BASH: $CLAUDE_TOOL_INPUT\" >> ~/.claude/bash-audit.log"
}
]
}
]
}
}
Auto-stage files after edit
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "git add $CLAUDE_FILE_PATHS 2>/dev/null || true"
}
]
}
]
}
}
Blocking vs. Non-blocking Hooks
- PreToolUse: exit non-zero → tool call is cancelled, stderr is fed back to Claude
- PostToolUse / Notification / Stop: exit code is ignored; the hook runs for side effects only
This asymmetry is intentional. Pre-hooks are for enforcement; post-hooks are for automation.
Debugging Hooks
Set CLAUDE_DEBUG=1 before running claude to see hook execution in the console output:
CLAUDE_DEBUG=1 claude
You can also write a simple hook that logs to a file and tail it in a separate terminal:
{
"type": "command",
"command": "echo \"Tool=$CLAUDE_TOOL_NAME Files=$CLAUDE_FILE_PATHS\" >> /tmp/claude-hook.log"
}
Key Constraints
- No interactive commands — hooks must run non-interactively. A hook that prompts for input will hang indefinitely.
- Timeout — by default hooks time out after 60 seconds. Long-running hooks will be killed.
- Working directory — hooks run in the project root (where you launched
claude). - No return values — hooks communicate back to Claude only via exit code (PreToolUse) and stderr (PreToolUse error messages). Stdout is discarded.
Hooks are one of the most underused Claude Code features. A project-level .claude/settings.json with a formatter hook and a lint check is the single fastest way to keep Claude’s output clean without adding any review burden.