Hooks

Hooks

Deterministic automation that runs before or after Claude's tool use.

Hooks vs. CLAUDE.md

This is the key distinction:

  • CLAUDE.md — Advisory. Claude reads it and tries to follow it. It may forget or misjudge.
  • Hooks — Deterministic. Your script runs automatically every time, no matter what. Claude cannot skip it.

Use CLAUDE.md for guidelines and preferences. Use hooks for rules that must never be violated.

Hook Events

Hooks fire at specific points in Claude's agent loop. The most commonly used events:

EventWhen it fires
PreToolUseBefore a tool executes — can approve, deny, or modify the call
PostToolUseAfter a tool completes successfully
PostToolUseFailureAfter a tool execution fails
UserPromptSubmitWhen the user submits a prompt, before Claude processes it
StopWhen the main agent finishes responding
SubagentStart / SubagentStopWhen a subagent starts or completes
TaskCreated / TaskCompletedWhen an agent-team task is created or completed
TeammateIdleWhen an agent-team teammate becomes idle
SessionStart / SessionEndAt the beginning or end of a session
PreCompact / PostCompactBefore and after context compaction
NotificationPermission prompts, idle alerts, auth events
PermissionDeniedAfter auto-mode classifier denials (return {retry: true} to let Claude retry)

Configuration

Hooks are defined in .claude/settings.json under the hooks key. Each event maps to an array of matcher objects, and each matcher contains a hooks array with one or more handler definitions:

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

The matcher is a regex tested against the tool name (case-sensitive). Use | to match multiple tools: "Write|Edit". The type field picks the handler: "command" runs a shell script, "http" POSTs the event JSON to an endpoint, "mcp_tool" calls a connected MCP tool, "prompt" evaluates with a Claude model, "agent" spawns a subagent.

Use command hooks for fast deterministic checks. Use prompt or agent hooks only when the check genuinely needs model judgment; they add latency and token cost.

Practical Examples

Auto-Lint After Every Write

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

Block Writes to Sensitive Files

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | grep -qE '(\\.env|secrets|credentials)' && exit 2 || exit 0"
          }
        ]
      }
    ]
  }
}

If the hook exits with code 2, Claude Code blocks the tool and feeds the error message back to Claude. Exit code 0 means success; other non-zero codes are non-blocking errors.

Run Tests After Changes

json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "file=$(jq -r '.tool_input.file_path'); if echo \"$file\" | grep -q 'src/'; then npm test -- --related \"$file\"; fi"
          }
        ]
      }
    ]
  }
}

Run a Script When Claude Finishes

json
{
  "hooks": {
    "Stop": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude finished\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

Hook Input & Environment Variables

Hook scripts receive the full event context as JSON on stdin — parse it with jq or similar. Key fields:

  • tool_name — the tool that was called (e.g., Write, Bash, Read)
  • tool_input — the tool's arguments object; for Write/Edit/Read, tool_input.file_path is always an absolute path
  • tool_response — (PostToolUse only) the result returned by the tool
  • session_id, cwd, transcript_path

Environment variables exported to hook commands:

  • $CLAUDE_PROJECT_DIR — project root
  • $CLAUDE_PLUGIN_ROOT / $CLAUDE_PLUGIN_DATA — set inside plugin-defined hooks
  • $CLAUDE_ENV_FILE — (SessionStart / Setup / CwdChanged / FileChanged only) append export VAR=value lines here to persist env vars into subsequent Bash calls
  • $CLAUDE_EFFORT — current effort level (inside tool-use context)
  • $CLAUDE_CODE_REMOTE"true" when running in a remote web environment

File paths and session_id are only in the stdin JSON, not env vars. Parse with jq -r '.tool_input.file_path' rather than reaching for a non-existent $CLAUDE_TOOL_INPUT_FILE_PATH.

Conditional Hooks with if

Add an if field to a handler (inside the inner hooks[] array, not on the outer matcher) to fire only when a permission rule matches. This reduces unnecessary process spawning:

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "echo 'Git command detected' >> ~/.claude/audit.log"
          }
        ]
      }
    ]
  }
}

Disabling Hooks

To disable all hooks for a session (useful for debugging):

json
{
  "disableAllHooks": true
}

Hooks are deterministic control over an agent

The advisory vs. deterministic distinction is a core concept in agent design. Hooks give you guaranteed behavior regardless of what the LLM decides — a pattern used in all production agent systems.

Learn about Tool Use & agent control patterns

Related Reading

Continue with practice

You have finished the core ideas of Hooks.

If you want to turn the idea into something reusable, continue practicing on AgentWay.