How to Write and Configure Claude Code Hooks: A Complete Developer Guide

Claude Code hooks are executable scripts that intercept 25 built-in session events (like PreToolUse and UserPromptSubmit) via JSON stdin/stdout contracts, enabling automated validation, formatting, and security checks with configurable matchers and four execution types (command, HTTP, prompt, agent).

Claude Code hooks provide an event-driven automation layer that runs inside your Claude Code sessions. According to the luongnv89/claude-howto repository, these hooks allow you to enforce coding standards, block dangerous operations, and trigger custom workflows without leaving your editor. They execute at specific lifecycle moments, receiving contextual data via standard input and returning structured responses via standard output.

Understanding the Claude Code Hooks Architecture

The hook system in Claude Code operates through a structured event pipeline defined in 06-hooks/README.md. Understanding these core components is essential before writing your first script.

Event-Driven Execution Model

Claude Code exposes 25 built-in hook events that fire at well-defined moments in a session lifecycle. Critical events include:

  • PreToolUse – Fires before any tool execution (Bash, Write, Edit, etc.), allowing blocking or modification
  • PostToolUse – Fires after tool completion for logging or cleanup
  • UserPromptSubmit – Intercepts user input before processing for content moderation
  • Stop – Triggers when a session ends for final validation or reporting
  • SessionStart and PermissionRequest – Handle initialization and permission workflows

These events are documented in the hook events section of 06-hooks/README.md#hook-events.

The Four Hook Types

Hooks determine how scripts execute through four distinct types:

  • command – Executes a local bash or Python script with full shell access
  • http – POSTs JSON payloads to remote endpoints for external integrations
  • prompt – Evaluates an LLM prompt against the current context for intelligent decisions
  • agent – Spawns a sub-agent with tool access for complex multi-step validation

Each type serves different automation needs, from simple file formatting to complex security scanning.

Matcher Patterns for Targeted Execution

The matcher field controls which specific tool invocations trigger a hook. Patterns support:

  • Exact strings: "Bash" targets only Bash tool calls
  • Regex patterns: "Write|Edit" matches multiple tool types
  • Wildcards: "mcp__memory__.*" captures all MCP memory operations

Matchers are evaluated against the tool_name field in the incoming JSON payload, allowing surgical precision in hook deployment.

Configuration Scope and Precedence

Hooks can be declared across four configuration layers, with later layers overriding earlier ones:

  1. Global user settings: ~/.claude/settings.json (applies to all projects)
  2. Project settings: .claude/settings.json (repository-specific rules)
  3. Plugin scope: hooks/hooks.json (plugin-contained logic)
  4. Component front-matter: SKILL.md, agent.md, or command.md files (task-specific hooks)

Writing Your First Claude Code Hook

All hooks must respect a strict input/output contract regardless of programming language.

The Input/Output Contract

Hooks receive a JSON blob via stdin containing session metadata, tool names, and inputs. They must return JSON on stdout with specific exit codes:

  • Exit code 0: Success (non-blocking) or explicit allow
  • Exit code 2: Blocking error (prevents the original action from executing)
  • HTTP hooks: Must explicitly declare allowedEnvVars for security

The payload includes fields like session_id, tool_name, tool_input, and event-specific context.

Example 1: Auto-Formatting After File Writes

The 06-hooks/format-code.sh script demonstrates a PostToolUse hook that automatically formats code after Write or Edit operations:

#!/bin/bash

# 06-hooks/format-code.sh

INPUT=$(cat)                                   # read whole JSON payload

TOOL=$(echo "$INPUT" | python3 -c "import sys,json;print(json.load(sys.stdin).get('tool_name',''))")
FILE=$(echo "$INPUT" | python3 -c "import sys,json;print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))")

[[ "$TOOL" != "Write" && "$TOOL" != "Edit" ]] && exit 0

case "$FILE" in
  *.js|*.jsx|*.ts|*.tsx|*.json) command -v prettier >/dev/null && prettier --write "$FILE" ;;
  *.py)   command -v black    >/dev/null && black "$FILE" ;;
  *.go)   command -v gofmt   >/dev/null && gofmt -w "$FILE" ;;
esac
exit 0

This script extracts the tool name and file path from stdin, checks if the operation modified a code file, and runs the appropriate formatter.

Example 2: Blocking Dangerous Commands

The 06-hooks/validate-prompt.py file shows a UserPromptSubmit hook that prevents destructive operations by analyzing user input:

#!/usr/bin/env python3
import json, sys, re
BLOCKED = [
    (r"rm\s+-rf\s+/", "Dangerous: rm -rf /"),
    (r"delete\s+database", "Dangerous: DB deletion"),
]
data = json.load(sys.stdin)
prompt = data.get("user_prompt") or data.get("prompt", "")
for pat, msg in BLOCKED:
    if re.search(pat, prompt, re.I):
        print(json.dumps({"decision":"block","reason":msg}))
        sys.exit(0)
sys.exit(0)

When the regex matches dangerous patterns, the hook returns a JSON block decision and exits with code 0 (allowing the hook response to block the action), preventing data loss.

Example 3: LLM-Based Task Validation

For intelligent validation without custom code, use a prompt hook. This configuration snippet (from 06-hooks/README.md) runs when the session stops:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Did Claude complete all requested tasks? List any missing items.",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

This leverages Claude's own reasoning to verify task completion before ending the session.

Configuring Hooks in Claude Code

Global vs. Project-Level Setup

Create a hooks directory and set executable permissions:

mkdir -p ~/.claude/hooks
cp 06-hooks/*.sh ~/.claude/hooks/
chmod +x ~/.claude/hooks/*.sh

For project-specific hooks, use .claude/hooks/ within your repository instead.

Complete Configuration Example

Add the following JSON structure to ~/.claude/settings.json or .claude/settings.json as shown in 06-hooks/README.md#complete-configuration-example:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.py\"",
            "timeout": 10
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/format-code.sh\"",
            "timeout": 30
          },
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/security-scan.py\"",
            "timeout": 10
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/validate-prompt.py\""
          }
        ]
      }
    ]
  }
}

Restart Claude Code or run claude --debug to register the new hooks.

Debugging and Testing Your Hooks

Test hooks independently using sample JSON payloads:

echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' | python3 .claude/hooks/validate-bash.py
echo $?

Enable verbose logging by pressing Ctrl+O inside Claude Code or starting with the --debug flag. The debug output shows hook execution order under "Running hooks for..." messages, helping you verify that matchers trigger correctly and exit codes propagate as expected.

Security Best Practices for Hook Development

Hooks execute with the same permissions as the Claude Code process. Follow these guidelines from 06-hooks/README.md#security-considerations:

  • Validate all external input – Never trust tool_input fields without sanitization
  • Quote shell variables – Use "$VAR" syntax to prevent word-splitting attacks
  • Use $CLAUDE_PROJECT_DIR – Reference files through this environment variable instead of hardcoding absolute paths
  • Restrict environment variables – For HTTP hooks, explicitly list only required variables in allowedEnvVars
  • Exclude secrets – Ensure hooks never read .env, .git/, or private key files unless specifically designed for credential management

Summary

  • Claude Code hooks intercept 25 session events via JSON stdin/stdout contracts, enabling event-driven automation
  • Four hook types (command, HTTP, prompt, agent) support diverse execution models from shell scripts to LLM evaluation
  • Matcher patterns (exact, regex, wildcard) provide surgical control over which tool invocations trigger hooks
  • Configuration layers range from global ~/.claude/settings.json to project-specific .claude/settings.json and component front-matter
  • Exit code 2 blocks operations for PreToolUse and UserPromptSubmit events, while code 0 allows continuation
  • Security requires input validation, quoted variables, and $CLAUDE_PROJECT_DIR usage to prevent path traversal and injection attacks

Frequently Asked Questions

How do I block a dangerous command before it executes?

Create a PreToolUse hook with a matcher targeting the specific tool (e.g., "Bash"), then implement validation logic that inspects tool_input.command from the JSON stdin. Return exit code 2 or output {"decision":"block","reason":"..."} to prevent execution. The 06-hooks/validate-prompt.py example demonstrates pattern matching for dangerous commands.

Can I use Claude Code hooks to automatically format code after every file save?

Yes. Configure a PostToolUse hook with the matcher "Write|Edit" that executes a command hook script. The 06-hooks/format-code.sh reference implementation reads the file path from stdin and runs prettier, black, or gofmt based on file extension. Set the timeout appropriately (30 seconds recommended) to prevent blocking on large files.

What is the difference between command hooks and prompt hooks?

Command hooks execute local shell or Python scripts with full access to the filesystem and environment, suitable for formatting, linting, or file system operations. Prompt hooks send a text prompt to the LLM for evaluation and return the response, ideal for semantic analysis, task completion verification, or content moderation without writing custom parsing code.

Where should I store my hook scripts for team-wide access?

Store hooks in the .claude/hooks/ directory within your project repository and reference them using "$CLAUDE_PROJECT_DIR/.claude/hooks/script-name" in your .claude/settings.json. This ensures all team members use identical validation logic when working in the same repository, while global hooks in ~/.claude/hooks/ remain specific to individual developer preferences.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →