What are Claude Code hooks?
Hooks let you wire shell commands to events inside Claude Code — the same way you'd use git hooks or npm lifecycle scripts, but for your AI coding agent.
Every time Claude performs an action (writes a file, runs a Bash command, calls a tool), hooks fire your shell code automatically. You don't change any prompts. You don't ask Claude to remember a rule. The hook runs regardless of what Claude is doing.
src/auth.ts. Before you see the result, your PostToolUse hook runs prettier --write src/auth.ts && eslint --fix src/auth.ts automatically. Every write, forever, in this project.
Hooks are stored in settings.json — either project-scoped (.claude/settings.json) or user-scoped (~/.claude/settings.json). They're plain JSON, version-controllable, and shareable with your team.
The 4 hook events
| Event | When it fires | Can block the action? |
|---|---|---|
| PreToolUse | Before Claude calls any tool (Bash, Write, Edit, Read, …) | Yes — exit non-zero to block |
| PostToolUse | After a tool call completes successfully | No — tool already ran |
| Stop | When Claude finishes a response and stops generating | No |
| UserPromptSubmit | When the user submits a message (before Claude sees it) | No |
Matchers
Each hook is scoped to a tool name pattern via a matcher field. Set it to the exact tool name (Write, Bash, Edit, Read) or use "" (empty string) to match every tool call.
settings.json structure
Here is the complete JSON structure. Add a "hooks" key to either .claude/settings.json (project) or ~/.claude/settings.json (user-global):
{
"permissions": { ... },
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\""
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/usr/local/bin/check-dangerous-command.sh"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"'"
}
]
}
]
}
}
Each event key maps to an array of hook groups. Each group has a matcher (tool name or "" for all) and a hooks array. The type is always "command" — it runs the shell command string via bash -c.
Environment variables in hooks
Claude Code injects these into every hook's environment at runtime:
| Variable | Value | Available in |
|---|---|---|
CLAUDE_TOOL_NAME | Name of the tool being called (e.g. Write) | Pre + Post |
CLAUDE_TOOL_INPUT | Full JSON input object for the tool call | Pre + Post |
CLAUDE_TOOL_INPUT_FILE_PATH | The file_path parameter, for Write/Edit/Read calls | Pre + Post |
CLAUDE_TOOL_INPUT_COMMAND | The shell command string, for Bash calls | Pre + Post |
CLAUDE_TOOL_OUTPUT | Tool result (stdout/return value) | PostToolUse only |
CLAUDE_SESSION_ID | Unique ID for the current Claude Code session | All events |
Copy-paste hook patterns
The most useful hooks developers add in the first week. Each is a drop-in hooks value for your settings.json.
1. Auto-format every file Claude writes
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true"
}
]
}
]
}
2. Lint-on-write with ESLint
|| true prevents hook failures from blocking Claude's workflow — remove it if you want strict enforcement."hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [{ "type": "command", "command": "npx eslint --fix \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true" }]
},
{
"matcher": "Edit",
"hooks": [{ "type": "command", "command": "npx eslint --fix \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true" }]
}
]
}
3. Auto-stage files Claude modifies
git diff --staged at the end of a session."hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [{ "type": "command", "command": "git add \"$CLAUDE_TOOL_INPUT_FILE_PATH\" 2>/dev/null || true" }]
}
]
}
4. Desktop notification when Claude stops
/loop runs or slow builds."hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"' 2>/dev/null || notify-send 'Claude Code' 'Claude finished' 2>/dev/null || true"
}
]
}
]
}
5. Log every Bash command Claude runs
~/.claude-audit.log with a timestamp. Good for compliance or debugging agent behavior."hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"[$(date -u +%FT%TZ)] [$CLAUDE_SESSION_ID] $CLAUDE_TOOL_INPUT_COMMAND\" >> ~/.claude-audit.log"
}
]
}
]
}
6. Slack notification when Claude finishes
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "curl -s -X POST -H 'Content-type: application/json' --data '{\"text\":\"Claude Code finished a task in session $CLAUDE_SESSION_ID\"}' YOUR_SLACK_WEBHOOK_URL"
}
]
}
]
}
Blocking tool calls with PreToolUse
When a PreToolUse hook exits with a non-zero status, Claude Code blocks the tool call and feeds the hook's stderr back to Claude as a rejection reason. Claude will typically explain the block to you and suggest an alternative.
Example: block writes to production config
"hooks": {
"PreToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "if echo \"$CLAUDE_TOOL_INPUT_FILE_PATH\" | grep -qE '(prod|production|\.env\.prod)'; then echo 'Writing to production config is blocked by hook policy' >&2; exit 1; fi"
}
]
}
]
}
Example: require tests to pass before any commit
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "if echo \"$CLAUDE_TOOL_INPUT_COMMAND\" | grep -q 'git commit'; then npm test --silent 2>&1 || { echo 'Tests must pass before committing' >&2; exit 1; }; fi"
}
]
}
]
}
Using /update-config to add hooks without editing JSON
The /update-config skill is the fastest way to add hooks — describe what you want in plain English and Claude writes the correct JSON:
- Open Claude Code in any project.
- Type:
/update-config - Describe the hook — for example:
"Add a PostToolUse hook that runs prettier on every file Claude writes" - Claude reads your current
settings.jsonand patches in the correct hook JSON, handling escaping and matcher syntax for you.
/update-config session: "also add an audit log hook for Bash calls" — Claude accumulates them in a single settings.json patch.
FAQ
Can a hook read the full tool input JSON, not just the file path?
$CLAUDE_TOOL_INPUT contains the complete JSON object passed to the tool. You can pipe it to jq to extract specific fields: echo "$CLAUDE_TOOL_INPUT" | jq -r '.command'.
Do hooks run for every tool, or just the matched one?
"matcher": "Write" fires only when the Write tool is called — not Edit, not Bash. Set "matcher": "" to match every tool call under that event type.
Are hooks project-specific or global?
.claude/settings.json in your project directory applies only to that project. ~/.claude/settings.json applies globally to every Claude Code session. Both files' hooks are merged — global hooks always fire, and project hooks layer on top.
Do hooks run asynchronously or block Claude?
PreToolUse hooks, this determines whether the tool runs. For PostToolUse and Stop hooks, keep them fast — a slow hook delays Claude's next action.
Can I use environment variables in hook commands?
.env file at the start of your hook command: source .env && your-script.sh.
More about skills: see the Claude Skills Browser for the full list of slash commands that work alongside hooks.