Claude Code Hooks: Automate Workflows with Pre and Post Tool Execution
Claude Code hooks are shell commands that run automatically before or after Claude executes a tool. They let you enforce code quality standards, run formatters, trigger tests, log activity, and block unsafe operations — all without Claude needing to remember to do any of it. Hooks fire at the system level, making them reliable in every session without prompting or reminders.
What hooks are and how they work
Every time Claude Code calls a tool — Write, Edit, Bash, Read, Grep, Glob — two hook points fire in sequence:
- PreToolUse: executes before the tool runs. If the hook exits with a non-zero code, the tool call is blocked entirely.
- PostToolUse: executes after the tool completes. Receives the result, can trigger side effects like formatting or logging.
This architecture means hooks are not suggestions to Claude. They are commands your shell runs unconditionally whenever Claude attempts a matched tool call. You can use PreToolUse as a hard gate (no rm -rf without approval), or PostToolUse as a side-effect trigger (run prettier after every file write).
Where to configure hooks
Hooks live in a settings.json file. There are two locations, and both can be active at once:
Project-level: .claude/settings.json in your project root. Apply to everyone working in that repository. Commit this file to share hooks across a team.
Global (user-level): ~/.claude/settings.json in your home directory. Apply to all Claude Code sessions on your machine, regardless of project.
When both files exist, hooks from both files are active. There is no override — both sets run. Use project-level hooks for project-specific enforcement (test suite, linter config, logging path) and global hooks for universal policies (security checks, audit logging).
Hook configuration structure
The hooks key at the top level of settings.json contains an object with PreToolUse and PostToolUse arrays. Each entry has a matcher (the tool name to match) and a nested hooks array with one or more command objects.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo 'About to run bash command'"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "prettier --write $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true"
}
]
}
]
}
}
The double-nested hooks key (one on the outer object, one inside each matcher entry) is intentional. The outer key signals that hooks configuration follows. The inner key holds the ordered list of commands to execute for that matcher.
Available matchers
Matchers correspond to Claude Code's built-in tool names. The tools you can match on are:
Bash— shell command executionWrite— creating or overwriting a fileEdit— targeted text replacement inside an existing fileRead— reading file contentsGlob— pattern-based file listingGrep— pattern-based content search
Match exactly one tool per entry. To hook multiple tools, add multiple objects to the PreToolUse or PostToolUse array.
Environment variables available in hook commands
Hook commands run as shell processes and receive context about the tool call through environment variables. The pattern is consistent:
$CLAUDE_TOOL_INPUT_*— input parameters passed to the tool. For Write, this includes$CLAUDE_TOOL_INPUT_FILE_PATHand$CLAUDE_TOOL_INPUT_CONTENT. For Bash,$CLAUDE_TOOL_INPUT_COMMANDcontains the command string.$CLAUDE_TOOL_OUTPUT_*— output values from the tool (PostToolUse only). For Read,$CLAUDE_TOOL_OUTPUT_CONTENTcontains the file contents that were returned.
The specific variable names mirror the parameter names of each tool. A Bash hook checking the command being run reads $CLAUDE_TOOL_INPUT_COMMAND. A Write hook formatting the file reads $CLAUDE_TOOL_INPUT_FILE_PATH to know which file was written.
Five practical hook configurations
1. Auto-format on Write
Run Prettier after every file Claude writes. The || true prevents a Prettier failure from appearing as a hook error.
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
}
]
}
Place this in PostToolUse. Claude writes the file; Prettier immediately reformats it to your project style. The file Claude wrote and the file on disk may differ — that is fine. The formatted version is what gets committed.
2. Run tests after editing a source file
Trigger the relevant test suite after Claude modifies a file. This surfaces failures immediately rather than at the end of a multi-step session.
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "npm test -- --testPathPattern=\"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>&1 | tail -20"
}
]
}
The tail -20 trims output to the last 20 lines so Claude's context is not flooded with verbose test output. Adjust to --passWithNoTests for files that have no direct test counterpart.
3. Security gate on Bash commands
Block shell commands that match known-dangerous patterns before they execute. Exit 1 in a PreToolUse hook cancels the tool call entirely.
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$CLAUDE_TOOL_INPUT_COMMAND\" | grep -qE '(rm -rf|sudo |DROP TABLE)' && echo 'Blocked: dangerous command pattern' && exit 1 || true"
}
]
}
This fires before every Bash call. If the command string matches any pattern in the grep, the hook exits 1 and Claude's Bash call is cancelled. Claude receives the hook's output as context and can explain or reroute.
4. Append all file changes to an audit log
Log every file Claude writes, with a timestamp. Useful for compliance requirements or debugging multi-step sessions after the fact.
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) WRITE $CLAUDE_TOOL_INPUT_FILE_PATH\" >> /tmp/claude-audit.log"
}
]
}
Place in PostToolUse so only successful writes are logged. The log is append-only and survives session restarts.
5. Lint TypeScript files on save
Run ESLint specifically on TypeScript files after any Write. The case check restricts linting to .ts and .tsx extensions so the hook is a no-op on Markdown or JSON writes.
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "case \"$CLAUDE_TOOL_INPUT_FILE_PATH\" in *.ts|*.tsx) npx eslint --fix \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true ;; esac"
}
]
}
--fix auto-applies safe lint corrections. Violations ESLint cannot auto-fix are reported to Claude's context, letting it decide whether to address them.
Using exit codes as gates
Non-zero exit codes in PreToolUse hooks are the enforcement mechanism. When a PreToolUse hook exits non-zero:
- Claude's tool call does not execute.
- The hook's stdout is returned to Claude as context.
- Claude can read the message, adjust its approach, or tell you it was blocked and why.
This means you can write hooks that act as interactive gates: check a condition, print a human-readable reason if the condition fails, exit 1. Claude will surface that reason in the conversation. Zero exit allows the tool call to proceed normally.
Project hooks vs global hooks
The decision of where to put a hook is a policy decision.
Project hooks (.claude/settings.json) encode project-specific rules: which test command to run, which formatter to use, which log path to write to. Commit this file so every team member and every CI run gets the same behavior.
Global hooks (~/.claude/settings.json) encode machine-wide policies: blocking rm -rf unconditionally, appending to a personal audit log, enforcing a personal coding standard across all projects. These are personal and not shared.
Hooks in both files run. There is no conflict resolution because they do not conflict — they compose. If your global hooks log all Write calls and your project hooks format all Write calls, both things happen on every Write, in the order they are listed.
Frequently asked questions
What are Claude Code hooks?
Claude Code hooks are shell commands configured in settings.json that run automatically before or after Claude uses a tool. They let you enforce formatting, run tests, block unsafe commands, and log activity without relying on Claude to remember to do those things in each session.
Where do I put hook configuration?
Hooks go in a settings.json file at .claude/settings.json (project-level, inside your repo) or ~/.claude/settings.json (global, for all projects on your machine). Both files can be active simultaneously — hooks from both run on every matching tool call.
Can hooks block Claude from doing something?
Yes. A PreToolUse hook that exits with a non-zero code prevents the matched tool from executing. Claude receives the hook's output as context, so you can print a reason explaining why the action was blocked. This is how security gates work: check the command, print a message, exit 1.
What environment variables are available inside hook commands?
Hooks receive $CLAUDE_TOOL_INPUT_* variables containing the tool's input parameters, and $CLAUDE_TOOL_OUTPUT_* variables (PostToolUse only) containing the tool's output. For example, a hook on Write receives $CLAUDE_TOOL_INPUT_FILE_PATH with the path of the file being written and $CLAUDE_TOOL_INPUT_CONTENT with its contents.
Do hooks run in every Claude Code session automatically?
Yes. Hooks in settings.json fire unconditionally in every session that loads that settings file. You do not need to prompt Claude or remind it — the hooks are executed by the Claude Code runtime, not by Claude's language model. This is what makes them reliable for enforcement and automation.
Take It Further
Power Prompts 300: Claude Code Productivity Patterns — Section 6 covers Claude Code Automation: 15 production hook configurations for formatting, testing, security scanning, and audit logging — plus the settings.json templates for 8 common project types that wire up all the hooks automatically.
-> Get Power Prompts 300 — $29
30-day money-back guarantee. Instant download.