Claude Code Hooks: The Complete Guide (with Examples)
Engineering
Claude Code Hooks: The Complete Guide (with Examples)
Hooks are how you make Claude Code do your thing automatically at the right moment — run a formatter after every edit, block a dangerous command before it runs, ping you when a turn finishes. A hook is just a shell command Claude Code runs when a lifecycle event fires, with the event data piped in on stdin. Here's the whole model, with copy-paste examples.
What a hook is
A hook is a command wired to an event. When the event fires, Claude Code runs your command, pipes a JSON payload to its stdin, and reads its exit code (and optionally JSON from stdout) to decide what happens next. Hooks live in settings.json (user, project, or local), so they're deterministic automation that doesn't depend on the model remembering to do anything.
The events you'll actually use
Claude Code fires hooks on a lot of events; the ones that matter day to day:
SessionStart— a session begins or resumes.UserPromptSubmit— you submit a prompt (can inject context, or block it).PreToolUse— before a tool runs (can block it — e.g. deny a dangerous command).PostToolUse— after a tool succeeds (e.g. format the file that was just edited).Stop— Claude finishes responding (can notify you, or block stopping to keep it working).Notification— Claude needs your attention (a permission prompt, or idle-waiting for input).SubagentStop— a subagent finishes.PreCompact/SessionEnd— before compaction / at session end.
(There are more — PostToolUseFailure, PermissionRequest, and others — but those cover almost everything.)
Configuring a hook
Hooks go in settings.json under hooks, keyed by event. Tool events (PreToolUse / PostToolUse) take a matcher (the tool name or a regex); other events don't. Here's "format the file that was just edited":
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs -r prettier --write" }
]
}
]
}
}
Matchers are case-sensitive (Bash, not bash) and accept regex (mcp__.*).
What your hook receives (stdin)
Every hook gets a JSON object on stdin. The common fields:
{
"session_id": "abc123",
"transcript_path": "/.../transcript.jsonl",
"cwd": "/home/you/my-project",
"hook_event_name": "PreToolUse"
}
Tool events add tool_name and tool_input (e.g. { "command": "npm test" } for Bash, { "file_path": "..." } for Edit). Stop adds last_assistant_message and stop_hook_active. Notification adds message and notification_type.
How your hook responds (exit codes + JSON)
Two ways to talk back:
- Exit code 0 — success. If you print JSON to stdout, Claude Code parses it for a decision or extra context.
- Exit code 2 — block the action and show your stderr to Claude. This only blocks on events that can be blocked:
PreToolUse,UserPromptSubmit,Stop(and a few others). OnPostToolUsethe tool already ran, so exit 2 just surfaces a message. - Any other code — non-blocking error; the first line of stderr goes to the transcript.
The richer path is JSON on stdout. A PreToolUse hook can deny a tool outright:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Blocked: rm -rf on a protected path"
}
}
A UserPromptSubmit or Stop hook can inject text into Claude's context with additionalContext.
Example: block a dangerous command
A PreToolUse hook on Bash that refuses rm -rf / — exit 2 blocks the tool and the reason goes to Claude:
#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$CMD" | grep -qiE 'rm -rf /'; then
echo "Refusing: destructive rm" >&2
exit 2
fi
exit 0
Example: don't let it stop until tests pass
A Stop hook that blocks stopping while tests fail — with the critical loop guard (stop_hook_active) so it can't get stuck:
#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then exit 0; fi
if npm test >/dev/null 2>&1; then
exit 0
else
echo '{"decision":"block","reason":"Tests are failing — fix them before finishing."}'
exit 0
fi
Without the stop_hook_active check, a blocking Stop hook can loop; Claude Code overrides it after several consecutive blocks, but you should guard it yourself.
Bundling hooks as a plugin
For hooks you want to reuse across machines or share, wrap them in a plugin: a .claude-plugin/plugin.json manifest plus a hooks/hooks.json, referencing your scripts with ${CLAUDE_PLUGIN_ROOT}. Then install it from a marketplace with /plugin install name@marketplace instead of hand-editing settings.json on every machine.
A hook you'll actually keep
The single most useful starter hook: get pinged when Claude finishes or needs you, so you can tab away. We packaged exactly that as a free, standalone plugin — see Get a desktop notification when Claude Code finishes. And when you're running several sessions at once, Mwah shows them all as robots on your desktop, flagged the moment one goes stuck.
Related: Get notified when Claude Code finishes · Run multiple Claude Code sessions
Keep reading
View all dispatches →Shopify AI Agent Tools: What Your Agents Can Actually Do in Your Store
Clearly now ships Shopify store tools your AI agents can call — browse the catalog, read orders, roll up revenue, edit products, and attach generated images — over MCP and the beehaven CLI, with a dry-run preview on every write. Here are the tools, with example commands.
How to Manage Your Shopify Store with AI Agents
AI agents can now do the work of running a Shopify store — writing descriptions, generating product images, answering customers, updating the catalog. Here's how agent-run commerce works, and how to set it up.
Claude Code for Shopify: Run Your Store from Your Terminal
If you already run your codebase with Claude Code, the same agents can run your Shopify store — edit the catalog, generate product images, answer customers — with a connector and the right guardrails. Here's the setup.