![]()
You can tell Claude Code to run Prettier after every file edit. You can write that in your CLAUDE.md, add it to your prompt, even repeat it three times. Claude will probably do it. Probably. Most of the time.
Hooks make it guaranteed.
Hooks are shell commands that execute at specific points in Claude Code's lifecycle — before a tool runs, after a tool completes, when a session starts, when Claude sends a notification. They're deterministic. They don't depend on the model remembering your instructions. They just run.
This guide covers everything from your first hook to production-ready configurations for auto-formatting, security gates, notifications, and context injection.
Every hook has three parts:
PreToolUse, PostToolUse, Notification)Write tool, only for .ts files)When an event fires and the matcher matches, Claude Code passes JSON context about the event to your command's stdin. Your command does its work and communicates the result back through its exit code:
PreToolUse hooks, this prevents the tool from executing)Any other exit code logs a warning but doesn't block execution.
Hooks live in your settings JSON files. You have three options depending on scope:
| Scope | File | Shared via Git? |
|---|---|---|
| Global (all projects) | ~/.claude/settings.json | No |
| Project (team-wide) | .claude/settings.json | Yes |
| Project (personal) | .claude/settings.local.json | No (gitignored) |
The structure inside any of these files looks like this:
{
"hooks": {
"EventName": [
{
"matcher": "regex pattern",
"command": "your-command-here"
}
]
}
}
You can also ask Claude itself to add hooks for you — just describe what you want and Claude will edit the settings file.
Claude Code supports a wide range of lifecycle events. Here are the ones you'll use most often:
| Event | When It Fires | Can Block? |
|---|---|---|
PreToolUse | Before a tool call executes | Yes (exit 2) |
PostToolUse | After a tool call completes successfully | No |
Notification | When Claude sends a notification (e.g., waiting for input) | No |
SessionStart | When a new session begins or resumes | No |
Stop | When Claude finishes its response | No |
SubagentStart | When a subagent is spawned | No |
SubagentStop | When a subagent finishes | No |
PreCompact | Before context compaction happens | No |
The PreToolUse event is by far the most powerful — it's the only one that can prevent an action from happening.
The most common hook auto-formats files after Claude edits them. No more reminding Claude to run Prettier — it happens every time, automatically.
Add this to your .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
}
]
}
}
This hook fires after every Edit or Write tool call and runs Prettier on the affected file. The $TOOL_INPUT_FILE_PATH environment variable is automatically set by Claude Code to the path of the file that was just modified.
When Claude edits a file, the sequence is:
Edit tool with the file path and changesPostToolUse hooks — the Edit|Write matcher matchesnpx prettier --write "src/app/page.tsx"The model never has to think about formatting. It just happens.
A PreToolUse hook can inspect commands before they run and block anything dangerous. This is useful for preventing accidental destructive operations.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash -c 'read input; cmd=$(echo \"$input\" | jq -r \".tool_input.command\"); case \"$cmd\" in *\"rm -rf /\"*|*\"DROP TABLE\"*|*\"--force\"*|*\"--no-verify\"*) echo \"Blocked dangerous command: $cmd\" >&2; exit 2;; *) exit 0;; esac'"
}
]
}
}
This hook:
Bash tool callsrm -rf /, DROP TABLE, --force, --no-verifyWhen a command is blocked, Claude sees the stderr message and knows the action was prevented. It can then adjust its approach or ask you for guidance.
For complex validation logic, use a dedicated script instead of an inline command. Create .claude/scripts/guard.sh:
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
if [ -z "$command" ]; then
exit 0
fi
blocked_patterns=(
"rm -rf /"
"DROP TABLE"
"DROP DATABASE"
"git push.*--force"
"git reset --hard"
"--no-verify"
"chmod 777"
)
for pattern in "${blocked_patterns[@]}"; do
if echo "$command" | grep -qE "$pattern"; then
echo "Blocked: command matches dangerous pattern '$pattern'" >&2
exit 2
fi
done
exit 0
Make it executable and reference it in your settings:
chmod +x .claude/scripts/guard.sh
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": ".claude/scripts/guard.sh"
}
]
}
}
This approach is easier to maintain, test, and extend. Your team can review and contribute to the script through normal code review.
If you run Claude Code on long tasks, you want to know when it needs your attention. A Notification hook sends a desktop alert:
macOS:
{
"hooks": {
"Notification": [
{
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
}
Linux:
{
"hooks": {
"Notification": [
{
"command": "notify-send 'Claude Code' 'Awaiting your input'"
}
]
}
}
You can also use the Stop event to get notified when Claude finishes its response entirely:
{
"hooks": {
"Stop": [
{
"command": "notify-send 'Claude Code' 'Task completed'"
}
]
}
}
The SessionStart hook runs when you open a new Claude Code session or resume an existing one. It's useful for injecting context that Claude should be aware of from the beginning.
{
"hooks": {
"SessionStart": [
{
"command": "echo '{\"message\": \"Current git branch: '$(git branch --show-current)'. Last 3 commits: '$(git log --oneline -3 | tr '\\n' '; ')'\"}'"
}
]
}
}
This gives Claude immediate awareness of your git state without you having to mention it. The JSON output with a message field gets added to Claude's context.
Here are other things you can inject at session start:
The matcher field accepts a regex pattern that's tested against the tool name. Here are common patterns:
| Matcher | Matches |
|---|---|
"Edit" | Only the Edit tool |
"Edit|Write" | Edit or Write tools |
"Bash" | Only Bash tool calls |
"Read" | Only file reads |
".*" | All tools (use carefully) |
| (omitted) | All tools — same as ".*" |
Matchers are case-sensitive and use standard regex syntax. The pattern is matched against the full tool name.
You can attach multiple hooks to the same event. They execute in order:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
},
{
"matcher": "Edit|Write",
"command": "npx eslint --fix \"$TOOL_INPUT_FILE_PATH\""
}
]
}
}
Both hooks run after every file edit — first Prettier, then ESLint. If the first hook fails (non-zero exit), the second still runs.
By default, hooks block Claude's execution until they complete. For tasks that don't need to finish before Claude continues, add "async": true:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "curl -X POST https://your-logging-service.com/api/log -d '{\"event\": \"file_edited\"}'",
"async": true
}
]
}
}
Async hooks run in the background. Claude doesn't wait for them and won't see their output. Use them for logging, analytics, or any side effect that doesn't affect Claude's next action.
Here are production-ready hooks you can adapt for your projects.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "bash -c 'if [[ \"$TOOL_INPUT_FILE_PATH\" == *.ts || \"$TOOL_INPUT_FILE_PATH\" == *.tsx ]]; then npx eslint --fix \"$TOOL_INPUT_FILE_PATH\"; fi'"
}
]
}
}
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "bash -c 'if [[ \"$TOOL_INPUT_FILE_PATH\" == *.test.* || \"$TOOL_INPUT_FILE_PATH\" == *.spec.* ]]; then npx jest \"$TOOL_INPUT_FILE_PATH\" --no-coverage; fi'",
"async": true
}
]
}
}
This runs tests asynchronously whenever Claude modifies a test file — giving you immediate feedback without slowing Claude down.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": "bash -c 'read input; cmd=$(echo \"$input\" | jq -r \".tool_input.command\"); if echo \"$cmd\" | grep -q \"git commit\" && [ \"$(git branch --show-current)\" = \"main\" ]; then echo \"Cannot commit directly to main branch\" >&2; exit 2; fi'"
}
]
}
}
{
"hooks": {
"PreToolUse": [
{
"command": "bash -c 'read input; echo \"$(date -Iseconds) | $input\" >> ~/.claude/tool-log.jsonl'",
"async": true
}
]
}
}
This creates an append-only log of every tool Claude calls, useful for auditing or debugging.
{
"hooks": {
"Stop": [
{
"command": "curl -s -X POST -H 'Content-type: application/json' --data '{\"text\":\"Claude Code finished a task\"}' $SLACK_WEBHOOK_URL",
"async": true
}
]
}
}
Claude Code offers several ways to customize behavior. Here's when to use each:
| Mechanism | Deterministic? | Use Case |
|---|---|---|
| Hooks | Yes — always runs | Formatting, security gates, notifications, logging |
| CLAUDE.md | No — model follows instructions | Project context, coding conventions, architecture rules |
| Skills | No — model-triggered | Reusable workflows, slash commands, specialized tasks |
| MCP Servers | Yes — tool availability | External integrations (GitHub, databases, APIs) |
The key distinction: hooks guarantee execution. Everything else relies on the model deciding to follow instructions. Use hooks when "probably" isn't good enough — formatting, security, notifications, and compliance checks.
When a hook isn't working as expected:
Check stderr — hook error messages show up in Claude's context. Add echo "debug: something" >&2 to your script to trace execution.
Test the command manually — run your hook command in a terminal with sample JSON piped to stdin:
echo '{"tool_input": {"command": "rm -rf /"}}' | .claude/scripts/guard.sh
echo $? # Should print 2 (blocked)
Verify the matcher — make sure your regex matches the exact tool name. Tool names are case-sensitive (Bash, not bash).
Check file permissions — scripts need to be executable (chmod +x).
Inspect JSON input — add a logging hook to see exactly what data Claude Code passes:
{
"hooks": {
"PreToolUse": [
{
"command": "bash -c 'read input; echo \"$input\" >> /tmp/hook-debug.jsonl'",
"async": true
}
]
}
}
Here's a full .claude/settings.json that combines the most useful hooks into a single configuration:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"command": ".claude/scripts/guard.sh"
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "npx prettier --write \"$TOOL_INPUT_FILE_PATH\""
}
],
"Notification": [
{
"command": "notify-send 'Claude Code' 'Awaiting your input'"
}
],
"Stop": [
{
"command": "notify-send 'Claude Code' 'Task completed'"
}
]
}
}
This gives you:
Start with this configuration and add hooks as your workflow demands. The beauty of hooks is that they compound — each one removes a manual step from your process, and they never forget to run.
PreToolUse is the most powerful event. It can block dangerous actions before they happen by exiting with code 2.PostToolUse is the most common event. Use it for auto-formatting, linting, and post-edit validation..claude/settings.json (project-wide, shareable) or ~/.claude/settings.json (global, personal)."Edit|Write" for file changes, "Bash" for commands."async": true for hooks that don't need to finish before Claude continues (logging, notifications, analytics).Production-ready CLAUDE.md templates, MCP server configs, custom hooks, and battle-tested workflows. Stop configuring, start building.