Skip to content
Claudekit.ai

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:

EventWhen it firesTypical use
SessionStartSession begins, resumes, clears or compactsInject dynamic context (recent commits, sprint notes)
UserPromptSubmitBefore Claude processes your promptAdd context, block prompts containing secrets
PreToolUseBefore a tool call runs; can block itGuardrails: protected files, dangerous commands
PermissionRequestWhen a tool call needs a permission decisionAuto-approve specific safe prompts
PostToolUseAfter a tool call succeedsFormat, lint, log
NotificationClaude is waiting for input or permissionDesktop alerts
SubagentStart / SubagentStopA subagent spawns or finishesLogging, per-agent setup
StopClaude finishes respondingVerification gates (“are the tests green?”)
PreCompact / PostCompactAround context compactionSave or restore state
SessionEndSession terminatesCleanup

There are more (PostToolUseFailure, PostToolBatch, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, InstructionsLoaded, Elicitation and others). See the official reference for the full list.

Where hooks live

LocationScopeShareable
~/.claude/settings.jsonAll your projectsNo
.claude/settings.jsonOne projectYes, commit it
.claude/settings.local.jsonOne projectNo (gitignored)
Managed policy settingsOrganisation-wideAdmin-controlled
Plugin hooks/hooks.jsonWhen the plugin is enabledYes
Skill or subagent frontmatterWhile that skill/subagent is activeYes

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 single hooks object. 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, or mcp__.* for all MCP tools. For SessionStart it matches how the session started (startup, resume, clear, compact, fork). Omit it (or use "") to match everything.
  • The handler has a type. Most hooks use command. Others are http (POST the event JSON to a URL), mcp_tool (call an MCP tool), prompt (single-turn model judgment) and agent (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. For SessionStart and UserPromptSubmit, 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

  • prompt hooks 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.
  • agent hooks 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.
  • http hooks POST the same JSON to an endpoint, useful for a shared audit service. Header values can interpolate environment variables, but only those listed in allowedEnvVars.
{
  "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). Set timeout (seconds) per hook.
  • PostToolUse can’t undo an action; the tool already ran. Block in PreToolUse instead.
  • Stop fires whenever Claude finishes responding, not only at task completion, and not on user interrupts.
  • Shell profile noise: if your .zshrc or .bashrc prints text, it can corrupt JSON output. Keep profile output behind an interactive-shell check.
  • additionalContext must be nested inside hookSpecificOutput; at the top level it’s ignored.
  • Parallel execution: one hook’s deny doesn’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.json and 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.

Get the weekly Claude digest

New skills, MCP servers, prompts and Claude updates — one short email a week. No spam, unsubscribe anytime.

The newsletter is launching soon. Follow the RSS feed in the meantime.