Claude Code hooks are shell commands (or HTTP calls, or model checks) that Claude Code runs automatically at fixed points, such as before a tool call or when Claude finishes a turn. This guide explains the events, the settings format, exit codes and JSON output, then gives seven recipes you can paste into .claude/settings.json today.
- Hooks are deterministic: they run every time an event fires, unlike CLAUDE.md instructions, which Claude may or may not follow.
- Configure hooks in the
hooksobject of~/.claude/settings.json,.claude/settings.jsonor.claude/settings.local.json, then run/hooksto confirm they loaded. PreToolUsewith exit code 2 blocks a tool call and sends your stderr message to Claude;PostToolUseruns after the tool and can't undo it.- A
Stophook that exits 2 keeps Claude working, which turns "tests must pass" into a hard gate; checkstop_hook_activeto avoid loops. - Hooks run with your user's permissions, so review every hook script in a repository before you trust it.
What are Claude Code hooks?
Claude Code hooks are user-defined commands that Claude Code runs automatically at specific points in a session: when it starts, before and after each tool call, when Claude needs your input, when it finishes a turn and more. The hooks guide describes the point of them as "deterministic control: certain actions always happen rather than relying on the LLM to choose to run them."
That is the key difference from instructions. A line in CLAUDE.md saying "run Prettier after editing" is a request Claude may forget. A PostToolUse hook that runs Prettier after every edit runs every time. Anthropic's best practices put it this way: CLAUDE.md instructions are advisory, hooks are deterministic. Our guide to CLAUDE.md best practices covers the advisory side.
Typical uses:
- Format or lint every file Claude edits.
- Block edits to
.env, lockfiles or migrations. - Block or log risky shell commands.
- Send a desktop notification when Claude is waiting for you.
- Re-inject key context after the conversation is compacted.
- Refuse to let Claude stop until the tests pass.
Which hook events does Claude Code support?
Claude Code fires more than 30 hook events, but most useful hooks use six of them. The full list lives in the hooks reference; these are the ones to learn first.
| Event | When it fires | Can it block? | Common use |
|---|---|---|---|
SessionStart | A session begins, resumes, or restarts after /clear or compaction | No | Inject context, load environment |
UserPromptSubmit | You submit a prompt, before Claude sees it | Yes | Add context, reject prompts with secrets |
PreToolUse | Before a tool call runs | Yes | Block commands or protected files |
PostToolUse | After a tool call succeeds | No (already ran) | Format, lint, log |
Notification | Claude Code sends a notification, such as a permission prompt | No | Desktop or phone alerts |
Stop | Claude finishes responding | Yes | Run tests, refuse to stop until they pass |
Other events worth knowing: PreCompact and PostCompact around compaction, SubagentStart and SubagentStop for subagents, InstructionsLoaded when a CLAUDE.md or rules file loads (handy for debugging), FileChanged for watched files, and SessionEnd.
How do you configure a hook?
You configure hooks in a hooks object inside a Claude Code settings file. Each event name maps to a list of groups; each group has an optional matcher and a list of handlers.
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" } ] } ] }}Where the file lives decides who gets the hook:
| Location | Scope | Committed? |
|---|---|---|
~/.claude/settings.json | All your projects | No |
.claude/settings.json | This project, whole team | Yes |
.claude/settings.local.json | This project, only you | No |
| Managed policy settings | Organization-wide | Admin-controlled |
Hooks can also ship inside plugins, skills and subagent definitions. Run /hooks inside Claude Code to browse every configured hook grouped by event. Edits to settings files are normally picked up automatically; restart the session if a change doesn't show up.
Matchers
A matcher narrows when a hook fires. For tool events it matches the tool name: Bash, Edit|Write, or a regex such as mcp__github__.* for every tool on one MCP server. For SessionStart it matches how the session started (startup, resume, clear, compact), and for Notification the notification type, such as permission_prompt or idle_prompt. An empty matcher fires on everything. Matchers are case-sensitive.
For finer control on tool events, the if field uses permission-rule syntax, so "if": "Bash(git *)" runs the hook only for git commands instead of every shell command.
How do hooks read input and return decisions?
A command hook receives the event as JSON on stdin and answers with an exit code, stderr and optionally JSON on stdout. For a shell command, a PreToolUse hook receives something like this:
{ "session_id": "abc123", "cwd": "/Users/sarah/myproject", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "npm test" }}Exit codes carry the basic decision:
| Exit code | Meaning |
|---|---|
0 | No objection. For PreToolUse this does not approve the call; the normal permission flow still applies. For SessionStart and UserPromptSubmit, plain stdout is added to Claude's context. |
2 | Block, where the event allows it. Write the reason to stderr. On PreToolUse the tool call is blocked and Claude sees the reason; on Stop Claude keeps working. |
| Anything else | A non-blocking error on most events. The action proceeds and a hook error notice appears. |
For richer decisions, exit 0 and print JSON. A PreToolUse hook can deny with a reason, escalate to the normal prompt with "ask", or skip the prompt with "allow":
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Use rg instead of grep" }}Two rules from the docs that save debugging time: pick either exit 2 or JSON per hook, not both, and keep fields like permissionDecision and additionalContext inside hookSpecificOutput, because misplaced fields are silently ignored.
Seven Claude Code hook recipes you can copy
These recipes come from or build on the official examples. The Bash scripts use jq to parse JSON, so install it first (brew install jq or apt-get install jq).
1. Get notified when Claude needs you
Put this in ~/.claude/settings.json so it works in every project. This is the Linux version; on macOS replace the command with osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'.
{ "hooks": { "Notification": [ { "matcher": "", "hooks": [ { "type": "command", "command": "notify-send 'Claude Code' 'Claude Code needs your attention'" } ] } ] }}Set the matcher to permission_prompt to be pinged only when Claude is waiting for approval.
2. Format every file Claude edits
The PostToolUse example in the configuration section above runs Prettier on each file Claude writes. Swap in ruff format, gofmt -w or rustfmt for other languages. Claude can also change files through shell commands, which Edit|Write does not catch; the docs suggest a Stop hook that scans the working tree if you need full coverage.
3. Protect sensitive files
Save this script as .claude/hooks/protect-files.sh and run chmod +x on it:
#!/bin/bashINPUT=$(cat)FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")for pattern in "${PROTECTED_PATTERNS[@]}"; do if [[ "$FILE_PATH" == *"$pattern"* ]]; then echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2 exit 2 fidoneexit 0Register it before every edit:
{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" } ] } ] }}$CLAUDE_PROJECT_DIR points at the project root, so the path works from any subdirectory.
4. Block dangerous shell commands
The same pattern works for Bash. This script stops rm -rf and dropping tables; Claude receives the message and tries another approach.
#!/bin/bashCOMMAND=$(jq -r '.tool_input.command')if echo "$COMMAND" | grep -qiE 'rm -rf|drop table'; then echo "Blocked: destructive command. Ask the user first." >&2 exit 2fiexit 0Register it under PreToolUse with "matcher": "Bash". A hook deny holds in every permission mode, including --dangerously-skip-permissions.
Pattern-matching shell commands is best-effort; a determined command can be phrased in ways your regex misses. Use permission deny rules and a sandboxed environment as the real boundary, and hooks as an extra layer.
5. Log every command Claude runs
An audit trail is useful for unattended runs:
{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt" } ] } ] }}6. Re-inject context after compaction
When a long session is compacted, details can drop out. A SessionStart hook with the compact matcher adds text back, because stdout from SessionStart goes into Claude's context:
{ "hooks": { "SessionStart": [ { "matcher": "compact", "hooks": [ { "type": "command", "command": "echo 'Reminder: use pnpm, not npm. Run pnpm test before committing. Current task: auth refactor.'" } ] } ] }}7. Don't let Claude stop until tests pass
A Stop hook that exits 2 keeps Claude working and shows it your stderr. Claude Code overrides a Stop hook after eight consecutive blocks without a tool call in between, and the input includes stop_hook_active so you can avoid loops:
#!/bin/bashINPUT=$(cat)if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then exit 0fiif ! npm test --silent > /tmp/claude-test.log 2>&1; then echo "Tests are failing. Fix them before finishing:" >&2 tail -n 30 /tmp/claude-test.log >&2 exit 2fiexit 0Register it under "Stop" (no matcher needed). Keep the test command fast; a slow suite runs at the end of every turn, and Stop fires whenever Claude finishes responding, not only at task completion.
What are prompt, agent and HTTP hooks?
Besides shell commands, Claude Code supports hooks that ask a model or call a URL. Use them when a decision needs judgment or when logic lives on a server.
| Type | What it does | Default timeout |
|---|---|---|
command | Runs a shell command | 10 minutes |
http | POSTs the event JSON to a URL | 10 minutes |
prompt | Sends the event to a Claude model for a single yes or no decision | 30 seconds |
agent | Spawns a subagent that can read files and run tools before deciding (experimental) | 60 seconds |
A prompt hook on Stop can ask "are all requested tasks complete?" and return {"ok": false, "reason": "..."} to send Claude back to work. An HTTP hook suits a shared audit service that logs tool use for a whole team. Override any timeout with the timeout field in seconds.
What are the common gotchas with Claude Code hooks?
Most hook problems come from five causes: the matcher, the script, the JSON shape, loops, and trusting a hook you didn't write.
- Hook never fires. Run
/hooksand check the event and matcher. RememberPreToolUseruns before the tool andPostToolUseafter. - "command not found". Use
"$CLAUDE_PROJECT_DIR"or absolute paths, make scripts executable, and installjq. - Test a script by hand. Pipe sample input in and check the exit code:
echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?. - JSON ignored with no error. Your shell profile may print text before the JSON, or a field sits outside
hookSpecificOutput. Runclaude --debugand search the log. - Hooks run in parallel. When several hooks match, all run to completion, and for
PreToolUsethe most restrictive decision wins. Don't rely on one hook's deny to stop another hook's side effects. PostToolUsecan't undo. The tool already ran. UsePreToolUseto prevent.- Security. Hooks execute shell commands with your user's permissions. A cloned repository can ship hooks in
.claude/settings.json, so read them before you trust the project. To switch everything off, set"disableAllHooks": true.
Hooks matter most when you are not watching. If agents run for hours on a server while you sleep, a protected-files hook, a command log and a tests-must-pass Stop hook are what keep the morning review short. Orkio runs Claude Code on an always-on, isolated cloud box, and the hooks committed in your repository's .claude/settings.json come along with the project. For the CI side of automation, see Claude Code GitHub Actions, and for running unattended work well, our guide to background coding agents.
How to add your first hooks today
Start with three hooks and grow from real problems:
- Add the notification hook to
~/.claude/settings.jsonso you stop watching the terminal. - Add the formatter hook to the project's
.claude/settings.jsonand commit it. - Add the protect-files hook for
.envand lockfiles. - Run
/hooksto confirm all three loaded, then trigger each once to see it work. - When Claude repeats a mistake that a script could catch, turn the CLAUDE.md rule into a hook.
- Before running agents unattended, add the command log and the tests-must-pass
Stophook.
Frequently asked questions
What is the difference between Claude Code hooks and CLAUDE.md?
CLAUDE.md is context that Claude reads and tries to follow, with no guarantee. Hooks are commands the Claude Code client runs at fixed lifecycle events whatever the model decides. Use CLAUDE.md for conventions and hooks for anything that must happen every time.
Where do I put Claude Code hooks?
Put personal hooks in ~/.claude/settings.json, team hooks in the project's .claude/settings.json so they are committed, and private project hooks in .claude/settings.local.json. Hooks can also come from managed policy settings, plugins, skills and subagent definitions.
Can a hook approve a command automatically?
A PreToolUse hook can return a JSON permissionDecision of allow to skip the permission prompt, but deny rules from settings still apply. A deny from a hook blocks the tool in every permission mode, including bypassPermissions.
Why is my Claude Code hook not running?
Run /hooks and check that it appears under the right event, then check that the matcher matches the tool name exactly, since matchers are case-sensitive. Test the script by piping sample JSON into it, make sure it is executable, and check the settings file for invalid JSON such as trailing commas.
Do hooks work in headless mode with claude -p?
Yes. Hooks from your settings files run in non-interactive mode too, which makes them useful for scripted and unattended runs. PermissionRequest hooks behave differently there, so read the limitations in the docs before relying on them in automation.
