Claude Code Hooks Guide: Automate Custom Workflows with Pre and Post Execution Hooks

What Are Claude Code Hooks and Why They Matter

Claude Code hooks are automated shell commands that execute in response to specific events during a Claude Code session. When Claude calls a tool (like Edit, Write, or Bash), hooks can run commands before or after the tool execution. This turns Claude Code from a standalone AI assistant into an integrated development workflow engine.

Think of hooks as CI/CD for your AI coding sessions. Just as a pre-commit hook runs linting before allowing a commit, a Claude Code PreToolUse hook can run validation before Claude writes a file. Just as a post-deploy hook sends a Slack notification, a PostToolUse hook can notify your team when Claude completes a significant change.

Common hook use cases:

  • Auto-linting: run Prettier or ESLint after every file write
  • Type checking: run tsc after TypeScript file modifications
  • Test execution: run affected tests after code changes
  • Notifications: send Slack/Discord messages when tasks complete
  • Logging: track what Claude modifies for audit purposes
  • Guardrails: block changes to protected files or directories

Hook Architecture

Hook Types

Claude Code supports three hook types:

PreToolUse: fires before a tool executes. Can block the tool if the hook fails (non-zero exit code).

PostToolUse: fires after a tool executes. Cannot block the tool (it already ran) but can trigger follow-up actions.

Notification: fires when Claude produces specific output or completes a task.

Hook Configuration Structure

Hooks are defined in settings.json (project-level at .claude/settings.json or global at ~/.claude/settings.json):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "echo 'File modification detected'",
        "timeout": 10000
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "/path/to/your/script.sh",
        "timeout": 30000
      }
    ]
  }
}

Matcher Patterns

The matcher field determines which tool calls trigger the hook:

  • "Edit" — matches only the Edit tool
  • "Write" — matches only the Write tool
  • "Edit|Write" — matches Edit or Write
  • "Bash" — matches Bash tool calls
  • ".*" — matches all tool calls (use with caution)

Environment Variables Available to Hooks

When a hook runs, Claude Code passes context through environment variables:

  • CLAUDE_TOOL_NAME — which tool was called (Edit, Write, Bash, etc.)
  • CLAUDE_FILE_PATH — the file being modified (for Edit/Write)
  • CLAUDE_TOOL_INPUT — JSON string of the tool’s input parameters
  • CLAUDE_SESSION_ID — unique identifier for the current session

Setting Up Common Hook Workflows

Auto-Format on File Write

Run Prettier after every file modification:

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

The || true ensures the hook does not fail if Prettier encounters an unsupported file type.

Type Check After TypeScript Changes

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" == *.ts ]] || [[ \"$CLAUDE_FILE_PATH\" == *.tsx ]]; then npx tsc --noEmit --pretty 2>&1 | head -20; fi",
        "timeout": 30000
      }
    ]
  }
}

This only runs tsc when TypeScript files are modified, and limits output to 20 lines to avoid flooding the conversation.

Run Affected Tests

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" == *.ts ]] || [[ \"$CLAUDE_FILE_PATH\" == *.tsx ]]; then npx vitest related \"$CLAUDE_FILE_PATH\" --run 2>&1 | tail -10; fi",
        "timeout": 60000
      }
    ]
  }
}

vitest related runs only the tests that import the modified file — fast and targeted.

Protect Sensitive Files

Block Claude from modifying certain files:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "if echo \"$CLAUDE_FILE_PATH\" | grep -qE '\\.(env|pem|key)$|secrets/|credentials'; then echo 'BLOCKED: Cannot modify sensitive files' >&2; exit 1; fi",
        "timeout": 5000
      }
    ]
  }
}

A non-zero exit code from a PreToolUse hook blocks the tool execution. Claude will see the error message and adjust its approach.

Slack Notification on Task Completion

{
  "hooks": {
    "Notification": [
      {
        "matcher": ".*",
        "command": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"text\":\"Claude Code completed a task in '\"$PWD\"'\"}' $SLACK_WEBHOOK_URL",
        "timeout": 5000
      }
    ]
  }
}

Git Auto-Stage After Changes

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "git add \"$CLAUDE_FILE_PATH\" 2>/dev/null || true",
        "timeout": 5000
      }
    ]
  }
}

This auto-stages every file Claude modifies, making it easy to see all changes with git diff --staged.

Advanced Hook Patterns

Conditional Hooks Based on File Type

Create a hook script that handles different file types:

#!/bin/bash
# hooks/post-edit.sh

FILE="$CLAUDE_FILE_PATH"
EXT="${FILE##*.}"

case "$EXT" in
  ts|tsx)
    npx prettier --write "$FILE" 2>/dev/null
    npx tsc --noEmit --pretty 2>&1 | head -10
    ;;
  py)
    python -m black "$FILE" 2>/dev/null
    python -m mypy "$FILE" --no-error-summary 2>&1 | head -10
    ;;
  css|scss)
    npx prettier --write "$FILE" 2>/dev/null
    npx stylelint "$FILE" 2>&1 | head -5
    ;;
  sql)
    # Validate SQL syntax
    echo "$FILE modified - manual review recommended for SQL changes"
    ;;
esac

Reference it in settings:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "bash .claude/hooks/post-edit.sh",
        "timeout": 30000
      }
    ]
  }
}

Audit Logging

Track every modification Claude makes:

#!/bin/bash
# hooks/audit-log.sh

LOG_FILE=".claude/audit.log"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

echo "$TIMESTAMP | $CLAUDE_TOOL_NAME | $CLAUDE_FILE_PATH | session:$CLAUDE_SESSION_ID" >> "$LOG_FILE"

Dependency Check After Package Changes

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "if [[ \"$CLAUDE_FILE_PATH\" == *package.json ]]; then npm ls --depth=0 2>&1 | grep -i 'ERR\\|WARN' | head -5; fi",
        "timeout": 30000
      }
    ]
  }
}

Hook Configuration Best Practices

Keep Hooks Fast

Hooks run synchronously — a slow hook blocks Claude. Keep hooks under 10 seconds for PreToolUse and under 30 seconds for PostToolUse. If a process takes longer, run it asynchronously:

"command": "nohup bash .claude/hooks/slow-check.sh &>/dev/null &"

Handle Errors Gracefully

Always add || true or proper error handling to prevent hooks from crashing the session:

"command": "npx prettier --write \"$CLAUDE_FILE_PATH\" 2>/dev/null || true"

Use Project-Level Hooks

Store hooks in .claude/settings.json at the project root so they are:

  • Version-controlled with the project
  • Shared automatically with all team members
  • Specific to the project’s technology stack

Test Hooks Independently

Before adding a hook to settings, test it manually:

CLAUDE_FILE_PATH="src/app.ts" CLAUDE_TOOL_NAME="Edit" bash .claude/hooks/post-edit.sh

Document Your Hooks

Add a comment section to your settings.json or create a .claude/HOOKS.md explaining what each hook does and why.

Troubleshooting Common Hook Issues

Hook Does Not Fire

  • Verify the matcher pattern matches the tool name exactly
  • Check that the settings.json is in the correct location
  • Restart Claude Code after modifying settings.json

Hook Blocks Tool Execution Unexpectedly

  • PreToolUse hooks block on non-zero exit codes
  • Add || true if the hook should warn but not block
  • Check that error output goes to stderr, not stdout

Hook Output Clutters the Conversation

  • Redirect verbose output: command 2>/dev/null
  • Limit output: command | head -5
  • Send output to a log file instead: command >> .claude/hook.log 2>&1

Hook Timeout

  • Increase the timeout value (in milliseconds)
  • For slow operations, run asynchronously with nohup ... &
  • Optimize the hook script (avoid unnecessary operations)

Frequently Asked Questions

Can hooks modify Claude’s behavior?

PreToolUse hooks can block tool calls by returning a non-zero exit code. The error message (stderr) is shown to Claude, which can adjust its approach. PostToolUse hooks cannot modify what Claude already did but can trigger side effects.

Are hooks shared when pairing with Claude Code?

Hooks in .claude/settings.json (project-level) are shared with anyone who clones the repository. Hooks in ~/.claude/settings.json (global) are personal to each user.

Can I use hooks with Claude Code in VS Code?

Yes. The VS Code extension reads the same settings.json files. Hooks work identically in the terminal CLI and VS Code extension.

Do hooks work with all Claude Code tools?

Yes. The matcher can target any tool: Edit, Write, Bash, Read, Glob, Grep, and any MCP tools. Use the tool name as the matcher string.

Can hooks access the content Claude is writing?

The CLAUDE_TOOL_INPUT environment variable contains the JSON input parameters, which includes the content for Write/Edit operations. Parse it with jq or your preferred JSON tool.

Is there a performance impact from running hooks?

Yes. Each hook adds execution time to every matching tool call. Keep hooks fast (under 5 seconds) and use specific matchers to avoid running hooks unnecessarily.

Explore More Tools

Antigravity AI Content Pipeline Automation Guide: Google Docs to WordPress Publishing Workflow Guide Bolt.new Case Study: Marketing Agency Built 5 Client Dashboards in One Day Case Study Bolt.new Best Practices: Rapid Full-Stack App Generation from Natural Language Prompts Best Practices ChatGPT Advanced Data Analysis (Code Interpreter) Complete Guide: Upload, Analyze, Visualize Guide ChatGPT Custom GPTs Advanced Guide: Actions, API Integration, and Knowledge Base Configuration Guide ChatGPT Voice Mode Guide: Build Voice-First Customer Service and Internal Workflows Guide Claude API Production Chatbot Guide: System Prompt Architecture for Reliable AI Assistants Guide Claude Artifacts Best Practices: Create Interactive Dashboards, Documents, and Code Previews Best Practices Claude MCP Server Setup Guide: Build Custom Tool Integrations for Claude Code and Claude Desktop Guide Cursor Composer Complete Guide: Multi-File Editing, Inline Diffs, and Agent Mode Guide Cursor Case Study: Solo Founder Built a Next.js SaaS MVP in 2 Weeks with AI-Assisted Development Case Study Cursor Rules Advanced Guide: Project-Specific AI Configuration and Team Coding Standards Guide Devin AI Team Workflow Integration Best Practices: Slack, GitHub, and Code Review Automation Best Practices Devin Case Study: Automated Dependency Upgrade Across 500-Package Python Monorepo Case Study ElevenLabs Case Study: EdTech Startup Localized 200 Course Hours to 8 Languages in 6 Weeks Case Study ElevenLabs Multilingual Dubbing Guide: Automated Video Localization Workflow for Global Content Guide ElevenLabs Voice Design Complete Guide: Create Consistent Character Voices for Games, Podcasts, and Apps Guide Gemini 2.5 Pro vs Claude Sonnet 4 vs GPT-4o: AI Code Generation Comparison 2026 Comparison Gemini API Multimodal Developer Guide: Image, Video, and Document Analysis with Code Examples Guide Gemini Google Workspace Automation Guide: Docs, Sheets, and Slides AI Workflows Guide