Claude Code Hooks Guide: Automate and Guard Your Agent
How Claude Code hooks work: lifecycle events, matchers, exit codes and JSON output, plus ready-made hooks for formatting, alerts and blocking risky edits.
By Claudekit Team Published 12 min read
Instructions in CLAUDE.md are suggestions. Claude usually follows them, but “usually” isn’t good enough for “never edit .env” or “always run the formatter”. Hooks close that gap: they are commands Claude Code runs automatically at specific points in its lifecycle, every time, no matter what the model decides.
This guide covers how hooks work, where to configure them, and five hooks worth adding to almost any project. All details are verified against the official Claude Code hooks documentation as of October 2026.
How hooks work
When an event fires, Claude Code runs every matching hook in parallel and passes event data as JSON on stdin. Your hook communicates back through its exit code, stderr and stdout.
The most useful events:
| Event | When it fires | Typical use |
|---|---|---|
SessionStart | Session begins, resumes, clears or compacts | Inject dynamic context (recent commits, sprint notes) |
UserPromptSubmit | Before Claude processes your prompt | Add context, block prompts containing secrets |
PreToolUse | Before a tool call runs; can block it | Guardrails: protected files, dangerous commands |
PermissionRequest | When a tool call needs a permission decision | Auto-approve specific safe prompts |
PostToolUse | After a tool call succeeds | Format, lint, log |
Notification | Claude is waiting for input or permission | Desktop alerts |
SubagentStart / SubagentStop | A subagent spawns or finishes | Logging, per-agent setup |
Stop | Claude finishes responding | Verification gates (“are the tests green?”) |
PreCompact / PostCompact | Around context compaction | Save or restore state |
SessionEnd | Session terminates | Cleanup |
There are more (PostToolUseFailure, PostToolBatch, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, InstructionsLoaded, Elicitation and others). See the official reference for the full list.
Where hooks live
| Location | Scope | Shareable |
|---|---|---|
~/.claude/settings.json | All your projects | No |
.claude/settings.json | One project | Yes, commit it |
.claude/settings.local.json | One project | No (gitignored) |
| Managed policy settings | Organisation-wide | Admin-controlled |
Plugin hooks/hooks.json | When the plugin is enabled | Yes |
| Skill or subagent frontmatter | While that skill/subagent is active | Yes |
Run /hooks inside Claude Code to browse all configured hooks grouped by event. To turn hooks off, set "disableAllHooks": true in a settings file.
Anatomy of a hook
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
- The event name (
PostToolUse) is a key inside the singlehooksobject. If you already have hooks, add new events as siblings rather than replacing the object. - The matcher narrows when it fires. For tool events it matches the tool name:
Bash,Edit|Write, ormcp__.*for all MCP tools. ForSessionStartit matches how the session started (startup,resume,clear,compact,fork). Omit it (or use"") to match everything. - The handler has a
type. Most hooks usecommand. Others arehttp(POST the event JSON to a URL),mcp_tool(call an MCP tool),prompt(single-turn model judgment) andagent(a subagent that can use tools; experimental).
Input and output
A PreToolUse hook for a Bash call receives something like:
{
"session_id": "abc123",
"cwd": "/Users/sarah/myproject",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "npm test" }
}
Your hook then answers with an exit code:
- Exit 0: no objection. For
PreToolUse, the call still goes through the normal permission flow; exit 0 doesn’t approve anything. ForSessionStartandUserPromptSubmit, plain stdout is added to Claude’s context. - Exit 2: block. Write the reason to stderr; for blockable events Claude receives it as feedback and can adjust.
- Other codes: a non-blocking error on most events; the action proceeds.
For finer control, exit 0 and print JSON. A PreToolUse hook can deny with a reason, escalate to the user (ask) or skip the prompt (allow):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Use rg instead of grep for better performance"
}
}
When several hooks answer the same PreToolUse, the most restrictive decision wins (deny over defer over ask over allow). A hook’s deny holds even in bypassPermissions mode, but a hook’s allow can’t override deny rules in your settings.
Choose one approach per hook: exit 2 with stderr, or exit 0 with JSON.
Five hooks worth adding
1. Desktop notification when Claude needs you
Add to ~/.claude/settings.json (macOS):
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
On Linux, swap the command for notify-send 'Claude Code' 'Claude Code needs your attention' (it needs a desktop notification daemon, so it won’t work over plain SSH or in most containers).
2. Auto-format after every edit
The PostToolUse example above runs Prettier on each file Claude edits. It needs jq installed. Swap in black, gofmt, rustfmt or your formatter of choice. Note that the Edit|Write matcher doesn’t see files rewritten by Bash commands; use a FileChanged hook if that matters.
3. Block edits to protected files
Save as .claude/hooks/protect-files.sh and run chmod +x on it:
#!/bin/bash
# protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Normalize Windows backslash separators so the patterns below match
FILE_PATH="${FILE_PATH//\\//}"
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
fi
done
exit 0
Register it in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
Ask Claude to add a comment to .env to test it. Claude Code blocks the edit and passes the Blocked: message back to Claude. $CLAUDE_PROJECT_DIR keeps the path working regardless of the current directory.
4. Re-inject context after compaction
Compaction summarises the conversation and can drop details. A SessionStart hook with the compact matcher puts critical reminders back:
{
"hooks": {
"SessionStart": [
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "echo 'Reminder: use Bun, not npm. Run bun test before committing. Current sprint: auth refactor.'"
}
]
}
]
}
}
Replace the echo with something dynamic such as git log --oneline -5.
5. A verification gate before Claude stops
A prompt hook asks a model to judge whether the work is done. If it returns "ok": false, the reason is fed back and Claude keeps working:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
}
]
}
]
}
}
For a deterministic gate, use a command hook that runs your test suite and blocks the stop until it passes. When verification needs to read files or run commands, an agent hook can do it, though agent hooks are marked experimental.
Prompt, agent and HTTP hooks
prompthooks send the event data and your prompt to a Claude model, which returns{"ok": true}or{"ok": false, "reason": "…"}. Use them for judgment calls the input alone can answer. Default timeout: 30 seconds.agenthooks spawn a subagent with tool access (default 60-second timeout, up to 50 tool-use turns). Use them when verification depends on the real state of the codebase.httphooks POST the same JSON to an endpoint, useful for a shared audit service. Header values can interpolate environment variables, but only those listed inallowedEnvVars.
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "http",
"url": "http://localhost:8080/hooks/tool-use",
"headers": { "Authorization": "Bearer $MY_TOKEN" },
"allowedEnvVars": ["MY_TOKEN"]
}
]
}
]
}
}
Limits and gotchas
- Timeouts: command, HTTP and MCP-tool hooks default to 10 minutes (lower for some events: 30 seconds for
UserPromptSubmit). Settimeout(seconds) per hook. PostToolUsecan’t undo an action; the tool already ran. Block inPreToolUseinstead.Stopfires whenever Claude finishes responding, not only at task completion, and not on user interrupts.- Shell profile noise: if your
.zshrcor.bashrcprints text, it can corrupt JSON output. Keep profile output behind an interactive-shell check. additionalContextmust be nested insidehookSpecificOutput; at the top level it’s ignored.- Parallel execution: one hook’s
denydoesn’t stop sibling hooks from running, so don’t rely on it to suppress side effects elsewhere. - Security: hooks run with your user permissions. Review hooks in a cloned repo’s
.claude/settings.jsonand in any plugin before enabling them.
Writing hooks faster
You can ask Claude Code to write a hook for you: describe the behaviour (“block any Bash command containing rm -rf and log every command to ~/.claude/bash.log”) and review the result. Anthropic’s official marketplace also lists a hookify plugin aimed at creating hooks; see the plugins guide for installing from marketplaces.
Hooks in your Claude kit
Hooks are the enforcement layer of a Claude kit. A sensible split: facts in CLAUDE.md, procedures in skills, isolated work in subagents, external tools via MCP, and anything that must happen every time in hooks. Our skills vs subagents vs hooks guide walks through that decision. Anthropic’s best-practices guidance makes the same point: if Claude already does something correctly without an instruction, delete the instruction or convert it to a hook.
Frequently asked questions
What's the difference between a hook and an instruction in CLAUDE.md?
CLAUDE.md instructions are guidance the model may or may not follow. Hooks are deterministic: Claude Code runs them every time their event fires, regardless of what the model decides.
Can a hook block a tool call even in bypass permissions mode?
Yes. A PreToolUse hook that returns permissionDecision deny (or exits with code 2) blocks the tool in every permission mode, including bypassPermissions. A hook returning allow, however, cannot override deny rules from your settings.
Where should I put hooks so my team gets them?
Put them in .claude/settings.json in the repository and commit it, or ship them in a plugin's hooks/hooks.json. Personal hooks go in ~/.claude/settings.json; .claude/settings.local.json is for project hooks you don't want to commit.
Do hooks see changes Claude makes through Bash commands?
A PostToolUse hook matched on Edit|Write only sees the Edit and Write tools. If you need to catch every file change, add a Stop hook that scans the working tree once per turn, or use the FileChanged event.