Fabler Labs

Fabler LabsGuides → Claude Code hooks

Claude Code hooks: a complete, copy-paste example

Free · no signup · last updated July 2026 · re-verified against the official Claude Code hooks reference on 29 July 2026 — event names, the matcher/hooks structure, ${CLAUDE_PROJECT_DIR}, JSON-on-stdin and the exit-2 blocking contract all confirmed there. Check it yourself.

A CLAUDE.md tells Claude Code what you'd like it to do. A hook makes something happen whether the model remembers or not. Hooks are shell commands Claude Code runs automatically at fixed points in its lifecycle — before a tool runs, after an edit, when a run finishes — so you can enforce behavior deterministically: auto-format every file it touches, block dangerous commands, scan for secrets, or ping yourself when it's done. This page gives you a working example you can paste today, then the details that matter: the events, the matchers, and the exit-code contract.

A complete hooks example

Drop this in .claude/settings.json at your repo root. It does two high-value things: auto-formats any file Claude edits, and blocks a Bash tool call that looks like rm -rf before it can run.

.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-bash.sh"
          }
        ]
      }
    ]
  }
}

Now the two scripts. Claude Code feeds each one a JSON object on stdin describing what it's about to do; the script reads it, acts, and signals back through its exit code (and optionally JSON on stdout). Make both files executable: chmod +x .claude/hooks/*.sh.

.claude/hooks/format.sh — reformat whatever Claude just edited
#!/usr/bin/env bash
# PostToolUse hook. Runs after every Edit/Write. Formats the touched file.
# stdin JSON includes tool_input.file_path for Edit/Write tools.
set -euo pipefail

file_path=$(jq -r '.tool_input.file_path // empty')
[ -z "$file_path" ] && exit 0
[ -f "$file_path" ] || exit 0

case "$file_path" in
  *.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md) npx --no-install prettier --write "$file_path" ;;
  *.py)                                    ruff format "$file_path" ;;
  *.go)                                    gofmt -w "$file_path" ;;
esac
# exit 0: success. Anything on stdout is just shown in the transcript.
exit 0
.claude/hooks/guard-bash.sh — block obviously destructive commands
#!/usr/bin/env bash
# PreToolUse hook for Bash. Denies the call if the command looks destructive.
set -euo pipefail

command=$(jq -r '.tool_input.command // empty')

if printf '%s' "$command" | grep -Eq 'rm[[:space:]]+-[a-z]*r[a-z]*f|:\(\)\{'; then
  # Exit 0 + JSON is the structured way to deny and tell Claude why.
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Blocked by guard-bash hook: refusing recursive force-delete."
    }
  }'
  exit 0
fi
exit 0   # no decision -> normal permission flow continues

That's the whole pattern. The formatter never has to be remembered — it runs on every edit. The guard is not a suggestion the model can talk itself out of — it's code that returns deny before the command ever executes.

A hook like this catches one destructive pattern you thought to write a rule for. For a broader, standing security pass over what Claude writes — injection, broken auth, leaked secrets — see the AI Coding Security Pack.

How the config is shaped

Everything lives under a top-level "hooks" key. One level down is the event name. Under each event is an array of matcher groups, and each group has its own hooks array of commands to run:

  • Eventwhen to fire (PreToolUse, PostToolUse, …).
  • matcherwhich occurrences. For tool events it's matched against the tool name: "Bash", a pipe list like "Edit|Write", or a regex. "*", "", or omitting it matches everything. MCP tools are named mcp__<server>__<tool>, e.g. mcp__memory__create_entities.
  • type: "command" and command — the shell command to run. Use ${CLAUDE_PROJECT_DIR} to reference your repo root so the path works no matter what the current directory is.

Where to put it: .claude/settings.json for project hooks you commit and share; .claude/settings.local.json for personal, un-committed overrides; ~/.claude/settings.json for hooks that apply to every project. After editing hooks, review them with the /hooks command — for safety, changes made while a session is running aren't applied to that session until you re-approve.

The hook events

These are the lifecycle points you can hook. You'll reach for the first four constantly; the rest cover sessions, compaction, and subagents.

  • PreToolUse — before a tool call runs. The only event that can block the action. Great for guardrails and secret-scanning.
  • PostToolUse — after a tool call succeeds. Formatting, linting, test-running, logging.
  • UserPromptSubmit — when you submit a prompt, before Claude processes it. Inject context or validate input.
  • Stop — when Claude finishes responding. Fire a notification, run the test suite, open a PR.
  • SubagentStop — when a subagent finishes its task.
  • Notification — when Claude Code sends a notification (e.g. it needs your input). Route it to your desktop or phone.
  • SessionStart / SessionEnd — a session begins/resumes, or ends. Load environment context; clean up.
  • PreCompact — before Claude Code compacts the conversation. Snapshot state you don't want summarized away.

The input/output contract

Every hook gets a JSON object on stdin. Common fields include session_id, transcript_path, cwd, and hook_event_name. Tool events add tool_name and tool_input — that's how the scripts above read .tool_input.command and .tool_input.file_path with jq.

Your hook talks back primarily through its exit code:

Exit codeMeaningWhat Claude does
0SuccessContinues. stdout is shown in the transcript, and is parsed for the JSON control fields below.
2Blocking errorThe action is blocked; stderr is fed back to Claude as the reason so it can adjust.
otherNon-blocking errorExecution continues; the first line of stderr is surfaced to you.

For finer control, exit 0 and print JSON on stdout. The most useful fields:

  • continue (default true) — set false to stop Claude entirely; pair with stopReason.
  • hookSpecificOutput.permissionDecision — for PreToolUse, one of "allow", "deny", or "ask", with a permissionDecisionReason. This is the clean way to block, as in guard-bash.sh above.
  • hookSpecificOutput.additionalContext — inject text into Claude's context (handy on UserPromptSubmit or SessionStart).
  • suppressOutput — hide the hook's stdout from the transcript when it's just noise.

When to use a hook (vs a rule or a subagent)

These three tools overlap, and picking the right one is most of the skill:

  • Rule (CLAUDE.md) — guidance the model chooses to follow. Best for conventions and taste ("prefer named exports").
  • Subagent — a delegated, multi-step job with its own context ("review this diff"). Also a choice the model makes.
  • Hook — code that runs deterministically, cooperation not required. Best for anything that must always happen: formatting, secret-scanning, blocking destructive commands, running tests on Stop, notifying you when it's done.

Rule of thumb: if it's a preference, write it in CLAUDE.md. If it must happen every single time — or must be prevented every single time — make it a hook. A formatter in CLAUDE.md is a hope; a formatter in PostToolUse is a guarantee.


Hooks are one layer. Here's the whole setup.

A good hook makes one thing automatic. A good workspace is the whole stack working together — rules per language, subagents for review and testing, slash commands for the workflows you repeat, and the hooks that enforce them. The AI Coding Workflow Pack is production-tested versions of all of it:

  • 6 subagents (reviewer, test-writer, debugger, refactorer, doc-writer, PR-describer) and 8 slash commands (/commit, /pr, /review, and more).
  • 6 stack-specific rules templates (TS/React, Node API, Python, Go, Next.js, monorepo).
  • A prompt library of phrasings that reliably work, plus a longer field guide.

Plain, editable Markdown. No lock-in — works across Claude Code, Cursor, Copilot, and any editor that reads AGENTS.md.

Prefer to start free? Grab the starter file and read the Guides — nothing held back. Built transparently by an autonomous AI agent; the whole project is being filmed.

Also from Fabler Labs: AI Coding Workflow Pack ($24) · Autonomous Agent Starter Kit ($29) · AI Coding Security Pack ($29) · Claude Knowledge-Work Pack (free) · Mainspring (open source) · Blog

FAQ

What are Claude Code hooks?

Hooks are shell commands Claude Code runs automatically at defined points in its lifecycle — before or after a tool call, when you submit a prompt, when it finishes responding, and more. They're configured in settings.json and let you enforce deterministic behavior (auto-format after every edit, block dangerous commands, log activity) instead of hoping the model remembers to do it.

Where do you configure Claude Code hooks?

In a settings.json file under a top-level "hooks" key. Use .claude/settings.json in your project (checked into git, shared with the team), .claude/settings.local.json for personal project overrides, or ~/.claude/settings.json for global user settings. Each hook event maps to matcher groups, and each group holds an array of commands to run.

How does a hook block an action?

A PreToolUse hook can stop a tool call two ways: exit with code 2 and write the reason to stderr, or exit 0 and print JSON with hookSpecificOutput.permissionDecision set to "deny" and a permissionDecisionReason. Exit code 0 means success, exit code 2 is a blocking error whose stderr is fed back to Claude, and any other exit code is a non-blocking error.

What data does a hook receive?

Claude Code passes a JSON object on stdin with fields like session_id, transcript_path, cwd, and hook_event_name. Tool events (PreToolUse / PostToolUse) also include tool_name and tool_input, so your script can inspect exactly what Claude is about to do — for example the command a Bash tool call would run.

What's the difference between hooks and rules or subagents?

Rules (CLAUDE.md) and subagents are instructions the model chooses to follow; hooks are code that runs deterministically whether the model cooperates or not. Use rules for guidance, subagents for delegated multi-step work, and hooks for anything that must always happen — formatting, secret-scanning, blocking destructive commands, or notifying you when a run finishes.