Claude Code hooks are shell commands the harness runs automatically on events — before or after a tool runs, when a session starts, or when you submit a prompt. They let you enforce policies and automate your workflow deterministically, outside the model’s judgment. Here’s every event, copy-paste recipes, and when to reach for a hook.
Each event fires at a specific point and receives event-specific JSON on stdin.
PreToolUseBefore a tool runs. Can allow, deny, or ask, the deterministic place to block risky actions.
PostToolUseAfter a tool completes. Add advisory context or run follow-ups like format-on-save or tests.
UserPromptSubmitWhen you submit a prompt. Inject context or block the prompt before Claude sees it.
SessionStartAt session start, resume, or clear. Inject repo and branch context into the conversation.
SessionEndWhen a session ends. Clean up temporary state or log the session.
StopWhen the main agent finishes responding. Common for “run the test suite on stop”.
SubagentStopWhen a spawned subagent finishes its work.
PreCompactJust before Claude Code compacts the context window.
NotificationOn notifications, for example when Claude needs permission or goes idle.
Every hook is an entry under an event name in your settings.json. Tool events take a matcher (a regex over tool names like Write|Edit); event-only hooks like Stop omit it and always fire.
{
"hooks": {
"<EventName>": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "your-script.sh" }
]
}
]
}
}The command receives the event JSON on stdin (read a field with jq -r '.tool_input.file_path'). Exit 0 to continue; on a PreToolUse hook, exit 2 to block the action and send your stderr back to Claude as the reason.
Six hooks worth stealing, each a complete block you can drop straight into your settings.json. Have more than one? Merge them under a single "hooks" key.
PostToolUseRuns Prettier on whatever file a Write or Edit touched, so the tree stays formatted without you asking. The file path arrives on stdin as tool_input.file_path.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}StopFires once the main agent finishes responding. A fast way to catch a regression before you even read the diff. Stop takes no matcher, it always fires.
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "npm test --silent"
}
]
}
]
}
}PreToolUseBlocks any Write or Edit whose path contains .env and tells Claude why. Exit code 2 on a PreToolUse hook is a hard deny the model cannot override, and the message on stderr is fed back so it understands the refusal.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "f=$(jq -r '.tool_input.file_path'); case $f in *.env|*.env.*) echo 'Refusing to modify .env files.' >&2; exit 2;; esac"
}
]
}
]
}
}PreToolUseInspects the Bash command on stdin and blocks rm -rf (and rm -fr) with exit 2. Everything else passes straight through. A safety net for the one command you never want run unattended.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' | grep -qE 'rm -(rf|fr)' && { echo 'Blocked a recursive force-delete. Run it yourself if you really mean it.' >&2; exit 2; } || exit 0"
}
]
}
]
}
}SessionStartPrints the current branch and how many files are uncommitted. A SessionStart hook that exits 0 has its stdout injected straight into the conversation, so Claude starts every session knowing where the repo stands.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo Branch $(git branch --show-current 2>/dev/null) has $(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') uncommitted files"
}
]
}
]
}
}NotificationSurfaces a native notification when Claude asks for permission or goes idle. The event type arrives on stdin as notification_type. macOS shown; on Linux swap terminal-notifier for notify-send.
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "terminal-notifier -title 'Claude Code' -message $(jq -r .notification_type) 2>/dev/null || true"
}
]
}
]
}
}The three ways to change Claude Code’s behavior are not interchangeable:
Full breakdown in the anatomy of a 2026 Claude Code plugin.
See advisory, fail-safe hooks in action:
Hooks are shell commands the Claude Code harness runs automatically on events, before or after a tool runs, at session start, when you submit a prompt, and more. Because the harness runs them (not the model), they fire deterministically every time, which makes them ideal for enforcing policies and automating your workflow.
PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop, SubagentStop, PreCompact, and Notification. Each event carries event-specific JSON on stdin, the tool name, tool input and output, the prompt, or session info.
Add a hooks block to your settings.json (user or project scope), or ship a hooks/hooks.json file inside a plugin. Each entry has a matcher (for example "Edit|Write") and a command to run. Plugins reference bundled scripts with ${CLAUDE_PLUGIN_ROOT} so they resolve wherever the plugin is installed.
Use a PostToolUse hook matched to "Write|Edit" that reads the edited path from stdin (jq -r '.tool_input.file_path') and pipes it to your formatter, for example xargs npx prettier --write. It runs after every edit, so the tree stays formatted without you asking. There is a copy-paste version in the recipes above.
Use a PreToolUse hook matched to "Write|Edit" that checks the file path on stdin and exits with code 2 when it matches a file you want protected. Exit 2 is a hard deny the model cannot override, and the message you print to stderr is fed back so Claude understands the refusal. See the "Never let Claude touch your .env" recipe.
Add a Stop hook (no matcher, it always fires) whose command runs your test script, for example npm test --silent. It runs the moment the main agent stops responding, so a regression surfaces before you read the diff.
Yes. A PreToolUse hook can deny a tool (exit code 2, or a permissionDecision of "deny"), which is how you enforce hard rules like “never write outside the repo” or “never commit a secret”. Exit 0 with additionalContext is advisory, it surfaces a suggestion to Claude without blocking.
In hooks/hooks.json at the plugin root, which Claude Code auto-detects. The command references bundled scripts via ${CLAUDE_PLUGIN_ROOT} so they resolve to wherever the plugin is installed.
Hooks run arbitrary commands with your permissions, so only install hooks you trust. Well-built plugin hooks are fail-safe (any error exits 0 and does nothing) and advisory by default, so they surface a suggestion without ever blocking your work or breaking a session.
A well-written hook is near-instant. Keep hook commands fast and fail-safe; heavy or long-running work belongs in a slash command or a subagent, not a hook.