Modern Code Consulting

Claude Code

Claude Code Hooks: Config, Examples and Exit Codes

Claude Code hooks explained: where they go in settings, three working examples, how exit codes block tool calls, and mistakes we avoid on real builds.

By
Published

Last verified

Claude Code hooks are shell commands (or HTTP calls, MCP tool calls, or model prompts) that Claude Code runs at set points in a session: before a tool call, after a file edit, when it needs your attention, when it stops. You register them in a settings file, match them to an event, and they fire every time. That is the point. A line in CLAUDE.md is a request that Claude may or may not honor, and a hook is code the harness executes.

This is a working guide to the hooks we actually use on builds: the config shape, three hooks you can paste in today, and the mistakes that cost us time. If you want a team rolled out on Claude Code with a shared set of these, that is part of our Claude Code setup and consulting work.

How a Claude Code hook is wired

Hooks nest three levels deep: the event, then a matcher group, then one or more handlers. The event says when. The matcher says which tool or notification type. The handler says what to run.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

That is the formatter example from Anthropic's own hooks guide. It runs after Claude edits or writes a file, pulls the path out of the JSON on stdin with jq, and hands it to Prettier. Matchers made only of letters, digits, underscores, hyphens, commas and pipes are treated as an exact name or list, so Edit|Write means those two tools. Any other character turns the matcher into a JavaScript regular expression, which is how you match a family such as mcp__memory__.*.

Where Claude Code hooks go

Hooks live in whichever settings file matches how widely you want them to apply:

  • ~/.claude/settings.json: every project on your machine. Good for notifications and personal logging.
  • .claude/settings.json: one project, committed, so the whole team gets the same guardrails.
  • .claude/settings.local.json: one project, gitignored. Good for a hook that only works with your local setup.

Plugins, skills, subagent frontmatter and managed policy settings can define hooks too. Type /hooks in a session to browse everything configured and see which file each entry came from. It is read-only, so you still edit the JSON by hand.

Three Claude Code hooks worth setting up first

1. Block edits to files Claude should never touch

This is the hook we would add to every repo. A PreToolUse hook on Edit|Write checks the target path and exits with code 2 to block the edit. Claude sees the stderr message and adjusts. This is adapted from the Anthropic guide:

#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(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
  fi
done
exit 0

Run chmod +x on the script, then register it with the command "$CLAUDE_PROJECT_DIR"/.claude/hooks/protect-files.sh so it resolves no matter which directory Claude is in. The instructions file can say "do not edit .env", but only the hook makes it impossible.

2. Format after every edit

The Prettier example above is the lowest-friction win. Claude stops spending turns arguing about quote style, and diffs stay clean. One limit from the docs: this fires on the Edit and Write tools, so a Bash command that rewrites a file will not trigger it. For that case the guide points you to the FileChanged event instead.

3. Get pinged when Claude needs you

A Notification hook fires when Claude is waiting for input or permission. On Linux the guide uses notify-send 'Claude Code' 'Claude Code needs your attention', and on macOS an osascript call. On a headless server or in a container there is usually no notification daemon, so test the command directly before blaming the hook.

Exit codes decide what happens

This is the part people get wrong. A hook's exit code is a control signal, not just pass or fail.

Exit codeWhat Claude Code does
0Success. If stdout is a single JSON object, it is parsed for decisions.
2Blocking error on events that can block. On PreToolUse the call stops and stderr becomes the reason Claude sees.
Anything elseNon-blocking error. The action proceeds and you get a one-line notice.

A protect-files script that exits 1 on a match looks like it works, because an error notice appears, but the edit still goes through. Exit 1 does not block. Use 2, or return JSON with permissionDecision set to deny, and test by asking Claude to do the exact thing you meant to prevent.

Claude Code hook events and types, as of November 12, 2026

This is the volatile section. The event list and hook types change as Claude Code ships, so check the official hooks reference before you build on any of it. As of November 12, 2026 the reference lists:

  • Tool events: PreToolUse, PermissionRequest, PermissionDenied, PostToolUse, PostToolUseFailure, PostToolBatch.
  • Session and prompt events: SessionStart, Setup, UserPromptSubmit, Notification, Stop, StopFailure, SessionEnd, PreCompact, PostCompact.
  • Agent and task events: SubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle.
  • Environment events: InstructionsLoaded, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, WorktreeRemove, among others.
  • Handler types: command, http, mcp_tool, prompt, and agent, which the docs mark experimental.
  • Default timeouts: 600 seconds for command, http and mcp_tool handlers, 30 seconds for prompt, 60 seconds for agent. UserPromptSubmit command hooks are lowered to 30 seconds, and SessionEnd hooks share a 1.5 second budget unless raised.
  • Input fields on stdin include session_id, transcript_path, cwd, permission_mode and hook_event_name. Tool events add tool_name, tool_input and tool_use_id.

Command hooks vs prompt and agent hooks

Use a command hook whenever the rule can be written as code: a path pattern, a banned command, a required file. It is fast, free and deterministic. A prompt hook sends the event to a Claude model for a single-turn yes or no, which fits judgment calls such as "does this change touch billing logic?" An agent hook can use tools to verify a condition, but it is marked experimental, slower, and it spends tokens on every fire.

Our rule: start with a command hook, and move to a prompt hook only when you catch yourself writing a regex that tries to understand English. Most rules never need the upgrade.

Mistakes we would avoid

  • Exiting 1 when you meant 2, covered above, is the easiest silent failure to ship.
  • A Stop hook with no loop guard. If your hook blocks Stop to make Claude keep going, read stop_hook_active and exit 0 when it is true. Claude Code cuts the loop after eight consecutive blocks, but you do not want to reach that cap on purpose.
  • Forgetting jq. Most examples parse stdin with jq. On a fresh server or container it may not exist, and the hook then errors without doing anything. Install it or parse with Python or Node.
  • Building JSON output by string concatenation. A quote in a file path breaks the payload. Build it with jq so values are escaped.
  • Treating a hook as a sandbox. A command hook is a shell command that runs as you. Read any hook someone hands you before adding it, the same way you would a script from the internet.

What we'd do

Put the protected-files hook and the formatter in .claude/settings.json and commit them, so every developer and every scripted run gets the same guardrails. Keep the notification hook in your personal settings, since it depends on your machine. Add further hooks only when a specific mistake repeats.

On our own setup, one Stop hook lints every drafted client message for phrases we never want to send, and bounces the reply once if it finds one. It exists because we kept catching the same mistakes by eye, which is the signal that a rule belongs in a hook. Skills carry the instructions for a workflow and hooks carry the rules that must hold. We cover the instruction side in our post on Claude Code skills.

Hooks matter most when Claude Code runs unattended, since nobody is watching to catch a bad edit. Our lead list personalization build is one of those scripted runs: a Claude Code skill does the judgment calls and Python does the exact work, and that split is where hook guardrails earn their keep.

If you want a shared hook set, instruction files and scripted runs set up on your team's repos, that is the Claude Code setup we offer.

Frequently asked questions

What are Claude Code hooks?
Claude Code hooks are user-defined handlers that Claude Code runs at fixed points in its lifecycle, such as before a tool call, after a file edit, or when it stops. The most common handler is a shell command that receives the event as JSON on stdin. Because the harness runs them, they happen every time, which is the difference from asking Claude to remember a rule in CLAUDE.md.
Where do Claude Code hooks go?
Hooks go in a settings file under a hooks key. Use ~/.claude/settings.json for all your projects, .claude/settings.json for one project you can commit, or .claude/settings.local.json for one project that stays gitignored. Plugins, skills, subagents and managed policy settings can also define hooks. The /hooks command shows a read-only list of what is configured and where each hook came from.
How do I block a tool call with a hook?
Run the hook on PreToolUse and either exit with code 2 or print JSON with permissionDecision set to deny. With exit code 2, whatever the script writes to stderr is fed back to Claude as the reason. With JSON, you return a hookSpecificOutput object containing hookEventName, permissionDecision and permissionDecisionReason. Exit code 2 on PostToolUse cannot undo anything, because the tool has already run.
Why is my Claude Code hook not firing?
The usual causes are a matcher that does not match the tool name, the hook sitting under the wrong event, a script that is not executable, or jq missing from the machine. Open /hooks to confirm the hook is registered and which file it came from, then run the command by hand with sample JSON piped in. The Claude Code hooks guide has a troubleshooting section that covers each of these.
Can a Claude Code hook make Claude keep working?
Yes, a Stop hook can block Claude from stopping and send it back to work. Claude Code overrides a Stop hook that blocks eight times in a row with no tool call in between, so the script should read stop_hook_active from its input and exit early when it is true. Without that check you can loop until the cap hits.

Want this built for your business?

Book a free 15-minute call. Tell us what's eating your team's hours and we'll tell you what we'd automate first, how we'd build it and what it would cost.

Book a free automation audit