CoralBack to the site

Claude Code hooks: a practical guide with examples

What Claude Code hooks are, every event, matchers, the JSON on stdin, exit codes and JSON output, plus 7 copy-paste examples you can use today.

Updated

Claude Code hooks are shell commands that Claude Code runs at fixed points in a session: before a tool runs, after it runs, when Claude needs you, when it stops, and more. You define them in a settings.json file. Unlike an instruction in CLAUDE.md, a hook always runs, so it’s the right place for rules and automation that must not depend on the model remembering.

This guide follows the official hooks reference and hooks guide as of October 2026. Claude Code ships often, so check there for anything newer.

Where hooks live

Hooks go in a hooks object inside a settings file. Where you put it sets the scope:

FileScopeShared?
~/.claude/settings.jsonAll your projectsNo, only your machine
.claude/settings.jsonOne projectYes, commit it
.claude/settings.local.jsonOne projectNo, gitignored
Managed policy settingsYour organizationSet by admins
A plugin’s hooks/hooks.jsonWhile the plugin is onWith the plugin

Hooks from every level merge. A project hook doesn’t replace your user hook: both run. To turn every hook off for a while, set "disableAllHooks": true.

The shape is always the same: an event name, a list of matcher groups, and the handlers in each group.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "echo edited >> ~/hook.log" }
        ]
      }
    ]
  }
}

Most hooks are "type": "command". There are also http (POST the event to a URL), mcp_tool, prompt (ask a model a yes or no question) and agent (experimental).

The events

These are the events most people use. The reference lists more, including PostToolBatch, TaskCreated, ConfigChange, CwdChanged, FileChanged, WorktreeCreate, PostCompact, Elicitation and the model switch events.

EventWhen it firesMatcher filtersCan block?
SessionStartA session starts or resumesstartup, resume, clear, compact, forkNo, but can add context
UserPromptSubmitYou send a prompt, before Claude sees itnoneYes
PreToolUseBefore a tool call runsTool nameYes
PermissionRequestClaude Code is about to ask you for permissionTool nameAnswers it with JSON
PostToolUseAfter a tool call succeedsTool nameFeeds back to Claude
PostToolUseFailureAfter a tool call failsTool nameFeeds back to Claude
NotificationClaude Code sends a notificationNotification typeNo
SubagentStart / SubagentStopA subagent starts or finishesAgent typeStop: yes
StopClaude finishes respondingnoneYes, keeps it working
StopFailureA turn ends on an API errorError typeNo
PreCompactBefore context compactionmanual, autoYes
SessionEndA session endsclear, logout, prompt_input_exit, other and moreNo

For Notification, useful matchers are permission_prompt (a permission prompt has waited about six seconds) and idle_prompt (Claude finished about 60 seconds ago and you haven’t typed).

Matchers

  • "*", "" or no matcher: fires every time.
  • Only letters, digits, _, -, spaces, , and |: an exact name or a list, like Bash or Edit|Write.
  • Anything else: an unanchored JavaScript regex. Edit.* also matches NotebookEdit, so use ^Edit$ when you mean one tool.
  • MCP tools are named mcp__<server>__<tool>. Match a whole server with mcp__github__.*.
  • Matchers are case-sensitive. A matcher on an event that doesn’t support one is ignored.

On tool events you can also add an if field with permission rule syntax, like "if": "Bash(git *)", so the hook only spawns for git commands. The docs call it best-effort, so don’t rely on it for a hard security rule.

What your hook receives

Each hook gets one JSON object on stdin. Common fields are session_id, transcript_path, cwd, permission_mode and hook_event_name. Tool events add tool_name and tool_input. A PreToolUse for Bash looks like this:

{
  "session_id": "abc123",
  "cwd": "/Users/you/code/hawkins",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "npm test", "description": "Run test suite" },
  "tool_use_id": "toolu_01ABC123"
}

$CLAUDE_PROJECT_DIR points at the project root, so use it for script paths. The examples below parse stdin with jq (brew install jq on a Mac).

Exit codes and JSON output

  • Exit 0: success. Claude Code reads any JSON you print. For SessionStart and UserPromptSubmit, plain stdout is added to Claude’s context.
  • Exit 2: a blocking error. Your stderr goes to Claude as the reason. It blocks PreToolUse, UserPromptSubmit and PreCompact, and makes Stop keep working. PermissionRequest ignores exit 2: answer it with JSON instead.
  • Any other code: a non-blocking error. The action goes ahead and the transcript shows a hook error notice.

For finer control, exit 0 and print JSON. Common fields are continue (false stops Claude entirely), stopReason and systemMessage (a warning shown to you). Event-specific fields go inside hookSpecificOutput:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Database writes are not allowed"
  }
}

permissionDecision takes allow, deny, ask or defer. SessionStart and UserPromptSubmit take additionalContext. PostToolUse and Stop use a top-level "decision": "block" with a reason. A field at the wrong level is ignored without an error.

Seven examples you can paste

Put each hooks block in ~/.claude/settings.json or the project’s .claude/settings.json. If you already have a hooks object, add the event as a sibling key.

1. Desktop notification when Claude needs you

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

On Linux, use notify-send 'Claude Code' 'Claude Code needs you'. On a Mac, if nothing shows, allow notifications for Script Editor in System Settings. More options in Claude Code notifications.

2. A sound when Claude finishes

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "afplay /System/Library/Sounds/Glass.aiff" }
        ]
      }
    ]
  }
}

Stop fires each time Claude finishes responding, not only at the end of a task, and not when you interrupt it.

3. Block edits to .env files

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

#!/bin/bash
FILE_PATH=$(jq -r '.tool_input.file_path // empty')
case "$(basename "$FILE_PATH")" in
  .env|.env.*)
    echo "Blocked: $FILE_PATH is a secrets file. Ask me to change it." >&2
    exit 2
    ;;
esac
exit 0
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-env.sh" }
        ]
      }
    ]
  }
}

Claude sees the message and changes course. Claude can still touch the file through Bash (cat >, sed -i), so pair this with a deny rule in your permissions if it matters.

4. Format after every edit

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

Swap in gofmt -w or ruff format for your stack.

5. Log every Bash command

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '\"\\(now | todate) \\(.cwd) \\(.tool_input.command)\"' >> ~/.claude/bash-log.txt"
          }
        ]
      }
    ]
  }
}

6. Give Claude context at session start

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|compact",
        "hooks": [
          { "type": "command", "command": "git status --short && git log --oneline -5" }
        ]
      }
    ]
  }
}

What it prints goes into Claude’s context, which helps most after a compaction.

7. Approve one safe permission prompt

{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -e '.tool_input.command == \"npm test\"' >/dev/null && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":{\"behavior\":\"allow\"}}}' || true"
          }
        ]
      }
    ]
  }
}

It approves exactly npm test and nothing else: anything longer, like npm test && rm -rf build, still asks you. Keep these matches exact. A loose pattern approves more than you meant. See Claude Code permission prompts for the other ways to cut prompts.

Debugging hooks

  • Run /hooks in Claude Code. It lists every configured hook by event and shows which file it came from.
  • Press ⌃O to open the transcript view. A blocked action shows its message, and a failed hook shows a hook error notice.
  • Start with claude --debug, or claude --debug-file /tmp/claude.log and tail -f it, to see exit codes, stdout and stderr. /debug turns logging on mid-session.
  • Test a script by hand: echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | ./my-hook.sh; echo $?
  • JSON ignored? Something printed before the {, often an echo in your shell profile. Or a field sits outside hookSpecificOutput.
  • A Stop hook that blocks forever: check stop_hook_active in the input and exit 0 when it’s true. Claude Code overrides a Stop hook after 8 blocks in a row.

Security: read hooks before you install them

A hook runs with your user account’s permissions, with no prompt, every time its event fires. A project’s .claude/settings.json comes with the repo, so read it before you trust a repo you cloned. Read any hook script or plugin before installing it, the same as any shell script from the internet. Hooks fail open by default: if one crashes or times out, the action usually goes ahead.

Codex has hooks too

The Codex CLI runs hooks as well, on by default. They live in ~/.codex/hooks.json, a project’s .codex/hooks.json, or [hooks] tables in config.toml. Events include SessionStart, PreToolUse, PermissionRequest, PostToolUse, UserPromptSubmit, Stop and more, and exit 2 with a reason on stderr blocks like in Claude Code. Codex asks you to review non-managed hooks with /hooks before they run. Details are in the Codex hooks docs.

How Coral uses hooks

Coral is built on these same hooks. When a coding agent asks for permission, has a question or finishes, the hook tells Coral, and the prompt shows up in your Mac’s notch and on your iPhone, where you answer with one key or tap.

Coral asks first. During setup, each agent gets one row that connects it (for Claude Code, its hooks), and the row says exactly which files change and where the backups go. You can choose Later and skip it. When Coral updates, it keeps its own hooks current but never replaces one you changed; Settings → Agents tells you once, with a way to put Coral’s back. Coral’s hooks sit in your settings file next to your own, so /hooks shows them like any other. Details are in Get started and Privacy and security.

Removing Coral’s hooks

In the island menu, open Agent Hooks and choose Remove Claude Code Hooks. Codex, Copilot CLI and OpenCode have their own items in the same menu (Remove Codex Hooks, Remove Copilot Hooks, Remove OpenCode Plugin). Choose Add Claude Code Hooks… there to put them back.

  • Only Coral’s entries go. Your own hooks and every other setting stay as they were. A Coral hook you edited is removed too.
  • A backup first. Before it writes, Coral copies the file next to it, for example ~/.claude/settings.json.coral-backup-20261021-143000.
  • Quitting Coral is safe. If Coral isn’t running, its hook exits at once without output, so your agents ask in the terminal as usual.
  • On a server, Remove… in the Servers window takes Coral’s hooks and its coral-hook program back off. See Servers.

Questions

What are hooks in Claude Code?

User-defined commands that Claude Code runs at set points: before and after tools, when you send a prompt, when it needs you, and when it stops. They run every time, unlike instructions the model may forget.

What’s the difference between PreToolUse and PermissionRequest?

PreToolUse runs before every matching tool call and can block it with exit 2. PermissionRequest runs only when Claude Code is about to show you a permission prompt, and answers it with a JSON decision.

Why isn’t my hook firing?

Run /hooks to check it’s listed under the right event. Then check the matcher: it’s case-sensitive, and the script must be executable. Start with claude --debug to see what ran.

Can a hook stop Claude from finishing?

Yes. A Stop hook that exits 2, or returns "decision": "block" with a reason, makes Claude keep working. Check stop_hook_active so it doesn’t loop.

Never leave an agent waiting.

Coral taps you in your Mac’s notch or on your iPhone when an agent needs you. Free.

Download for freeSee pricing

More guides

Coral alternatives and comparisons