Hooks let Claude Code run shell commands at specific moments: before a tool executes, after it completes, or when Claude finishes responding. They live in ~/.claude/settings.json.
1. Structure
{
"hooks": {
"PostToolUse": [...],
"PreToolUse": [...]
}
} Each entry has a matcher (a regex against the tool name) and an array of hooks to run.
2. The one I use: auto-format after every edit
This is the most useful hook I have. Whenever Claude edits or writes a file, Prettier runs automatically:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | tr -d '\\r' | xargs -r bunx prettier --write --ignore-unknown"
}
]
}
]
}
} How it works:
matcher: "Edit|Write"triggers after anyEditorWritetool calljq -r '.tool_input.file_path // empty'extracts the file path from the JSON Claude Code pipes into stdintr -d '\r'is needed on Windows becausejqunder MINGW64 outputs CRLF, and the stray\rgets appended to the path, causing Prettier to fail with “file not found”xargs -rskips if the path is empty, then passes it to Prettier
Claude Code pipes a JSON object into each hook’s stdin containing tool_name, tool_input, and tool_response. Use jq to extract what you need.
3. Hook types
| Event | When it fires |
|---|---|
PreToolUse | Before a tool executes, can block it |
PostToolUse | After a tool completes |
Stop | When Claude finishes a response |
SubagentStop | When a subagent finishes |
4. PreToolUse: blocking dangerous commands
PreToolUse can prevent a tool from running entirely. Here is an example that blocks rm -rf:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -e '.tool_input.command | test(\"rm -rf\")' && echo '{\"action\":\"block\",\"reason\":\"rm -rf not allowed\"}' || true"
}
]
}
]
}
} Outputting JSON with action: "block" stops the tool before it runs.
5. A few things worth knowing
matcheris a regex, so"Edit|Write|MultiEdit"matches all three- Multiple hooks in an array run sequentially
PostToolUsehooks are fire-and-forget, their exit code does not affect Claude- Always use
jqto parse stdin, never assume the format