Kipt

Claude Code hooks: events, config and copy-ready examples

Claude Code hooks run your commands at fixed points, like before a tool call. All 33 events, the five hook types, exit codes and six copy-ready examples.

By the Kipt team · Updated 23 October 2026

What are Claude Code hooks?

Claude Code hooks are handlers that Claude Code runs automatically at specific points in a session: before a tool runs, after a file is edited, when Claude needs your input, when it stops. A hook can be a shell command, an HTTP endpoint, an MCP tool call, an LLM prompt or a subagent. You define hooks in a settings file, and they fire every time their event happens.

That is the key difference from instructions. A line in CLAUDE.md like "never edit .env" is a request Claude usually follows. A PreToolUse hook that blocks the edit is enforcement. Use hooks for anything that must happen the same way every time: formatting, guardrails, notifications, test runs.

The copy-ready examples come first below, followed by the full list of 33 events, the five hook types and the reference details.

Where do Claude Code hooks go?

Hooks live in a hooks object inside a settings file. Where you put it decides who gets the hook.

Location Applies to Shared?
~/.claude/settings.json All your projects No
.claude/settings.json One project Yes, commit it
.claude/settings.local.json One project No
Plugin hooks/hooks.json Wherever the plugin is enabled Yes
Subagent or skill frontmatter While that subagent runs, or after the skill is invoked Yes

The shape is always the same: event name, then a list of matcher groups, then the handlers to run.

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

If your file already has a hooks key, add new events as siblings inside it rather than replacing the object. Settings files are strict JSON, so no comments and no trailing commas. After saving, type /hooks in Claude Code to confirm the hook is listed under the right event.

Six copy-ready Claude Code hook examples

The examples use jq to read the JSON input. Install it first (brew install jq on macOS).

1. Format every file Claude edits

Add to .claude/settings.json in your project:

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

Swap Prettier for gofmt, black or rustfmt to suit your stack. Note that Claude can also change files through Bash; this hook only sees the Edit and Write tools.

2. Block edits to protected files

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

#!/bin/bash
INPUT=$(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
  fi
done

exit 0

Then register it:

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

Ask Claude to add a comment to .env to test it. The edit is refused and Claude sees the Blocked: message.

3. Get a desktop notification when Claude needs you

Add to ~/.claude/settings.json so it works in every project. This is the macOS version:

{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

On Linux, replace the command with notify-send 'Claude Code' 'Claude Code needs your attention'. Set the matcher to permission_prompt or idle_prompt if you only want one kind of alert.

4. Block dangerous shell commands

Save as .claude/hooks/block-rm-rf.sh, make it executable, and register it on PreToolUse with matcher Bash, the same way as example 2:

#!/bin/bash
COMMAND=$(jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -qE 'rm -rf|git push --force'; then
  echo "Blocked: destructive command. Ask the user to run it themselves." >&2
  exit 2
fi

exit 0

A hook that denies a call holds in every permission mode, including bypass mode, which makes this a useful backstop.

5. Keep Claude working until tests pass

A Stop hook can refuse to let Claude finish. Exit 2 and Claude receives your stderr as the reason to continue. Check stop_hook_active so the hook does not loop forever:

#!/bin/bash
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi

if ! OUTPUT=$(npm test 2>&1); then
  echo "Tests are failing. Fix them before you finish:" >&2
  echo "$OUTPUT" | tail -n 30 >&2
  exit 2
fi

exit 0
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/require-tests.sh" }
        ]
      }
    ]
  }
}

Claude Code also caps this: after a Stop hook blocks eight times in a row with no tool call in between, it ends the turn anyway. For long suites, the docs show an alternative that runs tests in the background with "async": true on a PostToolUse hook and reports the result on the next turn.

6. Re-add key context after compaction

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

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Reminder: use pnpm, not npm. Run pnpm test before committing.'"
          }
        ]
      }
    ]
  }
}

Replace the echo with something dynamic like git log --oneline -5 if you want recent commits instead.

All Claude Code hook events

As of October 2026 the docs list 33 hook events. The last column says what exit code 2 does, which tells you whether a hook on that event can stop something.

Event When it fires Exit 2
SessionStart A session begins or resumes Does not block; plain stdout is added to Claude's context
Setup You start with --init-only, or --init / --maintenance in -p mode Ignored
InstructionsLoaded A CLAUDE.md or .claude/rules/*.md file loads Ignored
UserPromptSubmit You submit a prompt, before Claude sees it Blocks the prompt
UserPromptExpansion A typed command expands into a prompt Blocks the expansion
MessageDisplay Assistant text is being displayed Original text is shown
PreToolUse Before a tool call runs Blocks the tool call
PermissionRequest Claude Code is about to ask you for permission Not honored; use JSON to allow or deny
PermissionDenied Auto mode denies a tool call Ignored
PostToolUse After a tool call succeeds Stderr goes to Claude; the tool already ran
PostToolUseFailure After a tool call fails Stderr goes to Claude
PostToolBatch After a batch of parallel tool calls, before the next model call Stops the loop
Notification Claude is waiting for input or permission Ignored
SubagentStart A subagent is spawned Stderr shown to you only
SubagentStop A subagent finishes The subagent keeps working
TaskCreated A task is being created Rolls back the task
TaskCompleted A task is being marked complete Keeps it open
Stop Claude finishes responding Claude keeps working
StopFailure The turn ends on an API error Ignored
TeammateIdle An agent team teammate is about to go idle The teammate keeps working
ConfigChange A settings or skills file changes mid-session Blocks the change (except policy settings)
CwdChanged The working directory changes Stderr shown to you only
DirectoryAdded A directory is added with /add-dir Logged only
FileChanged A watched file changes on disk Stderr shown to you only
WorktreeCreate A worktree is being created Any non-zero exit fails creation
WorktreeRemove A hook-created worktree is being removed Any non-zero exit fails removal
PreCompact Before context compaction Blocks compaction
PostCompact After compaction completes Stderr shown to you only
PreModelSwitch Before a model switch you requested Blocks the switch
PostModelSwitch After the session's model changes Stderr shown to you only
Elicitation An MCP server asks for user input Declines the request
ElicitationResult After you answer an MCP elicitation Blocks the response
SessionEnd The session ends Stderr shown to you only

In practice most setups use five: PreToolUse for guardrails, PostToolUse for formatting and linting, Notification for alerts, Stop for "are you really done" checks, and SessionStart for injecting context. The hooks reference has the input schema for each event.

What are the five hook types?

The five hook types are command, http, mcp_tool, prompt and agent. Each handler has a type, and most hooks use command:

Type What it does Default timeout
command Runs a shell command that gets the event JSON on stdin 10 minutes
http POSTs the event JSON to a URL and reads the decision from the response body 10 minutes
mcp_tool Calls a tool on an MCP server you have configured 10 minutes
prompt Sends the event to a Claude model, which returns {"ok": true} or {"ok": false, "reason": "..."} 30 seconds
agent Spawns a subagent that can read files and run commands before deciding. Marked experimental 60 seconds

Override the timeout per hook with a timeout field in seconds. A few events use shorter defaults, such as 30 seconds for UserPromptSubmit. A prompt hook is handy when the decision needs judgment rather than a fixed rule:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Check if all tasks are complete. If not, respond with {\"ok\": false, \"reason\": \"what remains to be done\"}."
          }
        ]
      }
    ]
  }
}

Command hooks can also run in the background with "async": true. They cannot block anything then, but their output reaches Claude on the next turn, which suits slow test suites.

Matchers

Without a matcher, a hook fires on every occurrence of its event. A matcher narrows it down. For tool events it matches the tool name:

  • Bash matches only shell commands.
  • Edit|Write matches either tool. A comma works the same way.
  • mcp__github__.* matches every tool from an MCP server named github.

Other events match other fields. SessionStart matches how the session started (startup, resume, clear, compact), Notification matches the notification type (permission_prompt, idle_prompt and others), and PreCompact matches manual or auto. Events like Stop and UserPromptSubmit ignore matchers. Matchers are case-sensitive.

For finer control on tool events, add an if field to a handler. It uses permission rule syntax, so "if": "Bash(git *)" only spawns the hook for git commands.

Input, output and exit codes

Each hook gets a JSON object on stdin. Every event includes fields like session_id, cwd and hook_event_name. Tool events add tool_name and tool_input:

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

Your script tells Claude Code what to do through its exit code:

Exit code What happens
0 Success. No objection. For PreToolUse this does not approve the call; the normal permission flow still applies
2 Blocking error. On events that can block, the action stops and your stderr becomes the reason. On PostToolUse, Claude sees the stderr even though the tool already ran
Anything else Non-blocking error. The action goes ahead and the transcript shows a hook error notice

For more control, exit 0 and print one JSON object to stdout. A PreToolUse hook can deny a call with a reason Claude sees:

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

permissionDecision also accepts ask and allow. Pick one approach per hook: exit 2 with stderr, or exit 0 with JSON.

Safety notes

  1. Hooks run as you. Command hooks have your full user permissions and can modify or delete anything your account can. Read every hook before adding it, especially from a repo you cloned.
  2. Workspace trust. In interactive sessions, Claude Code holds back settings-file hooks until you accept the trust dialog for the folder. In claude -p and SDK runs there is no dialog, so committed hooks run. Review .claude/ before scripting Claude over a repo you did not write, or pass --settings '{"disableAllHooks": true}'.
  3. Quote variables and use absolute paths. Write "$VAR", not $VAR, and reference scripts with "$CLAUDE_PROJECT_DIR".
  4. Keep matchers narrow. An empty matcher on PermissionRequest that returns allow would approve every prompt.
  5. Use permissions for hard rules too. The if filter is best effort. For a strict allow or deny list, pair hooks with permission rules.

To switch every hook off, set "disableAllHooks": true in a settings file.

Hooks, skills and subagents together

Hooks handle what must always happen. Skills handle how Claude should do a task, and subagents handle work that needs its own context. A common setup is a formatter hook, a protected-files hook, a review skill and a read-only reviewer subagent. The best practices guide covers how they fit into a daily workflow.

If you keep hook scripts, prompts and skills scattered across machines, Kipt can hold the prompts and skills in one private GitHub repo you own. Turn a prompt you reuse into a SKILL.md with Promote to skill, then install every skill on a new machine with one npx skills add command.

Source: Automate actions with hooks and the hooks reference.

Questions

What are hooks in Claude Code?

Hooks are commands you configure in a settings file that Claude Code runs at fixed points in a session, such as before a tool call, after a file edit, or when Claude finishes responding. They run every time, so they are the right place for rules that must always hold.

Where do I put Claude Code hooks?

In a hooks block inside a settings file: ~/.claude/settings.json for all your projects, .claude/settings.json to share with your team, or .claude/settings.local.json for a private project setup. Run /hooks inside Claude Code to see what is configured.

How does a hook block an action?

Exit with code 2 and write the reason to stderr. On events that can block, such as PreToolUse, Claude Code cancels the action and passes your message to Claude so it can adjust.

Do hooks work with subagents and skills?

Yes. Subagent and skill files can define hooks in their frontmatter. Subagent hooks run while that subagent is active, and skill hooks run for the rest of the session once the skill is invoked.

Are Claude Code hooks safe?

Command hooks run with your full user permissions, so review every command before you add it. Interactive sessions hold back settings-file hooks until you accept the workspace trust dialog for the folder.

More in Claude Code

All guides

Keep your prompts and skills in one place, in your own GitHub. Install every skill with one command.

Get started

Kipt is an independent product, not affiliated with or endorsed by Anthropic, OpenAI, Cursor or GitHub. Claude, ChatGPT, Codex, Cursor and GitHub are trademarks of their owners.