Claude Code hooks: events, config and 7 recipes you can copy

Claude Code hooks: events, config and 7 recipes you can copy

Claude Code hooks are shell commands (or HTTP calls, or model checks) that Claude Code runs automatically at fixed points, such as before a tool call or when Claude finishes a turn. This guide explains the events, the settings format, exit codes and JSON output, then gives seven recipes you can paste into .claude/settings.json today.

In shortTL;DR
  • Hooks are deterministic: they run every time an event fires, unlike CLAUDE.md instructions, which Claude may or may not follow.
  • Configure hooks in the hooks object of ~/.claude/settings.json, .claude/settings.json or .claude/settings.local.json, then run /hooks to confirm they loaded.
  • PreToolUse with exit code 2 blocks a tool call and sends your stderr message to Claude; PostToolUse runs after the tool and can't undo it.
  • A Stop hook that exits 2 keeps Claude working, which turns "tests must pass" into a hard gate; check stop_hook_active to avoid loops.
  • Hooks run with your user's permissions, so review every hook script in a repository before you trust it.

What are Claude Code hooks?

Claude Code hooks are user-defined commands that Claude Code runs automatically at specific points in a session: when it starts, before and after each tool call, when Claude needs your input, when it finishes a turn and more. The hooks guide describes the point of them as "deterministic control: certain actions always happen rather than relying on the LLM to choose to run them."

That is the key difference from instructions. A line in CLAUDE.md saying "run Prettier after editing" is a request Claude may forget. A PostToolUse hook that runs Prettier after every edit runs every time. Anthropic's best practices put it this way: CLAUDE.md instructions are advisory, hooks are deterministic. Our guide to CLAUDE.md best practices covers the advisory side.

Typical uses:

  • Format or lint every file Claude edits.
  • Block edits to .env, lockfiles or migrations.
  • Block or log risky shell commands.
  • Send a desktop notification when Claude is waiting for you.
  • Re-inject key context after the conversation is compacted.
  • Refuse to let Claude stop until the tests pass.

Which hook events does Claude Code support?

Claude Code fires more than 30 hook events, but most useful hooks use six of them. The full list lives in the hooks reference; these are the ones to learn first.

EventWhen it firesCan it block?Common use
SessionStartA session begins, resumes, or restarts after /clear or compactionNoInject context, load environment
UserPromptSubmitYou submit a prompt, before Claude sees itYesAdd context, reject prompts with secrets
PreToolUseBefore a tool call runsYesBlock commands or protected files
PostToolUseAfter a tool call succeedsNo (already ran)Format, lint, log
NotificationClaude Code sends a notification, such as a permission promptNoDesktop or phone alerts
StopClaude finishes respondingYesRun tests, refuse to stop until they pass

Other events worth knowing: PreCompact and PostCompact around compaction, SubagentStart and SubagentStop for subagents, InstructionsLoaded when a CLAUDE.md or rules file loads (handy for debugging), FileChanged for watched files, and SessionEnd.

How do you configure a hook?

You configure hooks in a hooks object inside a Claude Code settings file. Each event name maps to a list of groups; each group has an optional matcher and a list of handlers.

.claude/settings.json
{  "hooks": {    "PostToolUse": [      {        "matcher": "Edit|Write",        "hooks": [          {            "type": "command",            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"          }        ]      }    ]  }}

Where the file lives decides who gets the hook:

LocationScopeCommitted?
~/.claude/settings.jsonAll your projectsNo
.claude/settings.jsonThis project, whole teamYes
.claude/settings.local.jsonThis project, only youNo
Managed policy settingsOrganization-wideAdmin-controlled

Hooks can also ship inside plugins, skills and subagent definitions. Run /hooks inside Claude Code to browse every configured hook grouped by event. Edits to settings files are normally picked up automatically; restart the session if a change doesn't show up.

Matchers

A matcher narrows when a hook fires. For tool events it matches the tool name: Bash, Edit|Write, or a regex such as mcp__github__.* for every tool on one MCP server. For SessionStart it matches how the session started (startup, resume, clear, compact), and for Notification the notification type, such as permission_prompt or idle_prompt. An empty matcher fires on everything. Matchers are case-sensitive.

For finer control on tool events, the if field uses permission-rule syntax, so "if": "Bash(git *)" runs the hook only for git commands instead of every shell command.

How do hooks read input and return decisions?

A command hook receives the event as JSON on stdin and answers with an exit code, stderr and optionally JSON on stdout. For a shell command, a PreToolUse hook receives something like this:

stdin for PreToolUse
{  "session_id": "abc123",  "cwd": "/Users/sarah/myproject",  "hook_event_name": "PreToolUse",  "tool_name": "Bash",  "tool_input": { "command": "npm test" }}

Exit codes carry the basic decision:

Exit codeMeaning
0No objection. For PreToolUse this does not approve the call; the normal permission flow still applies. For SessionStart and UserPromptSubmit, plain stdout is added to Claude's context.
2Block, where the event allows it. Write the reason to stderr. On PreToolUse the tool call is blocked and Claude sees the reason; on Stop Claude keeps working.
Anything elseA non-blocking error on most events. The action proceeds and a hook error notice appears.

For richer decisions, exit 0 and print JSON. A PreToolUse hook can deny with a reason, escalate to the normal prompt with "ask", or skip the prompt with "allow":

stdout
{  "hookSpecificOutput": {    "hookEventName": "PreToolUse",    "permissionDecision": "deny",    "permissionDecisionReason": "Use rg instead of grep"  }}

Two rules from the docs that save debugging time: pick either exit 2 or JSON per hook, not both, and keep fields like permissionDecision and additionalContext inside hookSpecificOutput, because misplaced fields are silently ignored.

Seven Claude Code hook recipes you can copy

These recipes come from or build on the official examples. The Bash scripts use jq to parse JSON, so install it first (brew install jq or apt-get install jq).

1. Get notified when Claude needs you

Put this in ~/.claude/settings.json so it works in every project. This is the Linux version; on macOS replace the command with osascript -e 'display notification "Claude Code needs your attention" with title "Claude Code"'.

~/.claude/settings.json
{  "hooks": {    "Notification": [      {        "matcher": "",        "hooks": [          { "type": "command", "command": "notify-send 'Claude Code' 'Claude Code needs your attention'" }        ]      }    ]  }}

Set the matcher to permission_prompt to be pinged only when Claude is waiting for approval.

2. Format every file Claude edits

The PostToolUse example in the configuration section above runs Prettier on each file Claude writes. Swap in ruff format, gofmt -w or rustfmt for other languages. Claude can also change files through shell commands, which Edit|Write does not catch; the docs suggest a Stop hook that scans the working tree if you need full coverage.

3. Protect sensitive files

Save this script as .claude/hooks/protect-files.sh and run chmod +x on it:

.claude/hooks/protect-files.sh
#!/bin/bashINPUT=$(cat)FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")for pattern in "${PROTECTED_PATTERNS[@]}"; do  if [[ "$FILE_PATH" == *"$pattern"* ]]; then    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2    exit 2  fidoneexit 0

Register it before every edit:

.claude/settings.json
{  "hooks": {    "PreToolUse": [      {        "matcher": "Edit|Write",        "hooks": [          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh" }        ]      }    ]  }}

$CLAUDE_PROJECT_DIR points at the project root, so the path works from any subdirectory.

4. Block dangerous shell commands

The same pattern works for Bash. This script stops rm -rf and dropping tables; Claude receives the message and tries another approach.

.claude/hooks/block-dangerous.sh
#!/bin/bashCOMMAND=$(jq -r '.tool_input.command')if echo "$COMMAND" | grep -qiE 'rm -rf|drop table'; then  echo "Blocked: destructive command. Ask the user first." >&2  exit 2fiexit 0

Register it under PreToolUse with "matcher": "Bash". A hook deny holds in every permission mode, including --dangerously-skip-permissions.

Warning

Pattern-matching shell commands is best-effort; a determined command can be phrased in ways your regex misses. Use permission deny rules and a sandboxed environment as the real boundary, and hooks as an extra layer.

5. Log every command Claude runs

An audit trail is useful for unattended runs:

.claude/settings.json
{  "hooks": {    "PostToolUse": [      {        "matcher": "Bash",        "hooks": [          { "type": "command", "command": "jq -r '.tool_input.command' >> ~/.claude/command-log.txt" }        ]      }    ]  }}

6. Re-inject context after compaction

When a long session is compacted, details can drop out. A SessionStart hook with the compact matcher adds text back, because stdout from SessionStart goes into Claude's context:

.claude/settings.json
{  "hooks": {    "SessionStart": [      {        "matcher": "compact",        "hooks": [          { "type": "command", "command": "echo 'Reminder: use pnpm, not npm. Run pnpm test before committing. Current task: auth refactor.'" }        ]      }    ]  }}

7. Don't let Claude stop until tests pass

A Stop hook that exits 2 keeps Claude working and shows it your stderr. Claude Code overrides a Stop hook after eight consecutive blocks without a tool call in between, and the input includes stop_hook_active so you can avoid loops:

.claude/hooks/require-tests.sh
#!/bin/bashINPUT=$(cat)if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then  exit 0fiif ! npm test --silent > /tmp/claude-test.log 2>&1; then  echo "Tests are failing. Fix them before finishing:" >&2  tail -n 30 /tmp/claude-test.log >&2  exit 2fiexit 0

Register it under "Stop" (no matcher needed). Keep the test command fast; a slow suite runs at the end of every turn, and Stop fires whenever Claude finishes responding, not only at task completion.

What are prompt, agent and HTTP hooks?

Besides shell commands, Claude Code supports hooks that ask a model or call a URL. Use them when a decision needs judgment or when logic lives on a server.

TypeWhat it doesDefault timeout
commandRuns a shell command10 minutes
httpPOSTs the event JSON to a URL10 minutes
promptSends the event to a Claude model for a single yes or no decision30 seconds
agentSpawns a subagent that can read files and run tools before deciding (experimental)60 seconds

A prompt hook on Stop can ask "are all requested tasks complete?" and return {"ok": false, "reason": "..."} to send Claude back to work. An HTTP hook suits a shared audit service that logs tool use for a whole team. Override any timeout with the timeout field in seconds.

What are the common gotchas with Claude Code hooks?

Most hook problems come from five causes: the matcher, the script, the JSON shape, loops, and trusting a hook you didn't write.

  • Hook never fires. Run /hooks and check the event and matcher. Remember PreToolUse runs before the tool and PostToolUse after.
  • "command not found". Use "$CLAUDE_PROJECT_DIR" or absolute paths, make scripts executable, and install jq.
  • Test a script by hand. Pipe sample input in and check the exit code: echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?.
  • JSON ignored with no error. Your shell profile may print text before the JSON, or a field sits outside hookSpecificOutput. Run claude --debug and search the log.
  • Hooks run in parallel. When several hooks match, all run to completion, and for PreToolUse the most restrictive decision wins. Don't rely on one hook's deny to stop another hook's side effects.
  • PostToolUse can't undo. The tool already ran. Use PreToolUse to prevent.
  • Security. Hooks execute shell commands with your user's permissions. A cloned repository can ship hooks in .claude/settings.json, so read them before you trust the project. To switch everything off, set "disableAllHooks": true.

Hooks matter most when you are not watching. If agents run for hours on a server while you sleep, a protected-files hook, a command log and a tests-must-pass Stop hook are what keep the morning review short. Orkio runs Claude Code on an always-on, isolated cloud box, and the hooks committed in your repository's .claude/settings.json come along with the project. For the CI side of automation, see Claude Code GitHub Actions, and for running unattended work well, our guide to background coding agents.

How to add your first hooks today

Start with three hooks and grow from real problems:

  1. Add the notification hook to ~/.claude/settings.json so you stop watching the terminal.
  2. Add the formatter hook to the project's .claude/settings.json and commit it.
  3. Add the protect-files hook for .env and lockfiles.
  4. Run /hooks to confirm all three loaded, then trigger each once to see it work.
  5. When Claude repeats a mistake that a script could catch, turn the CLAUDE.md rule into a hook.
  6. Before running agents unattended, add the command log and the tests-must-pass Stop hook.

Frequently asked questions

What is the difference between Claude Code hooks and CLAUDE.md?

CLAUDE.md is context that Claude reads and tries to follow, with no guarantee. Hooks are commands the Claude Code client runs at fixed lifecycle events whatever the model decides. Use CLAUDE.md for conventions and hooks for anything that must happen every time.

Where do I put Claude Code hooks?

Put personal hooks in ~/.claude/settings.json, team hooks in the project's .claude/settings.json so they are committed, and private project hooks in .claude/settings.local.json. Hooks can also come from managed policy settings, plugins, skills and subagent definitions.

Can a hook approve a command automatically?

A PreToolUse hook can return a JSON permissionDecision of allow to skip the permission prompt, but deny rules from settings still apply. A deny from a hook blocks the tool in every permission mode, including bypassPermissions.

Why is my Claude Code hook not running?

Run /hooks and check that it appears under the right event, then check that the matcher matches the tool name exactly, since matchers are case-sensitive. Test the script by piping sample JSON into it, make sure it is executable, and check the settings file for invalid JSON such as trailing commas.

Do hooks work in headless mode with claude -p?

Yes. Hooks from your settings files run in non-interactive mode too, which makes them useful for scripted and unattended runs. PermissionRequest hooks behave differently there, so read the limitations in the docs before relying on them in automation.

SOURCES

  1. Claude Code docs: Automate actions with hooks
  2. Claude Code docs: Hooks reference
  3. Claude Code docs: Best practices
  4. Claude Code docs: How Claude remembers your project
  • #claude code
  • #hooks
  • #automation
  • #ai coding
Orkio team

The team building Orkio. We write about putting AI to work: coding agents that run overnight, always-on boxes and pull requests ready by morning.

Keep reading

All posts

07:06 · YOU MERGED #128

Go to sleep. Wake up to pull requests.

Start for $19 a month
© 2026 Orkio · Hosted in Europe TürkçeStatus