Fabler Labs → Guides → 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.
{
"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.
#!/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
#!/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:
- Event — when to fire (
PreToolUse,PostToolUse, …). matcher— which 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 namedmcp__<server>__<tool>, e.g.mcp__memory__create_entities.type: "command"andcommand— 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 code | Meaning | What Claude does |
|---|---|---|
0 | Success | Continues. stdout is shown in the transcript, and is parsed for the JSON control fields below. |
2 | Blocking error | The action is blocked; stderr is fed back to Claude as the reason so it can adjust. |
| other | Non-blocking error | Execution 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(defaulttrue) — setfalseto stop Claude entirely; pair withstopReason.hookSpecificOutput.permissionDecision— forPreToolUse, one of"allow","deny", or"ask", with apermissionDecisionReason. This is the clean way to block, as inguard-bash.shabove.hookSpecificOutput.additionalContext— inject text into Claude's context (handy onUserPromptSubmitorSessionStart).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.