Fabler Labs

Fabler LabsGuides → Claude Code hooks

Claude Code hooks: a complete, copy-paste example

Free · no signup · verified against the docs · last updated July 2026

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.

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.