CLAUDE-CODE
9 MIN READ

Claude Code Hooks: The Complete Setup Guide for 2026

V
THE VIBE CODE
Hero image for Claude Code Hooks: The Complete Setup Guide for 2026

Hooks are the feature that separates Claude Code users who trust their AI from those constantly reviewing every line. They let you run validation, formatting, and custom scripts automatically as Claude works. No manual checks. No forgotten linting. Just code that meets your standards on the first pass.

Most developers skip hooks because the docs feel abstract. This guide covers the practical setup: what each hook type does, how exit codes control Claude's behavior, and copy-paste configs you can use today.

What Hooks Actually Do

Hooks are shell commands or prompts that run at specific points during a Claude Code session. You configure them in ~/.claude/settings.json or project-level .claude/settings.json.

Claude Code supports 12 hook types:

  • SessionStart: Session begins or resumes
  • UserPromptSubmit: User submits a prompt
  • PreToolUse: Before tool execution
  • PermissionRequest: When permission dialog appears
  • PostToolUse: After tool succeeds
  • PostToolUseFailure: After tool fails
  • SubagentStart: When spawning a subagent
  • SubagentStop: When subagent finishes
  • Stop: Claude finishes responding
  • PreCompact: Before context compaction
  • SessionEnd: Session terminates
  • Notification: Claude Code sends notifications

Each hook can either approve the action, deny it with feedback, or run silently in the background.

The Exit Code Behavior Nobody Explains

This is the most misunderstood part of hooks. The exit code from your script determines what Claude sees:

The breakdown:

  • Exit 0: Success. Claude does NOT see stdout (except for UserPromptSubmit and SessionStart hooks where stdout becomes context). The tool proceeds normally.
  • Exit 2: Blocking error. Claude sees your stderr message directly and stops the action.
  • Other exit codes: Non-blocking error. Shows to the user in verbose mode but execution continues.

This means if you want to give Claude feedback, you write to stderr and exit with code 2. If you just want logging for yourself, write to stdout and exit 0.

Basic Setup: Auto-Format on Every Edit

Let's start with the most common use case: running Prettier after Claude edits a JavaScript or TypeScript file.

Add this to ~/.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'input=$(cat); fp=$(echo \"$input\" | jq -r \".tool_input.file_path\"); if echo \"$fp\" | grep -qE \"\\.(ts|tsx|js|jsx)$\"; then prettier --write \"$fp\" 2>&1 | head -5; fi'"
          }
        ],
        "description": "Auto-format JS/TS files after edits"
      }
    ]
  }
}

The matcher field matches tool names. It supports:

  • Simple strings: Write matches only the Write tool
  • Regex patterns: Edit|Write matches both
  • Wildcards: * matches all tools

Since matchers only filter by tool name, you do file path filtering inside the hook script by parsing the JSON input from stdin with jq.

Block Dangerous Commands

PreToolUse hooks can prevent Claude from doing something before it happens. This is useful for security.

Here's a hook that blocks commits containing API keys:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'input=$(cat); cmd=$(echo \"$input\" | jq -r \".tool_input.command\"); if echo \"$cmd\" | grep -q \"git commit\"; then if git diff --cached | grep -qE \"(API_KEY|SECRET|PASSWORD)=\"; then echo \"Blocked: staged changes contain secrets\" >&2; exit 2; fi; fi'"
          }
        ],
        "description": "Block commits with secrets"
      }
    ]
  }
}

When this hook exits with code 2, Claude sees the stderr message and knows not to proceed. It will typically offer to help you fix the issue.

Console.log Warning Hook

Here's a practical hook that warns about console.log statements without blocking the edit:

{
  "matcher": "Edit|Write",
  "hooks": [
    {
      "type": "command",
      "command": "bash -c 'input=$(cat); fp=$(echo \"$input\" | jq -r \".tool_input.file_path\"); if echo \"$fp\" | grep -qE \"\\.(ts|tsx|js|jsx)$\"; then if grep -n \"console\\.log\" \"$fp\" 2>/dev/null; then echo \"[Hook] WARNING: console.log found\" >&2; fi; fi'"
    }
  ],
  "description": "Warn about console.log statements"
}

This writes to stderr but exits 0, so the edit completes and you see the warning.

Hook Input Format

Your hook scripts receive JSON input via stdin. Here's what it looks like for an Edit tool call:

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../session.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PostToolUse",
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "/path/to/file.ts",
    "old_string": "...",
    "new_string": "..."
  },
  "tool_response": {
    "success": true
  }
}

Parse this with jq in your shell scripts:

#!/bin/bash
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
tool_name=$(echo "$input" | jq -r '.tool_name')

# Your validation logic here

The only environment variable available is $CLAUDE_PROJECT_DIR, which contains the absolute path to the project root.

Multi-Stage Validation

For complex validation, chain multiple hooks. Quick checks run first, expensive checks only run if needed:

{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/quick-check.sh",
          "timeout": 5
        },
        {
          "type": "command",
          "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/deep-check.sh",
          "timeout": 30
        }
      ]
    }
  ]
}

The quick-check.sh script can approve safe commands immediately (exit 0) and let the deep check handle complex cases. All matching hooks run in parallel by default.

Prompt-Based Hooks for Stop Events

For Stop and SubagentStop hooks, you can use an LLM to evaluate whether Claude should continue working:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Evaluate if Claude should stop. Context: $ARGUMENTS. Check if all tasks are complete. Respond with JSON: {\"ok\": true} to allow stopping, or {\"ok\": false, \"reason\": \"explanation\"} to continue.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

This sends the context to a fast LLM (Haiku) which decides whether to let Claude stop or keep working.

Complete Production Setup

Here's a full settings.json combining common hooks:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'input=$(cat); cmd=$(echo \"$input\" | jq -r \".tool_input.command\"); if echo \"$cmd\" | grep -q \"git commit\"; then if git diff --cached | grep -qE \"(API_KEY|SECRET|PASSWORD)=\"; then echo \"Blocked: secrets in staged changes\" >&2; exit 2; fi; fi'"
          }
        ],
        "description": "Block commits with secrets"
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'input=$(cat); fp=$(echo \"$input\" | jq -r \".tool_input.file_path\"); if echo \"$fp\" | grep -qE \"\\.(ts|tsx|js|jsx)$\"; then prettier --write \"$fp\" 2>&1 | head -5; fi'"
          }
        ],
        "description": "Auto-format JS/TS files"
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'input=$(cat); cmd=$(echo \"$input\" | jq -r \".tool_input.command\"); output=$(echo \"$input\" | jq -r \".tool_response.output // empty\"); if echo \"$cmd\" | grep -qE \"gh pr create\"; then pr_url=$(echo \"$output\" | grep -oE \"https://github.com/[^/]+/[^/]+/pull/[0-9]+\"); [ -n \"$pr_url\" ] && echo \"[Hook] PR created: $pr_url\" >&2; fi'"
          }
        ],
        "description": "Log PR URL after creation"
      }
    ]
  }
}

Hooks in Skills and Subagents

Hooks can also be defined in skill and subagent frontmatter. This lets specialized agents have their own validation:

The Claude Code Feature Senior Engineers Keep Missing

---
name: code-reviewer
description: Reviews code for quality issues
model: inherit
tools: ["Read", "Grep", "Glob"]
hooks:
  PostToolUse:
    - matcher: "Read"
      hooks:
        - type: command
          command: "echo 'File read for review' >> /tmp/review.log"
---

These hooks are scoped to the component's lifecycle and only run when that component is active. Supported events are PreToolUse, PostToolUse, and Stop.

Common Issues

Hooks Seem to Be Ignored

Claude sometimes works around hooks if they're too restrictive:

Solutions:

  • Make hooks specific rather than broad
  • Use PreToolUse for hard blocks, PostToolUse for soft warnings
  • Combine hooks with CLAUDE.md instructions for context

Hooks Slow Down Sessions

If your session feels sluggish:

  • Add timeout values to prevent hanging (default is 60 seconds)
  • Move expensive validation to PostToolUse instead of PreToolUse
  • All matching hooks run in parallel, so splitting into multiple hooks doesn't help speed

Exit Codes Not Working as Expected

Remember:

  • Exit 0 = success, Claude doesn't see stdout (except UserPromptSubmit/SessionStart)
  • Exit 2 = deny, Claude sees stderr
  • Other codes = warning, user sees it in verbose mode but action continues

If Claude isn't seeing your feedback, check you're writing to stderr (>&2) and exiting with code 2.

What to Hook First

Start simple. Add one hook at a time and verify it works before adding more.

Recommended order:

  1. Auto-format: PostToolUse on Edit|Write for Prettier/ESLint fix
  2. Secret scanning: PreToolUse on Bash for git commit
  3. TypeScript check: PostToolUse on Edit|Write for .ts files
  4. Custom validation: Whatever your project needs

The npx template library has 43 pre-built hooks if you want to skip the manual setup:

npx claude-code-templates@latest

Summary

Hooks transform Claude Code from a helpful assistant into a production-grade development environment. The key insights:

  • Exit codes control what Claude sees: 0 = silent success, 2 = deny with feedback
  • Matchers filter by tool name only (Write, Edit|Write, Bash, etc.)
  • Do file path filtering inside your hook script using jq
  • PreToolUse blocks actions before they happen
  • PostToolUse validates and formats after the fact
  • Prompt-based hooks work for Stop/SubagentStop events
  • Start with auto-formatting and secret scanning

Hooks are the difference between hoping Claude follows your standards and knowing it does. Set them up once and forget about manual code review.

For the official documentation, see code.claude.com/docs.