Claude Code Hooks: The Extensibility Feature That Makes It Programmable

The complete guide to Claude Code hooks — the killer extensibility feature that separates Claude Code from every other AI coding tool. Automate formatting, enforce security policies, build domain-specific workflows, and make Claude Code truly yours.

February 15, 2026 · 4 min read

Claude Code hooks are the feature that makes Claude Code programmable in a way no other AI coding tool is. They are shell commands or LLM prompts that fire automatically at specific points in Claude Code's lifecycle — and they are the difference between babysitting your AI assistant and letting it run autonomously with guardrails.

Boris Cherny, engineer and author of Programming TypeScript, captured this in early 2026: "Reflecting on what engineers love about Claude Code, one thing that jumps out is its customizability: hooks, plugins, LSPs, MCPs. Each is an extension point that lets you mold Claude Code to your exact workflow." No other AI coding tool offers this level of programmability.

Why Hooks Are the Killer Feature

The distinction matters. VS Code extensions customize your editor. GitHub Actions customize your CI. Claude Code hooks customize your AI agent. They turn a general-purpose coding assistant into a domain-specific development partner that enforces your team's rules, integrates with your infrastructure, and never forgets a step.

14
Lifecycle events you can hook into
3
Hook types: command, prompt, agent

Unlike CLAUDE.md instructions which are advisory and may be ignored during long sessions, hooks are deterministic. They execute every time, guaranteed. This is the critical distinction:

MechanismBehaviorBest For
CLAUDE.mdAdvisory — Claude reads it but may forget in long sessionsGuidelines: 'Prefer named exports', 'Use Bun, not npm'
HooksDeterministic — executes every time, zero exceptionsRequirements: 'Run eslint after every edit', 'Block writes to migrations/'

The trust problem hooks solve

You cannot fully trust an LLM to always do the right thing. You should not have to babysit it on every action. Hooks are the middle ground — deterministic guardrails that let you run Claude Code in autonomous mode with confidence. They are the reason power users report running Claude Code with --allowedTools permissions they would never grant without hooks protecting critical paths.

The Three Hook Types

Every hook is one of three types, each suited to different levels of decision complexity.

Command Hooks

Run shell scripts. Receive JSON via stdin, return results through exit codes and stdout. The workhorse — 90% of hooks in the wild are commands.

Prompt Hooks

Send a prompt to a Claude model for single-turn yes/no evaluation. No shell scripting required — the LLM decides. Fast, cheap, no tool access.

Agent Hooks

Spawn a subagent that can read files, search code, and run commands to verify conditions. Up to 50 tool-use turns. The most powerful type.

Choose command hooks for anything deterministic (formatting, blocking, logging). Choose prompt hooks when the decision requires judgment but no file access. Choose agent hooks when verification requires inspecting the actual codebase state.

Hook Events: The Complete Lifecycle

Hooks fire at 14 different lifecycle events. Each corresponds to a specific moment in Claude's execution where you can inject custom behavior. The four most commonly used events are highlighted.

EventWhen It FiresCan Block?
SessionStartSession begins, resumes, or context is compactedNo
UserPromptSubmitYou submit a prompt, before Claude processes itYes
PreToolUseBefore a tool call executesYes
PermissionRequestWhen a permission dialog appearsYes
PostToolUseAfter a tool call succeedsNo
PostToolUseFailureAfter a tool call failsNo
NotificationClaude sends a notificationNo
SubagentStartA subagent is spawnedNo
SubagentStopA subagent finishesYes
StopClaude finishes respondingYes
TeammateIdleAgent team teammate about to go idleYes
TaskCompletedTask marked as completedYes
PreCompactBefore context compactionNo
SessionEndSession terminatesNo

Matchers: Filtering When Hooks Fire

Without a matcher, a hook fires on every occurrence of its event. Matchers are regex strings that narrow this down. Use "*", "", or omit the matcher to match everything.

EventMatches OnExample Matchers
PreToolUse / PostToolUseTool nameBash, Edit|Write, mcp__.*
SessionStartHow session startedstartup, resume, clear, compact
SessionEndWhy session endedclear, logout, other
NotificationNotification typepermission_prompt, idle_prompt
SubagentStart / StopAgent typeBash, Explore, Plan, custom names
PreCompactCompaction triggermanual, auto

How Hooks Work: Input, Output, Exit Codes

Every hook receives JSON context via stdin when it fires. Your script reads this data, takes action, and tells Claude Code what to do next.

JSON Input

Example: PreToolUse input for a Bash command

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

Exit Codes

Exit 0 — Success

Action proceeds. For UserPromptSubmit and SessionStart, stdout is added as context for Claude.

Exit 2 — Block

Action is blocked. Stderr text is fed back to Claude as an error so it can adjust its approach.

Other Codes — Non-blocking

Action proceeds. Stderr is logged in verbose mode (Ctrl+O) but not shown to Claude.

Structured JSON Output

For more control than exit codes alone, exit 0 and print JSON to stdout. PreToolUse hooks can return permissionDecision (allow/deny/ask). PostToolUse and Stop hooks can return decision: "block" with a reason.

PreToolUse JSON output — deny a tool call with feedback

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

The Security Angle: Programmable Guardrails

When VS Code 1.109 introduced terminal sandboxing in late 2025 to prevent AI agents from running dangerous commands, it validated a problem Claude Code users had already solved with hooks. But hooks go far beyond sandboxing.

CapabilityVS Code SandboxingClaude Code Hooks
Block dangerous commandsYes (fixed blocklist)Yes (custom logic)
Custom rules per projectNoYes — different rules per repo
Custom rules per directoryNoYes — protect specific paths
Audit loggingNoYes — log all actions to file
Require confirmationBinary allow/denyAllow / deny / ask (prompt user)
Inspect tool argumentsNoYes — read full JSON input
Integrate with external systemsNoYes — call any API or service

Hooks are programmable security policies, not just a blocklist. You write the logic. You decide what gets blocked, what gets logged, and what requires human confirmation. This is the level of control that makes teams comfortable running Claude Code in autonomous mode on production codebases.

Security-focused PreToolUse hook — inspect and block dangerous patterns

#!/bin/bash
# .claude/hooks/security-guard.sh
# Programmable security policy — block destructive commands,
# require review for sensitive operations

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
TOOL=$(echo "$INPUT" | jq -r '.tool_name')

# Block destructive shell commands
if [ "$TOOL" = "Bash" ]; then
  if echo "$COMMAND" | grep -qE 'rm -rf|git push --force|drop table|truncate'; then
    echo "BLOCKED: destructive command not allowed" >&2
    exit 2
  fi
  # Log all shell commands to audit trail
  echo "$(date -u +%FT%TZ) BASH: $COMMAND" >> .claude/audit.log
fi

# Protect sensitive files from edits
if [ "$TOOL" = "Edit" ] || [ "$TOOL" = "Write" ]; then
  if echo "$FILE_PATH" | grep -qE '\.env|migrations/|package-lock\.json|\.git/'; then
    echo "BLOCKED: $FILE_PATH is a protected file" >&2
    exit 2
  fi
fi

exit 0

Register as a universal PreToolUse hook

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

Auto-Format, Block, Notify: Core Patterns

These are the three hooks every Claude Code user should set up on day one. They eliminate the most common friction points.

Typecheck on Every Commit (Recommended)

The highest-value hook you can add today. bun run typecheck:fast uses tsgo (the native TypeScript type checker) to catch type errors in under a second. Run it as a Stop hook so Claude cannot finish a task with broken types.

Stop hook — typecheck with tsgo before Claude finishes

// .claude/settings.json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "#!/bin/bash\nINPUT=$(cat)\nif [ \"$(echo \"$INPUT\" | jq -r '.stop_hook_active')\" = \"true\" ]; then exit 0; fi\nbun run typecheck:fast 2>&1 || { echo 'Type errors found. Fix before finishing.' >&2; exit 2; }"
          }
        ]
      }
    ]
  }
}

Why tsgo changes everything

Traditional tsc takes 10-30 seconds on a medium project. tsgo (the native Go port) completes in under a second. This makes typecheck-on-every-commit practical — the hook runs so fast that it feels instant. Add "typecheck:fast": "tsgo --noEmit" to your package.json scripts, then use this hook. Every commit Claude makes will be type-safe.

Pair this with a CLAUDE.md instruction to push to branches:

CLAUDE.md — enforce branch-based workflow

# Workflow
- IMPORTANT: Always push to a branch, never directly to main
- Run `bun run typecheck:fast` after every series of code changes
- Create a PR for review — do not merge directly

Auto-Format After Every Edit

The most popular hook in the community. AI-generated code instantly matches your project's style guide without relying on Claude to remember formatting rules.

PostToolUse hook — auto-format with Prettier

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

Works with any formatter

Replace npx prettier --write with any command: black for Python, gofmt -w for Go, rustfmt for Rust, mix format for Elixir. The hook extracts the file path from JSON input with jq and passes it to your formatter.

Desktop Notifications

Stop watching the terminal. Get alerted when Claude needs input so you can focus on other work.

Notification hook — macOS native alerts

// ~/.claude/settings.json (applies to all projects)
{
  "hooks": {
    "Notification": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
          }
        ]
      }
    ]
  }
}

macOS

osascript -e 'display notification "..." with title "Claude Code"'

Linux

notify-send 'Claude Code' 'Needs attention'

Windows

powershell.exe -Command "[Windows.Forms.MessageBox]::Show('Needs attention')"

Stop Hooks as Quality Gates

Stop hooks fire when Claude finishes responding. They can prevent Claude from stopping if conditions are not met. This turns "please remember to run tests" from an advisory suggestion into an enforced requirement.

Stop hook — require passing tests before Claude can finish

#!/bin/bash
# .claude/hooks/require-tests.sh

INPUT=$(cat)

# CRITICAL: Check stop_hook_active to prevent infinite loops
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0  # Already continued once — let Claude stop
fi

# Run tests
if ! npm test 2>&1; then
  echo "Tests failing. Fix before finishing." >&2
  exit 2  # Block Claude from stopping
fi

exit 0

The infinite loop trap

Always check stop_hook_active in Stop hooks. When it is true, Claude is already continuing because of a previous Stop hook. Exit 0 immediately. Without this check, your hook blocks Claude forever. This is the #1 mistake new hook authors make.

Stop hooks are where hooks become genuinely transformational. They implement what the community calls "quality gates" — automated checks that run every time Claude says it is done. Combined with best practices for task scoping, they close the loop on the "80% problem" where AI solutions are almost right but not quite.

Prompt and Agent-Based Hooks

Not every decision can be made with a shell script. For decisions that require judgment, use prompt or agent hooks.

Prompt Hooks

Single-turn LLM evaluation. The model returns yes/no. Fast, cheap, no tool access. Best for: "Is this commit message descriptive enough?"

Prompt-based Stop hook

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

Agent Hooks

Multi-turn verification with tool access. Spawns a subagent that can read files and run commands. Best for: "Do all tests pass?"

Agent-based Stop hook

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify all unit tests
pass. Run the test suite
and check results.
$ARGUMENTS",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

Both types return the same format: {"ok": true} to allow the action, or {"ok": false, "reason": "..."} to block it. Use prompt hooks when the input data alone is enough to decide. Use agent hooks when you need to verify against the actual state of the codebase.

Hook + MCP Integration

This is where hooks become truly powerful. MCP (Model Context Protocol) servers give Claude access to external tools — databases, APIs, search engines. Hooks let you add guardrails, logging, and automation around those MCP tools.

MCP tools follow the naming pattern mcp__server__tool. Use regex matchers to target specific servers or operations.

Log all GitHub MCP operations

// .claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "mcp__github__.*",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '"GITHUB: " + .tool_name + " " + (.tool_input | tostring)' >> .claude/mcp-audit.log"
          }
        ]
      }
    ]
  }
}

Validate search queries before WarpGrep executes

// .claude/settings.json — Hook + MCP integration example
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "mcp__warpgrep__.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Search complete. Results injected into context.' >> .claude/search.log",
            "async": true
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "mcp__.*__write.*|mcp__.*__delete.*|mcp__.*__create.*",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'MCP write operation detected — logging for audit' >> .claude/mcp-audit.log"
          }
        ]
      }
    ]
  }
}

Hooks + MCPs = custom development environments

The combination of hooks (deterministic guardrails) and MCPs (tool access) is what makes Claude Code uniquely extensible. Hooks ensure safety and consistency. MCPs provide capability. Together, they let teams build development environments tailored to their exact stack and workflow — something no other AI coding tool supports at this level.

Domain-Specific Automation

Hooks stop being "a feature" and become transformational when you realize they can encode your domain's rules. Claude Code is not just a coding assistant — with hooks, it becomes your coding assistant, shaped to your industry's exact requirements.

Healthcare / HIPAA

PreToolUse hook scans every file write for potential PHI exposure. Blocks edits to files containing patient data patterns. Logs all file access for compliance audit trail.

Fintech / PCI-DSS

PreToolUse hook requires human confirmation for any edit in payment-related directories. PostToolUse hook runs security linter after every code change. Agent Stop hook verifies no secrets in committed code.

Game Development

PostToolUse hook auto-runs asset pipeline after shader or model file edits. PreToolUse hook prevents changes to locked asset files being edited by other team members.

Monorepo / Platform Teams

PreToolUse hooks enforce package boundaries — block edits crossing module ownership lines. PostToolUse hooks auto-run affected package tests. SessionStart hooks inject team-specific context.

Example: HIPAA compliance hook

#!/bin/bash
# .claude/hooks/hipaa-guard.sh
# Block writes to files that may contain PHI patterns

INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [ "$TOOL" = "Edit" ] || [ "$TOOL" = "Write" ]; then
  # Check if file is in protected patient data directories
  if echo "$FILE_PATH" | grep -qE 'patient_data/|phi/|medical_records/'; then
    echo "BLOCKED: PHI directory requires manual review" >&2
    exit 2
  fi

  # Check file content for SSN/MRN patterns before allowing write
  if [ -f "$FILE_PATH" ]; then
    if grep -qE '[0-9]{3}-[0-9]{2}-[0-9]{4}|MRN-[0-9]+' "$FILE_PATH"; then
      echo "BLOCKED: File contains potential PHI patterns" >&2
      exit 2
    fi
  fi
fi

# Log all operations for compliance
echo "$(date -u +%FT%TZ) $TOOL: $FILE_PATH" >> .claude/hipaa-audit.log
exit 0

Advanced Patterns

Async Hooks for Background Tasks

Set "async": true to run a hook in the background. Claude continues working while the hook executes. Results are delivered on the next conversation turn.

Async PostToolUse — run tests in background

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests-async.sh",
            "async": true,
            "timeout": 300
          }
        ]
      }
    ]
  }
}

Context Re-Injection After Compaction

When Claude's context is compacted, important details can be lost. A SessionStart hook with a compact matcher re-injects critical context automatically.

SessionStart hook — re-inject context after compaction

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'REMINDER: Use Bun, not npm. Run bun test before commits. Current sprint: auth refactor. See CLAUDE.md for full context.'"
          }
        ]
      }
    ]
  }
}

Persist Environment Variables

SessionStart hooks have access to CLAUDE_ENV_FILE. Write export statements to this file and they are available in all subsequent Bash commands.

Set environment variables for the session

#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo 'export NODE_ENV=development' >> "$CLAUDE_ENV_FILE"
  echo 'export DEBUG_LOG=true' >> "$CLAUDE_ENV_FILE"
  echo "export PROJECT_ROOT=$(pwd)" >> "$CLAUDE_ENV_FILE"
fi
exit 0

Hooks in Skills and Agent Frontmatter

Hooks defined in skill or agent YAML frontmatter are scoped to that component's lifecycle. They only run while the skill/agent is active and are cleaned up when it finishes. This is ideal for security checks or validation that only applies during specific workflows.

Configuration Reference

Hooks live in JSON settings files with three levels of nesting: event, matcher group, and handler.

Full hook structure

{
  "hooks": {
    "PostToolUse": [          // 1. Hook event
      {
        "matcher": "Edit|Write", // 2. Matcher (regex)
        "hooks": [              // 3. Handler(s)
          {
            "type": "command",
            "command": "npx prettier --write $(jq -r '.tool_input.file_path')"
          }
        ]
      }
    ]
  }
}

Where to Store Hooks

LocationScopeShareable?
~/.claude/settings.jsonAll your projectsNo (local to machine)
.claude/settings.jsonSingle projectYes (commit to git)
.claude/settings.local.jsonSingle projectNo (gitignored)
Plugin hooks/hooks.jsonWhen plugin enabledYes (bundled with plugin)
Skill/agent frontmatterWhile component activeYes (in file)
Managed policyOrganization-wideYes (admin-controlled)

The fastest way to create a hook is the /hooks interactive menu. Type /hooks in Claude Code to view, add, and delete hooks without editing JSON directly. You can also ask Claude: "Write a hook that runs eslint after every file edit."

Debugging hooks

If your hook is not firing, check three things: (1) the matcher is case-sensitive — bash does not match Bash, (2) your shell profile (~/.zshrc) might have unconditional echo statements that prepend text to JSON output — wrap them in if [[ $- == *i* ]], and (3) check the hook timeout — the default is 60 seconds and long-running hooks will be killed.

Frequently Asked Questions

What are Claude Code hooks?

Claude Code hooks are user-defined shell commands or LLM prompts that execute automatically at specific points in Claude Code's lifecycle. They provide deterministic control — unlike CLAUDE.md which is advisory, hooks guarantee the action happens every time. They are the extensibility feature that makes Claude Code programmable, letting you enforce security policies, automate formatting, and build domain-specific workflows.

How do I set up my first Claude Code hook?

Type /hooks in Claude Code to open the interactive menu. Select an event, set a matcher, and enter a shell command. You can also add hooks manually to .claude/settings.json or ask Claude: "Write a hook that runs eslint after every file edit."

What is the difference between PreToolUse and PostToolUse hooks?

PreToolUse runs before Claude executes a tool and can block it (exit code 2). Ideal for security guards. PostToolUse runs after a tool completes and cannot undo actions. Ideal for formatting, testing, and logging.

How do Claude Code hooks compare to VS Code terminal sandboxing?

VS Code 1.109 introduced terminal sandboxing with a fixed blocklist. Claude Code hooks provide the same safety but are programmable — custom logic per project, per directory, with audit logging, external API integration, and three-way allow/deny/ask decisions. Hooks are proactive policies where sandboxing is reactive blocking.

How do I prevent a Stop hook from running forever?

Check the stop_hook_active field in the JSON input. When it is true, exit 0 immediately to let Claude stop. Without this check, the hook creates an infinite loop. This is the most common mistake new hook authors make.

Can I use hooks with MCP tools?

Yes. MCP tools follow the naming pattern mcp__server__tool. Use regex matchers like mcp__github__.* to match all tools from a specific server, or mcp__.*__write.* to match write operations across all servers.

What is the recommended typecheck hook for TypeScript projects?

Use bun run typecheck:fast with tsgo (the native Go port of the TypeScript type checker) as a Stop hook. It completes in under a second — fast enough to run on every commit. Traditional tsc takes 10-30 seconds and is too slow for a commit hook. Add "typecheck:fast": "tsgo --noEmit" to your package.json scripts. Pair this with a CLAUDE.md instruction to always push to a branch, never directly to main.

Can Claude Code hooks run asynchronously?

Yes. Set "async": true on a command hook. Claude continues working while the hook runs in the background. Results are delivered on the next conversation turn. Async hooks cannot block actions since the triggering action has already completed.

How do hooks enable domain-specific automation?

Hooks encode your domain's rules as deterministic guardrails. Healthcare teams enforce HIPAA compliance on every file write. Fintech teams require security review for payment code. Game studios auto-run asset pipelines after shader edits. Combined with MCP servers for domain-specific tools, hooks transform Claude Code from a general-purpose assistant into a domain-specific development partner.

Pair Hooks with Intelligent Search

WarpGrep gives Claude Code a search subagent that keeps your context clean — the perfect complement to hooks for building a fully automated development workflow.