Related ToolsClaude CodeCursorGithub Copilot

Claude Code Hooks Guide: PreToolUse, PostToolUse, Stop

Published Feb 2, 2026
Updated May 2, 2026
Read Time 10 min read
Author George Mustoe
Advanced Integration

Claude Code hooks are user-defined shell commands that execute automatically at specific points during Claude’s operation. They act as middleware for your AI coding assistant, intercepting actions, modifying behavior, and providing feedback without manual intervention. This guide covers the four hook types - PreToolUse, PostToolUse, Notification, and Stop - plus configuration, practical examples, matchers, and debugging.

In 2026, Claude Code hooks represent one of the most powerful yet underutilized features for developers seeking to customize their AI coding experience. By intercepting tool executions at strategic points, hooks enable you to enforce coding standards, automate repetitive tasks, and create sophisticated workflows that would otherwise require manual intervention.

This deep dive explores every aspect of Claude Code’s hook system, from basic configuration to advanced patterns that can transform how you interact with AI-assisted development.

Claude Code hooks documentation
Claude Code hooks enable automated pre/post tool execution workflows

What Are Claude Code Hooks and Why Do They Matter?

Claude Code Hooks covers the strategies and tools that deliver real productivity gains in this space. In 2026, Claude Code hooks represent one of the most powerful yet underutilized features for. This guide walks through the practical steps from setup through advanced optimization.

Hooks in Claude Code are user-defined shell commands that execute automatically at specific points during Claude’s operation. Think of them as middleware for your AI coding assistant - they intercept actions, can modify behavior, and provide feedback without requiring you to manually intervene each time. The concept is similar to Git hooks, which automate tasks at key points in the Git workflow.

Why Hooks Matter

Without hooks, every interaction with Claude Code is isolated. You might want to:

  • Automatically format code after Claude edits a file
  • Run linters before committing changes
  • Log all file modifications for audit purposes
  • Block certain operations in production directories
  • Enforce team coding standards automatically

Hooks make all of this possible through a declarative configuration that runs behind the scenes.

Hook Types Explained

Claude Code supports four distinct hook types, each serving a specific purpose in the execution lifecycle.

PreToolUse Hooks

PreToolUse hooks execute before Claude runs any tool. They’re your first line of defense for validation and can completely block operations.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "command": "python validate_edit.py \"$FILE_PATH\""
      }
    ]
  }
}

Common use cases include:

  • Validating file paths before edits
  • Checking permissions
  • Enforcing file naming conventions
  • Blocking edits to protected files

PostToolUse Hooks

PostToolUse hooks run after a tool completes successfully. They’re ideal for cleanup, formatting, and verification tasks.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "prettier --write \"$FILE_PATH\""
      }
    ]
  }
}

Use PostToolUse hooks for:

  • Auto-formatting edited files
  • Running tests after code changes
  • Updating documentation
  • Triggering build processes

Notification Hooks

Notification hooks fire when Claude sends messages or completes significant milestones. They’re perfect for external integrations.

{
  "hooks": {
    "Notification": [
      {
        "matcher": "*",
        "command": "notify-send 'Claude Code' \"$MESSAGE\""
      }
    ]
  }
}

Stop Hooks

Stop hooks execute when Claude completes a task or conversation. Use them for cleanup, logging, or triggering follow-up processes.

{
  "hooks": {
    "Stop": [
      {
        "command": "python log_session.py \"$SESSION_ID\""
      }
    ]
  }
}

How Do You Configure Hooks in settings.json?

Hooks are configured in your Claude Code settings file, typically located at .claude/settings.json in your project root or ~/.claude/settings.json for global settings.

Basic Configuration Structure

{
  "hooks": {
    "PreToolUse": [],
    "PostToolUse": [],
    "Notification": [],
    "Stop": []
  }
}

Hook Object Properties

Each hook object supports these properties:

PropertyTypeDescription
matcherstringPattern to match tool names or events
commandstringShell command to execute
timeoutnumberMaximum execution time in milliseconds
environmentobjectAdditional environment variables
Claude Code examples/hooks folder on GitHub
The official Claude Code repository includes example hook scripts in the examples/hooks folder

What Are Some Practical Claude Code Hook Examples?

Let’s explore real-world hooks that solve common development challenges.

Auto-Formatting After Edits

Keep your codebase consistently formatted without manual intervention:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "npx prettier --write \"$FILE_PATH\" 2>/dev/null || true"
      },
      {
        "matcher": "Write",
        "command": "npx prettier --write \"$FILE_PATH\" 2>/dev/null || true"
      }
    ]
  }
}

The || true ensures the hook doesn’t fail if Prettier encounters an unsupported file type.

Enforcing Workflow Steps Before Commits

Prevent commits without proper validation:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "command": "python check_commit_ready.py \"$COMMAND\""
      }
    ]
  }
}

The validation script (check_commit_ready.py):

import sys
import json

command = sys.argv[1] if len(sys.argv) > 1 else ""

if "git commit" in command:
    # Check if tests passed
    # Check if linting passed
    # Verify changelog updated
    checks_passed = run_all_checks()
    if not checks_passed:
        print("BLOCK: Pre-commit checks failed")
        sys.exit(1)

sys.exit(0)

Custom Notifications on Task Completion

Get notified when Claude finishes significant work:

{
  "hooks": {
    "Stop": [
      {
        "command": "curl -X POST https://your-webhook.com/claude-complete -d '{\"session\": \"$SESSION_ID\"}'"
      }
    ]
  }
}

Blocking Dangerous Operations

Protect production directories from accidental modifications:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "command": "python block_production.py \"$FILE_PATH\""
      },
      {
        "matcher": "Write",
        "command": "python block_production.py \"$FILE_PATH\""
      },
      {
        "matcher": "Bash",
        "command": "python check_safe_command.py \"$COMMAND\""
      }
    ]
  }
}

Hook Matchers Deep Dive

Matchers determine which tools trigger your hooks. Understanding matcher patterns is crucial for precise hook targeting.

Exact Tool Matching

Match specific tools by name:

{
  "matcher": "Edit"
}

Wildcard Matching

Match all tools:

{
  "matcher": "*"
}

Multiple Tool Matching

Use arrays for multiple tools (when supported):

{
  "matcher": ["Edit", "Write", "NotebookEdit"]
}

Pattern-Based Matching

Some configurations support glob-style patterns:

{
  "matcher": "Bash:*git*"
}

Environment Variables in Hooks

Claude Code passes contextual information to hooks through environment variables:

VariableDescription
$FILE_PATHPath to the file being operated on
$TOOL_NAMEName of the tool being executed
$COMMANDFor Bash hooks, the command being run
$SESSION_IDCurrent session identifier
$MESSAGEFor notification hooks, the message content

Custom Environment Variables

Add your own variables for hook scripts:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "python format_with_config.py",
        "environment": {
          "CONFIG_PATH": "/path/to/config",
          "STRICT_MODE": "true"
        }
      }
    ]
  }
}

Advanced Hook Patterns

Chaining Multiple Hooks

Execute multiple actions in sequence:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "command": "prettier --write \"$FILE_PATH\""
      },
      {
        "matcher": "Edit",
        "command": "eslint --fix \"$FILE_PATH\""
      },
      {
        "matcher": "Edit",
        "command": "python update_docs.py \"$FILE_PATH\""
      }
    ]
  }
}

Hooks execute in order, and a failure in one doesn’t prevent subsequent hooks from running (unless configured otherwise).

Conditional Execution

Build intelligence into your hooks:

#!/usr/bin/env python
# conditional_format.py
import sys
import os

file_path = os.environ.get('FILE_PATH', '')

# Only format TypeScript and JavaScript
if file_path.endswith(('.ts', '.tsx', '.js', '.jsx')):
    os.system(f'prettier --write "{file_path}"')
elif file_path.endswith('.py'):
    os.system(f'black "{file_path}"')
elif file_path.endswith('.go'):
    os.system(f'gofmt -w "{file_path}"')

sys.exit(0)

Stateful Hooks with External Storage

Track state across hook executions:

#!/usr/bin/env python
# track_edits.py
import json
import os
from datetime import datetime

STATE_FILE = '.claude/edit_history.json'

def load_state():
    if os.path.exists(STATE_FILE):
        with open(STATE_FILE) as f:
            return json.load(f)
    return {'edits': []}

def save_state(state):
    os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
    with open(STATE_FILE, 'w') as f:
        json.dump(state, f, indent=2)

state = load_state()
state['edits'].append({
    'file': os.environ.get('FILE_PATH'),
    'timestamp': datetime.now().isoformat(),
    'tool': os.environ.get('TOOL_NAME')
})
save_state(state)

Debugging Hooks

When hooks don’t behave as expected, use these debugging strategies.

Enable Verbose Logging

Add logging to your hook scripts:

import sys
import os
import logging

logging.basicConfig(
    filename='.claude/hooks.log',
    level=logging.DEBUG,
    format='%(asctime)s - %(message)s'
)

logging.debug(f"Hook triggered: {os.environ}")
logging.debug(f"Arguments: {sys.argv}")

Test Hooks Manually

Run hook commands directly to verify they work:

FILE_PATH="test.ts" python your_hook.py

Check Exit Codes

Hooks use exit codes to communicate:

  • 0: Success, continue execution
  • Non-zero: For PreToolUse, blocks the operation

Common Issues

Hook not triggering:

  • Verify matcher spelling matches exact tool name
  • Check file path in settings.json is correct
  • Ensure the command is executable

Hook blocking unexpectedly:

  • Add || true to commands that might fail
  • Check script exit codes
  • Review stderr output

Best Practices

Keep Hooks Fast

Hooks run synchronously and can slow down your workflow. Keep them under 1-2 seconds:

{
  "timeout": 2000
}

Fail Gracefully

Never let a hook break your entire workflow:

prettier --write "$FILE_PATH" 2>/dev/null || true

Use Project-Level Hooks

Keep hooks in .claude/settings.json within your project for team consistency rather than global settings.

Version Control Hook Scripts

Store hook scripts in your repository:

.claude/
├── settings.json
└── hooks/
    ├── format.py
    ├── validate.py
    └── notify.sh

Document Hook Behavior

Add comments explaining what each hook does:

{
  "hooks": {
    "PostToolUse": [
      {
        "_comment": "Auto-format TypeScript files after editing",
        "matcher": "Edit",
        "command": "prettier --write \"$FILE_PATH\""
      }
    ]
  }
}

Common Pitfalls and How to Avoid Them

After running hooks across several large repos with Claude Code, the same handful of mistakes show up again and again. Reviewing them here will save you a debugging session later.

Pitfall 1: Hooks that block silently. If a PreToolUse hook exits non-zero with no stderr output, Claude Code blocks the operation but the user has no idea why. Always print a one-line reason to stderr before exiting non-zero, even in the happy-path “I am intentionally blocking this” case. The reason text shows up in the Claude UI and gives the human a clear repair action.

Pitfall 2: Synchronous hooks that touch the network. A PostToolUse hook that POSTs to a webhook on every Edit feels harmless until your network has a 30-second timeout and every file save stalls the entire session. If a hook needs to call out, fork it into the background ((curl ... &) >/dev/null 2>&1) or buffer the events to disk and flush them via a Stop hook.

Pitfall 3: Path-based logic that breaks on Windows. Hook scripts written on macOS often use POSIX path separators and $HOME. On Windows under Git Bash or WSL, $FILE_PATH may arrive with backslashes or a C:\Users\... prefix. Normalize paths inside the script (os.path.normpath in Python, path.resolve in Node) before any string comparisons.

Pitfall 4: Hooks that grow into bash megaliths. If your hook command line is more than two pipes long, move it into a versioned script under .claude/hooks/ and call the script from the JSON. Inline shell pipelines are unreadable and impossible to test in isolation; an extracted script can be unit-tested with mocked environment variables.

Pitfall 5: Forgetting to reload settings. Edits to .claude/settings.json only take effect on a fresh Claude Code session. Many “my hook isn’t firing” tickets are really “you edited settings mid-session and the harness still has the old config cached.” Always restart the session after settings changes before debugging further.

How Do Claude Code Hooks Compare to Other AI Coding Tools?

Claude Code’s hook system is notably more flexible than alternatives. GitHub Copilot lacks user-configurable hooks entirely, while Cursor provides limited customization through settings but nothing approaching the programmatic control of Claude Code hooks. This aligns with developer surveys showing that customization and control are top priorities when adopting AI coding tools.

Conclusion

Claude Code hooks transform your AI coding assistant from a reactive tool into a proactive development partner. By automating formatting, enforcing standards, and integrating with your existing toolchain, hooks eliminate repetitive tasks and ensure consistency across your codebase. Teams already running Cursor or GitHub Copilot alongside Claude Code can use the same Prettier/ESLint commands across all three so output stays consistent regardless of which assistant made the edit.

Start with simple PostToolUse hooks for auto-formatting, then gradually add PreToolUse validation as you identify patterns in your workflow. Our Claude Code prompt engineering guide pairs well with hooks - prompts shape intent, hooks enforce output. The investment in hook configuration pays dividends every time Claude makes a change that automatically conforms to your team’s standards.

For more Claude Code customization, explore the official documentation and consider combining hooks with custom slash commands for even more powerful automation.


Frequently Asked Questions

What are Claude Code hooks?

Claude Code hooks are user-defined shell commands that execute automatically at specific points during Claude’s operation. They act as middleware for your AI coding assistant, intercepting actions, modifying behavior, and providing feedback without manual intervention. The concept is similar to Git hooks, which automate tasks at key points in the Git workflow. Unlike Git hooks, Claude Code hooks fire on tool-level events - every Edit, Write, or Bash invocation - giving you fine-grained control over how Claude interacts with your codebase.

What types of Claude Code hooks are available?

Claude Code supports four hook types. PreToolUse hooks run before a tool executes and can block operations entirely by exiting non-zero. PostToolUse hooks run after a tool completes successfully and are ideal for cleanup or formatting. Notification hooks fire on messages and milestones for external integrations like Slack or webhooks. Stop hooks execute when Claude completes a task or conversation, perfect for cleanup, audit logging, or triggering follow-up workflows like CI runs.

Where do you configure Claude Code hooks?

Hooks are configured in your Claude Code settings file, typically located at .claude/settings.json in your project root for project-specific behavior, or ~/.claude/settings.json for global settings. The configuration uses a JSON structure with PreToolUse, PostToolUse, Notification, and Stop arrays. Each hook object supports matcher, command, timeout, and environment properties. Project-level configuration is the better choice for team consistency since the file can be checked into version control.

Can Claude Code hooks enforce coding standards automatically?

Yes. PostToolUse hooks can auto-format edited files with tools like Prettier, and PreToolUse hooks can validate file paths, check permissions, enforce naming conventions, or block edits to protected files. A common pattern is running npx prettier --write on every Edit or Write tool use, with || true appended so the hook does not fail on unsupported file types. For stricter enforcement, combine a PreToolUse validator (rejects bad input) with a PostToolUse formatter (cleans up acceptable input).

How do I debug a hook that is not firing?

Three things to check. First, verify the matcher spelling matches the exact tool name (case-sensitive Edit, not edit). Second, confirm the settings file path is correct - project hooks live at .claude/settings.json and global hooks at ~/.claude/settings.json. Third, run the hook command directly in your shell with the relevant environment variables set (for example, FILE_PATH=test.ts python your_hook.py) to confirm the script itself works. Add file logging to the script for ongoing visibility.


Want to learn more about Claude Code?


External Resources

For official documentation and updates:

Related Guides