Hooks: Automating Your Workflow
Hooks let you run custom code automatically when specific events happen in Claude Code. They are the key to automating repetitive tasks, enforcing project rules, and building sophisticated workflows that run without manual intervention.
In this lesson, you will learn how hooks work, how to configure them, and how to use them to automate your development process.
What You Will Learn
- What hooks are and when they trigger
- How to configure hooks in settings.json
- The different hook event types
- Practical examples of useful hooks
- How to use hooks for quality gates and automation
- Debugging and troubleshooting hooks
What Are Hooks?
Hooks are scripts or commands that Claude Code runs automatically at specific points during its workflow. They are defined in your settings.json file and execute without Claude Code making a decision about whether to run them, which makes them fundamentally different from instructions in CLAUDE.md.
Think of hooks like git hooks (pre-commit, post-commit, etc.), but for Claude Code's workflow. They let you:
- Run linters or formatters automatically after Claude Code edits a file
- Validate changes before they are committed
- Send notifications when certain actions complete
- Enforce project rules that cannot be expressed in CLAUDE.md
- Chain multiple tools together in automated workflows
Hook Event Types
Claude Code can run hooks at many points in its lifecycle, such as when a session starts, when you submit a prompt, or before the conversation is compacted. Run /hooks to see what is available in your version. These four are the most common:
PreToolUse
Runs before Claude Code uses a tool (like editing a file or running a command). Use this to:
- Validate that an action should proceed
- Log what Claude Code is about to do
- Block certain actions under specific conditions
PostToolUse
Runs after Claude Code uses a tool. Use this to:
- Run formatters after file edits
- Validate the results of a command
- Trigger follow-up actions
Notification
Runs when Claude Code wants to notify you about something, for example when it needs your permission. Use this to:
- Send alerts to external systems (Slack, email)
- Log activity to a file
- Trigger desktop notifications
Stop
Runs when Claude Code finishes a task and is about to return control to you. Use this to:
- Run final validation checks
- Generate summaries of what changed
- Clean up temporary resources
Configuring Hooks
Hooks are configured in your settings.json file. You can have project-level hooks (.claude/settings.json) or user-level hooks (~/.claude/settings.json).
Basic Hook Structure
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
This hook runs Prettier on any file that Claude Code edits or creates. The config has three levels: the event (PostToolUse), a matcher group, and a nested hooks array that lists what to run.
The examples in this lesson use jq to read JSON. Install it first if your system does not have it.
Hook Configuration Fields
Each matcher group has these fields:
- matcher: A pattern that matches tool names. Examples:
"Edit","Write","Edit|Write","Bash". Leave it out, or use"*", to match everything. - hooks: The list of handlers to run when the matcher fits.
Each handler in that list has these fields:
- type: What kind of handler it is. This lesson uses
"command", which runs a shell command. Other types can call a URL, an MCP tool, or a Claude prompt. - command: The shell command to execute.
- timeout (optional): Maximum time in seconds before the hook is stopped.
How Hooks Receive Context
Command hooks receive context as a JSON object on stdin, not as environment variables. A PreToolUse hook for a file edit gets something like this:
{
"session_id": "abc123",
"cwd": "/home/you/my-app",
"hook_event_name": "PreToolUse",
"tool_name": "Edit",
"tool_input": {
"file_path": "/home/you/my-app/src/index.ts"
}
}
Read the fields you need with a tool like jq:
| Field | Description |
|---|---|
.tool_name | The name of the tool that triggered the hook |
.tool_input.file_path | The path of the file being edited (for Edit/Write tools) |
.tool_input.command | The shell command about to run (for the Bash tool) |
.tool_response | The tool's result (PostToolUse only) |
.session_id | The current session identifier |
A few environment variables are also set. The most useful is CLAUDE_PROJECT_DIR, the root of your project.
Exit Codes
The exit code tells Claude Code what to do next:
| Exit code | Meaning |
|---|---|
0 | Success. Claude Code carries on. |
2 | Blocking error. On events that can block, such as PreToolUse, the action stops and stderr is sent back to Claude. |
| Anything else | Non-blocking error. You see a notice and the action still runs. |
Exit code 1 does not block anything. Always use exit 2 to enforce a rule.
Practical Hook Examples
Auto-Format After Edits
Run your formatter automatically whenever Claude Code modifies a file:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
This ensures all code Claude Code writes matches your project's formatting rules, even if Claude Code does not perfectly follow your Prettier configuration.
Lint After Every Edit
Run the linter on changed files to catch issues immediately:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx eslint --fix"
}
]
}
]
}
}
Block Modifications to Protected Files
Prevent Claude Code from editing specific files:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | grep -qE 'generated|migrations' && echo 'BLOCKED: Cannot modify generated or migration files' >&2 && exit 2 || exit 0"
}
]
}
]
}
}
If the hook exits with code 2, the tool action is blocked and the message on stderr is sent back to Claude. Other non-zero codes are non-blocking errors, so exit 1 would not stop the edit.
Log All Shell Commands
Keep an audit trail of every command Claude Code runs:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$(date): $(jq -r .tool_input.command)\" >> \"$CLAUDE_PROJECT_DIR/.claude/command-log.txt\""
}
]
}
]
}
}
Run Tests After Changes
Automatically run related tests after Claude Code modifies source files:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "f=$(jq -r '.tool_input.file_path'); echo \"$f\" | grep -q '\\.test\\.' || npm test -- --findRelatedTests \"$f\" 2>/dev/null || true"
}
]
}
]
}
}
This runs tests related to the changed file (skipping test files themselves to avoid recursion).
Desktop Notification When Done
Get a system notification when Claude Code completes a long task:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code finished the task\" with title \"Claude Code\"'"
}
]
}
]
}
}
This macOS example uses AppleScript. On Linux, you could use notify-send instead. The Stop event does not use a matcher, so the group only needs its hooks array.
Combining Hooks for Quality Gates
You can combine several hooks to create comprehensive quality gates:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "f=$(jq -r '.tool_input.file_path'); npx prettier --write \"$f\" && npx eslint --fix \"$f\""
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "npm run typecheck"
}
]
}
]
}
}
This configuration:
- Formats every file Claude Code edits
- Lints every file Claude Code edits
- Runs the type checker when Claude Code finishes a task
Prettier and ESLint share one command so they run in order. Handlers listed separately can run at the same time and could both write to the same file.
Hook Best Practices
Keep hooks fast. By default, Claude Code waits for a hook to finish, so slow hooks delay its workflow. If a hook takes more than a second, consider adding "async": true to the handler or running it only on specific file types.
Handle errors on purpose. Only exit code 2 blocks. A command that fails with any other code shows a notice and Claude Code carries on. Make sure a script can only exit with 2 when you really want to stop the action.
Use matchers to be specific. Instead of running a hook on every tool use, match only the tools you care about. This reduces unnecessary processing.
Test hooks independently. Before adding a hook to your configuration, test the command manually in your terminal to make sure it works as expected.
Document your hooks. Add a comment in your CLAUDE.md explaining what hooks are configured and why, so team members understand the automated behaviors.
Debugging Hooks
Hook is not running: Check that the matcher matches the exact tool name. Common tool names are Edit, Write, Bash, Read. Run /hooks to see which hooks Claude Code has loaded.
Hook command fails: Run the command manually in your terminal and pipe in sample JSON to see the error. For example: echo '{"tool_input":{"file_path":"src/index.ts"}}' | your-command. You can also start Claude Code with claude --debug to write hook details to a debug log.
Hook blocks an action unexpectedly: Check PreToolUse hooks for exit codes. Exit code 2 blocks the action.
Hook runs too slowly: Add a timeout to the handler to limit how long it can run, or set "async": true so it runs in the background.
Key Takeaways
- Hooks are scripts that run automatically at specific points in Claude Code's workflow. The most common events are PreToolUse, PostToolUse, Notification, and Stop
- Configure hooks in
.claude/settings.json(project-level) or~/.claude/settings.json(user-level) as matcher groups, each with a nestedhooksarray of handlers (type, command, and optional timeout) - Common uses include auto-formatting after edits, running linters, blocking modifications to protected files, and sending notifications
- Command hooks receive context as JSON on stdin. Read fields like
.tool_input.file_pathwith a tool such asjq - PreToolUse hooks that exit with code 2 block the action, making them useful for enforcing rules
- Keep hooks fast and handle errors gracefully to avoid disrupting Claude Code's workflow
- Combine multiple hooks to create comprehensive quality gates that ensure code quality automatically
Quiz
Up next ยท Lesson 12 of 14
Git Workflows and Multi-File Edits
6 min read
11 of 14 lessons done

