Skip to content

Hooks

Event-driven automation that fires deterministically on specific Claude Code events.

Hooks are event-driven automation that fires on specific Claude Code events. Unlike CLAUDE.md instructions (which are non-deterministic), hooks execute deterministically — they run 100% of the time when triggered.

How hooks work

Hooks let you run commands, call HTTP endpoints, or invoke AI evaluations automatically in response to Claude Code events. Use them for linting, formatting, security checks, logging, and anything else that should happen consistently without relying on Claude to remember.

Key behaviors:

  • All matching hooks for an event run in parallel
  • Identical handlers are deduplicated automatically (by command string or URL)
  • Hooks run in the current directory with Claude Code’s environment
  • Settings file changes are picked up automatically by a file watcher

Configuration format

Hooks are configured in settings JSON files under the hooks key. Each event maps to an array of matcher groups, and each matcher group contains an array of hook handlers.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/my-script.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Structure breakdown

hooks
  └── EventName (e.g., "PreToolUse")
        └── Array of matcher groups
              ├── matcher: regex to filter when this group activates
              └── hooks: array of hook handlers
                    ├── type: "command" | "http" | "prompt" | "agent"
                    ├── if: permission rule filter (tool events only)
                    ├── timeout: seconds before timeout
                    ├── statusMessage: custom spinner text
                    ├── once: run only once per session (boolean)
                    └── [type-specific fields]

Event reference

Blocking events

These events pause execution until hooks complete. Hooks can block or modify the operation.

EventFires whenMatcher matches against
PreToolUseBefore Claude runs a toolTool name (Bash, Edit, mcp__.*)
PermissionRequestWhen Claude needs permissionTool name
UserPromptSubmitWhen you submit a promptNo matcher support
StopWhen Claude finishes respondingNo matcher support
SubagentStopWhen a subagent finishesAgent type (Explore, Bash, Plan)
TaskCreatedWhen a task is createdNo matcher support
TaskCompletedWhen a task completesNo matcher support
TeammateIdleWhen a teammate goes idleNo matcher support
ConfigChangeWhen settings changeConfig source (project_settings, etc.)
ElicitationWhen MCP server shows a formMCP server name
ElicitationResultWhen form is submittedMCP server name
WorktreeCreateWhen creating a worktreeNo matcher support

Non-blocking events

These events fire-and-forget. Hooks run but do not pause Claude’s execution.

EventFires whenMatcher matches against
PostToolUseAfter Claude runs a toolTool name
PostToolUseFailureAfter a tool failsTool name
PermissionDeniedWhen auto mode denies a tool callTool name
NotificationWhen Claude sends a notificationNotification type (permission_prompt, idle_prompt)
SubagentStartWhen a subagent startsAgent type
SessionStartWhen a session beginsSession source (startup, resume, clear)
SessionEndWhen a session endsEnd reason (clear, resume, logout)
StopFailureWhen Claude stops due to errorError type (rate_limit, billing_error, etc.)
CwdChangedWhen working directory changesNo matcher support
FileChangedWhen a watched file changesFilename (basename)
PreCompactBefore context compactionTrigger (manual, auto)
PostCompactAfter context compactionTrigger (manual, auto)
InstructionsLoadedWhen a CLAUDE.md is loadedLoad reason (session_start, include)
WorktreeRemoveWhen a worktree is removedNo matcher support

Hook types

Command hooks

Execute shell commands. The most common type. Receive event data as JSON on stdin and communicate results via exit codes and stdout.

{
  "type": "command",
  "command": "./scripts/lint-check.sh",
  "async": false,
  "timeout": 600
}
  • command (required): Shell command to run
  • async (optional): Run in background without blocking (default: false)
  • shell (optional): "bash" or "powershell" (default: "bash")
  • Default timeout: 600 seconds

HTTP hooks

Send a POST request with event data as JSON body.

{
  "type": "http",
  "url": "http://localhost:8080/hooks/pre-tool-use",
  "headers": {
    "Authorization": "Bearer $MY_TOKEN"
  },
  "allowedEnvVars": ["MY_TOKEN"],
  "timeout": 30
}
  • url (required): Endpoint URL
  • headers (optional): HTTP headers (supports $VAR interpolation)
  • allowedEnvVars (optional): Env vars that can be interpolated into headers
  • Default timeout: 30 seconds

Prompt hooks

Send a single-turn prompt to a Claude model for yes/no decisions.

{
  "type": "prompt",
  "prompt": "Does this command look safe? $ARGUMENTS",
  "model": "haiku",
  "timeout": 30
}
  • prompt (required): Evaluation text ($ARGUMENTS is replaced with event data)
  • model (optional): Model alias to use
  • Default timeout: 30 seconds

Agent hooks

Spawn a subagent with tool access to verify conditions.

{
  "type": "agent",
  "prompt": "Review this file change for security issues. $ARGUMENTS",
  "model": "sonnet",
  "timeout": 60
}
  • prompt (required): Task description for the agent
  • model (optional): Model alias to use
  • Default timeout: 60 seconds

Matchers and the if field

Matchers

The matcher field is a regex that determines when a matcher group activates. Use "*", "", or omit entirely to match all events of that type.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "./lint.sh" }]
      }
    ]
  }
}

For MCP tools, the naming pattern is mcp__<server>__<tool>:

  • mcp__memory__.* matches all memory server tools
  • mcp__.*__write.* matches write tools from any server

The if field

The if field provides additional filtering using permission rule syntax. It only works on tool events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest).

{
  "matcher": "Bash",
  "hooks": [
    {
      "type": "command",
      "if": "Bash(rm *)",
      "command": "./scripts/block-rm.sh"
    }
  ]
}

This hook only fires for Bash tool calls whose command matches rm *. The if field prevents the hook process from spawning at all when the pattern does not match.

Command hook input and output

Input (stdin)

Command hooks receive JSON on stdin with these common fields:

{
  "session_id": "abc123",
  "cwd": "/path/to/project",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite"
  }
}

The exact fields vary by event. Tool events include tool_name and tool_input. PostToolUse adds tool_response. Stop includes last_assistant_message.

Output (exit codes)

Exit codeBehavior
0Success. JSON output parsed if present
2Blocking error. Blocks the operation (tool call denied, prompt rejected, etc.)
OtherNon-blocking error. stderr shown in verbose mode; execution continues

JSON output (exit 0)

{
  "continue": true,
  "stopReason": "optional message when continue is false",
  "suppressOutput": false,
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow|deny|ask",
    "permissionDecisionReason": "Reason shown to user"
  }
}

Set "continue": false to stop Claude immediately. The hookSpecificOutput fields vary by event — for example, PreToolUse supports permissionDecision and updatedInput, while PostToolUse supports additionalContext.

Practical examples

Auto-lint after file edits

Run your linter every time Claude writes or edits a file:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "npx eslint --fix $(cat | jq -r '.tool_input.file_path')",
            "statusMessage": "Running linter..."
          }
        ]
      }
    ]
  }
}

Block destructive commands

Prevent Claude from running rm -rf without your approval:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": ".claude/hooks/block-rm.sh"
          }
        ]
      }
    ]
  }
}

The hook script (.claude/hooks/block-rm.sh):

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

if echo "$COMMAND" | grep -q 'rm -rf'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "rm -rf blocked by hook"
    }
  }'
else
  exit 0
fi

Add context to every prompt

Inject environment information into Claude’s context on every prompt:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"UserPromptSubmit\",\"additionalContext\":\"Current branch: '$(git branch --show-current)'\"}}'",
            "statusMessage": "Loading context..."
          }
        ]
      }
    ]
  }
}

Log all tool usage via HTTP

Send tool usage to a monitoring endpoint:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "http",
            "url": "http://localhost:8080/hooks/tool-usage",
            "headers": {
              "Authorization": "Bearer $HOOK_TOKEN"
            },
            "allowedEnvVars": ["HOOK_TOKEN"]
          }
        ]
      }
    ]
  }
}

Prevent Claude from stopping prematurely

Force Claude to keep going when it tries to stop without running tests:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Review Claude's last message. Did it run the test suite before stopping? If tests were not run and code was changed, respond NO.",
            "statusMessage": "Checking if tests were run..."
          }
        ]
      }
    ]
  }
}

Environment variables

These environment variables are available to all command hooks:

VariableDescription
CLAUDE_PROJECT_DIRProject root directory
CLAUDE_CODE_REMOTE"true" in remote/web environments; unset locally

SessionStart, CwdChanged, and FileChanged hooks also receive:

VariableDescription
CLAUDE_ENV_FILEPath to a file where you can persist env vars using export statements

Settings file locations

FileScopeShared via git?
~/.claude/settings.jsonAll projects (personal)No
.claude/settings.jsonThis project (team)Yes
.claude/settings.local.jsonThis project (personal)No (gitignored)
Managed policy settingsOrganization-wideAdmin-controlled

Team hooks in .claude/settings.json apply to everyone who clones the repo. Personal hooks in .claude/settings.local.json add to (but cannot remove) team hooks.

To disable all hooks, set "disableAllHooks": true in your settings file. Note that managed policy hooks cannot be disabled by user or project settings.

Browsing configured hooks

Use the /hooks slash command inside Claude Code to browse all configured hooks. It shows every event with a count of configured hooks, lets you drill into matchers and view handler details, and labels each hook with its source: [User], [Project], [Local], [Plugin], [Session], or [Built-in]. The /hooks view is read-only.

Hooks vs. CLAUDE.md instructions

AspectHooksCLAUDE.md
ExecutionDeterministic (always runs)Non-deterministic (Claude may skip)
ScopeShell commands, HTTP calls, AI evaluationNatural language instructions
Best forFormatting, linting, security gates, loggingCoding conventions, style preferences
Failure modeBlocks the operationClaude might not follow it

Rule of thumb: If something must always happen, use a hook. If it is a preference or guideline, use CLAUDE.md.

Quick reference

What you wantEventMatcherHook type
Lint after file changesPostToolUseEdit|Writecommand
Block dangerous commandsPreToolUseBash + if fieldcommand
Add context to promptsUserPromptSubmit(none)command
Custom notificationsNotificationnotification typecommand or http
Validate before stoppingStop(none)prompt or command
Log tool usagePostToolUse"" (all tools)http
Monitor MCP toolsPreToolUsemcp__server__.*command
Set up session environmentSessionStart(none)command
React to config changesConfigChangeconfig sourcecommand

Tips

  • Start with a PostToolUse lint hook on Edit|Write — it has the highest payoff-to-effort ratio.
  • Keep hook commands fast. Slow hooks disrupt the interactive workflow.
  • Use "async": true on command hooks for non-critical operations like logging.
  • Test hooks manually before adding them to settings — a broken hook on a blocking event will block all operations of that type.
  • Use the if field to narrow tool-event hooks without spawning a process for every invocation.
  • Use the statusMessage field to show a custom spinner message while your hook runs.
  • Set "once": true for hooks that only need to run once per session (e.g., environment setup).
  • Combine hooks with permissions: hooks enforce standards deterministically, permissions control what Claude can do interactively.
  • Use /hooks to verify your hooks are loaded and sourced correctly.