Claude Code Hooks: Automate Actions Before and After Every Tool Call
Claude Code hooks let you run shell commands automatically before or after any tool call — Edit, Write, Bash, Read, and more. Learn how to configure PreToolUse and PostToolUse hooks in .claude/settings.json to auto-lint, block dangerous commands, log every action, and more.
If you have ever used Claude Code to edit a file and then immediately typed prettier --write or npm test in a separate terminal, you already know the pattern hooks are designed to eliminate. Claude Code hooks are shell commands that execute automatically at defined points in the tool-call lifecycle — no prompting, no post-edit rituals, no manual oversight.
This guide covers the complete hooks system: what hooks are, how to configure them, which exit codes matter, and six practical use cases you can copy today.
What Hooks Are
A hook is a shell command that Claude Code runs before or after it executes one of its built-in tools. The built-in tools include:
Bash — running terminal commands
Edit — making changes to an existing file
Write — creating or overwriting a file
Read — reading a file's contents
WebFetch, WebSearch, and others
Every time Claude Code is about to invoke one of these tools, it can first run a hook you have defined. Every time Claude Code finishes invoking one of these tools, it can run a separate hook you have defined. The hook is just a shell command — it can be a one-liner, a script path, or a chained command.
Where to Configure Hooks
Hooks live in settings.json under the hooks key. There are two locations:
Location
Scope
.claude/settings.json (project root)
Applies to everyone on the project
~/.claude/settings.json (home directory)
Applies to all projects for your user
Both files use the same schema. If you want your team to share a lint-on-edit rule, put it in the project-level file and commit it to the repository. If you want personal logging or notifications, put it in your user-level file.
PreToolUse — runs before Claude executes the matched tool. If this hook exits with a non-zero code, Claude Code aborts the tool call entirely. This is how you block operations.
PostToolUse — runs after Claude has already executed the matched tool. The exit code of a PostToolUse hook does not block anything. Use this for side effects: formatting, testing, logging, notifications.
The Hook Format in settings.json
The hooks key contains an object with PreToolUse and/or PostToolUse arrays. Each entry in the array specifies a matcher (which tool to watch) and a hooks array (the commands to run).
json
{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"echo 'About to run bash'"}]}],"PostToolUse":[{"matcher":"Edit","hooks":[{"type":"command","command":"npm run lint --fix"}]}]}}
The matcher field is a string that matches the tool name exactly (case-sensitive): "Bash", "Edit", "Write", "Read", "WebFetch", etc. Multiple tools: use comma-separated matchers like "Bash,PowerShell" — a bug in versions before 2.1.191 caused those hooks to silently never fire; update Claude Code if multi-tool matchers do nothing (2.1.191 fix). The type field is always "command". The command field is the shell string that will be executed.
Hook Exit Codes
For PreToolUse hooks, exit codes carry enforcement weight:
Exit 0 — allow the tool call to proceed
Any non-zero exit — block the tool call; Claude Code aborts the operation and reports the failure
For PostToolUse hooks, exit codes are informational only. The tool has already run. A failing PostToolUse hook will surface an error in the output but will not undo what was done.
This asymmetry is intentional. PreToolUse is for gates and guards. PostToolUse is for reactions.
Six Real-World Use Cases
1. Auto-Format Every File Edit
The most common use case. After Claude edits or writes any file, run Prettier automatically.
The formatter runs immediately after each file operation, so Claude's next action always sees clean, formatted code.
2. Run Tests After File Changes
Every time Claude writes a file, run the test suite. This catches regressions before Claude moves on to the next step.
json
{"hooks":{"PostToolUse":[{"matcher":"Write","hooks":[{"type":"command","command":"npm test --silent"}]}]}}
For large test suites, scope it to the affected module rather than running everything:
json
{"type":"command","command":"npm test -- --testPathPattern=$(cat) --silent"}
3. Log Every Bash Command for an Audit Trail
In security-sensitive environments, you need a record of every command Claude runs. A PreToolUse hook on Bash that appends to a log file gives you this without blocking anything.
This exits 0 every time (because it only appends and echoes), so it never blocks the command. It just records it. Review ~/.claude/audit.log after any session to see the full command history.
4. Block Dangerous Commands
Use a PreToolUse hook on Bash to inspect the command before it runs and reject it if it matches a dangerous pattern.
When this hook exits with code 1, Claude Code aborts the Bash call and surfaces the error message. Claude will see the block and either rephrase the approach or ask you to confirm explicitly.
For a more maintainable setup, move the logic into a script:
For workflows where you want a commit checkpoint after Claude finishes writing a file:
json
{"hooks":{"PostToolUse":[{"matcher":"Write","hooks":[{"type":"command","command":"git add -A && git diff --cached --quiet || git commit -m 'auto: claude write checkpoint'"}]}]}}
The git diff --cached --quiet || guard prevents empty commits — it only commits if there are staged changes.
Use this with caution in projects with pre-commit hooks, since each Write will trigger the full pre-commit pipeline.
6. Send a Slack Notification When a Long Task Finishes
After Claude finishes a Bash command (which might have been a long build or deploy), send a Slack notification.
json
{"hooks":{"PostToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"curl -s -X POST -H 'Content-type: application/json' --data '{\"text\":\"Claude Code finished a bash task.\"}' $SLACK_WEBHOOK_URL"}]}]}}
Note: $SLACK_WEBHOOK_URL is read from your shell environment, not hardcoded in the JSON. Never put the raw webhook URL directly in settings.json.
User-Level vs. Project-Level Hooks
Project-level (.claude/settings.json):
Lives in the repository root
Committed to version control
Shared with every contributor
Best for: lint rules, test runners, safety guards your whole team should have
User-level (~/.claude/settings.json):
Lives in your home directory
Never committed
Applies to every Claude Code session on your machine
Best for: personal audit logs, Slack notifications, personal safety rules
When both files exist, Claude Code merges them. Project-level hooks and user-level hooks both run. There is no override — both lists execute.
Testing Hooks with --debug
Before relying on a hook in production, verify it works with the --debug flag:
bash
claude --debug
In debug mode, Claude Code prints each hook invocation — the matcher, the command, the exit code, and any stdout/stderr output. This is the fastest way to confirm your hook is being triggered and that the exit code is what you expect.
A common mistake: the hook command path is not in $PATH at the time Claude Code runs. If your hook fails silently, check whether the binary is accessible from a non-interactive shell (the environment Claude Code hooks run in).
Security Considerations
Hooks run with your OS user's full permissions. There is no sandboxing. A hook that runs curl or bash has the same access you do.
Two rules:
Never put secrets in hook command strings. If your hook needs an API key or a webhook URL, read it from an environment variable (like $SLACK_WEBHOOK_URL above) or from a file outside the repository. The command string in settings.json can end up in logs, process listings, and version control history.
Review third-party settings.json files. If you clone a repository that includes .claude/settings.json with hooks already defined, read those hooks before running Claude Code. A malicious hook in PreToolUse on Bash could exfiltrate data on every command.
The same common sense that applies to .github/workflows applies here.
Summary: Hook Decision Table
Goal
Hook Type
Matcher
Exit on block?
Auto-format files
PostToolUse
Edit, Write
No
Run tests after write
PostToolUse
Write
No
Audit log all bash
PreToolUse
Bash
No (exit 0)
Block dangerous commands
PreToolUse
Bash
Yes (exit 1)
Auto-commit checkpoints
PostToolUse
Write
No
Slack notifications
PostToolUse
Bash
No
Hooks are one of the most underused features in Claude Code. They close the gap between "Claude made a change" and "the change is verified, formatted, and logged" — without any additional prompting or terminal switching on your part.