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

> Master Claude Code hooks. This guide details writing and configuring hooks for event interception, validation, formatting, and security using JSON contracts and multiple execution types. Boost your Claude integrations.

- Repository: [Luong NGUYEN/claude-howto](https://github.com/luongnv89/claude-howto)
- Tags: how-to-guide
- Published: 2026-03-30

---

**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`](https://github.com/luongnv89/claude-howto/blob/main/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`](https://github.com/luongnv89/claude-howto/blob/main/.claude/settings.json) (repository-specific rules)
3. **Plugin scope**: [`hooks/hooks.json`](https://github.com/luongnv89/claude-howto/blob/main/hooks/hooks.json) (plugin-contained logic)
4. **Component front-matter**: [`SKILL.md`](https://github.com/luongnv89/claude-howto/blob/main/SKILL.md), [`agent.md`](https://github.com/luongnv89/claude-howto/blob/main/agent.md), or [`command.md`](https://github.com/luongnv89/claude-howto/blob/main/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`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/format-code.sh) script demonstrates a `PostToolUse` hook that automatically formats code after Write or Edit operations:

```bash
#!/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`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/validate-prompt.py) file shows a `UserPromptSubmit` hook that prevents destructive operations by analyzing user input:

```python
#!/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`](https://github.com/luongnv89/claude-howto/blob/main/06-hooks/README.md)) runs when the session stops:

```json
{
  "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:

```bash
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`](https://github.com/luongnv89/claude-howto/blob/main/.claude/settings.json) as shown in `06-hooks/README.md#complete-configuration-example`:

```json
{
  "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:

```bash
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`](https://github.com/luongnv89/claude-howto/blob/main/.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`](https://github.com/luongnv89/claude-howto/blob/main/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`](https://github.com/luongnv89/claude-howto/blob/main/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`](https://github.com/luongnv89/claude-howto/blob/main/.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.