---
name: hook-builder
description: Create Claude Code hooks for automated validation, context injection, and workflow automation. Use when adding git hooks, file validation, automated testing, or custom tool permissions. References complete Anthropic documentation and production examples.
---

# Hook Builder

Build hooks that automate validation, inject context, and integrate custom workflows into Claude Code's lifecycle.

## When to Use

- Add automated validation (type checks, linters, security scans)
- Inject dynamic context before/after tool execution
- Auto-approve safe operations (documentation reads, standard commands)
- Integrate external tools and workflows
- Implement custom permission logic
- Monitor and log Claude Code operations

## Hook Events Overview

Claude Code supports these hook events:

**Tool Events**:
- `PreToolUse` - Before tool execution (can block)
- `PostToolUse` - After tool execution (can add context)
- `Notification` - Claude requests permission or waits

**Prompt & Session Events**:
- `UserPromptSubmit` - User submits prompt (can block or add context)
- `SessionStart` - Session initialization
- `SessionEnd` - Session termination

**Control Events**:
- `Stop` - Main agent finishes responding
- `SubagentStop` - Subagent completes
- `PreCompact` - Before context compaction

## Quick Start Workflow

### 1. Design Your Hook

**What do you want to automate?**
- Validation (exit code 2 = block with stderr to Claude)
- Context injection (exit code 0 = success, stdout becomes context)
- Auto-approval (exit code 0 with JSON decision)
- Monitoring (exit code 0, log to file)

**Which event?**
- `PreToolUse` if validating before tool runs
- `PostToolUse` if adding context after tool completes
- `UserPromptSubmit` if validating user input

### 2. Write Hook Script

Hooks receive JSON via stdin and communicate via stdout/stderr + exit codes.

**Basic structure**:
```python
#!/usr/bin/env python3
import json
import sys

# Read hook input
try:
    input_data = json.load(sys.stdin)
except json.JSONDecodeError:
    sys.exit(1)

# Extract relevant fields
hook_event = input_data.get("hook_event_name")
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})

# Your validation/logic here
if should_block:
    print("Error message to Claude", file=sys.stderr)
    sys.exit(2)  # Block with feedback

# Success with optional context
print("Context for Claude")
sys.exit(0)
```

### 3. Configure in settings.json

Add hook to `.claude/settings.json` or `.claude/settings.local.json`:

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/your-hook.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}
```

### 4. Test

Make a change that triggers the hook and verify:
- Hook executes at correct event
- Stdin JSON is parsed correctly
- Exit codes behave as expected
- Claude sees stdout/stderr appropriately

## Essential Patterns

### Pattern 1: Type Check After Edits (Real Example)

**Lever repo's typecheck.sh** (production-grade hook):

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|MultiEdit|Write",
        "hooks": [
        {
            "type": "command",
            "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/typecheck.sh",
            "timeout": 20
        }
        ]
      }
    ]
  }
}
```

**Key features**:
- Runs `pnpm check` after file edits
- Only on `.ts`/`.tsx` files (exits early for others)
- Finds nearest `package.json` for scoped checking
- Formats errors for Claude (JSON to stderr)
- Exit code 2 ensures Claude sees errors
- 3 output modes: simple, balanced, advanced

See `hooks/scripts/typecheck.sh` for a production-grade implementation (434 lines, production-tested).

### Pattern 2: Validate Bash Commands

```python
#!/usr/bin/env python3
import json
import re
import sys

RULES = [
    (r"\bgrep\b(?!.*\|)", "Use Grep tool instead of grep command"),
    (r"\bfind\b", "Use Glob tool instead of find command"),
]

input_data = json.load(sys.stdin)
if input_data.get("tool_name") != "Bash":
    sys.exit(0)

command = input_data.get("tool_input", {}).get("command", "")

issues = [msg for pattern, msg in RULES if re.search(pattern, command)]

if issues:
    for msg in issues:
        print(f"• {msg}", file=sys.stderr)
    sys.exit(2)  # Block with feedback

sys.exit(0)
```

### Pattern 3: Auto-Approve Documentation Reads

```python
#!/usr/bin/env python3
import json
import sys

input_data = json.load(sys.stdin)

if input_data.get("tool_name") == "Read":
    file_path = input_data.get("tool_input", {}).get("file_path", "")
    if file_path.endswith((".md", ".txt", ".json")):
        output = {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "allow",
                "permissionDecisionReason": "Auto-approved docs"
            },
            "suppressOutput": True
        }
        print(json.dumps(output))
        sys.exit(0)

sys.exit(0)
```

### Pattern 4: Inject Dynamic Context

```python
#!/usr/bin/env python3
import json
import sys
import datetime

input_data = json.load(sys.stdin)

# Add timestamp context
context = f"Current time: {datetime.datetime.now()}"
print(context)
sys.exit(0)
```

### Pattern 5: Block Prompts with Secrets

```python
#!/usr/bin/env python3
import json
import sys
import re

input_data = json.load(sys.stdin)
prompt = input_data.get("prompt", "")

if re.search(r"(?i)\b(password|secret|key|token)\s*[:=]", prompt):
    output = {
        "decision": "block",
        "reason": "Security policy: remove secrets from prompt"
    }
    print(json.dumps(output))
    sys.exit(0)

sys.exit(0)
```

## Hook Input Contracts

All hooks receive JSON via stdin:

```json
{
  "session_id": "string",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/working/directory",
  "permission_mode": "default|plan|acceptEdits|bypassPermissions",
  "hook_event_name": "EventName"
}
```

**PreToolUse adds**:
```json
{
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path",
    "content": "..."
  }
}
```

**UserPromptSubmit adds**:
```json
{
  "prompt": "User's text prompt"
}
```

See [references/anthropic-hooks-docs.md](references/anthropic-hooks-docs.md) for all event-specific fields.

## Hook Output Contracts

**Exit Codes**:
- `0` - Success (stdout shown in transcript or becomes context)
- `2` - Blocking error (stderr goes to Claude for handling)
- Other - Non-blocking error (stderr shown to user only)

**JSON Output** (optional):
```json
{
  "continue": true,
  "stopReason": "Optional message",
  "suppressOutput": false,
  "systemMessage": "Optional warning",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow|deny|ask",
    "permissionDecisionReason": "Explanation"
  }
}
```

**PreToolUse Decision**:
```json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",  // or "deny" or "ask"
    "permissionDecisionReason": "Auto-approved docs"
  }
}
```

**PostToolUse Context**:
```json
{
  "decision": "block",  // optional
  "reason": "Explanation",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Info for Claude"
  }
}
```

## Matchers

**Tool matchers** (PreToolUse, PostToolUse):
- Exact: `"Write"` matches only Write tool
- Regex: `"Edit|Write"` or `"Notebook.*"`
- Wildcard: `"*"` or `""` matches all tools
- MCP: `"mcp__memory__.*"` or `"mcp__.*__write.*"`

**Event matchers** (SessionStart):
- `"startup"`, `"resume"`, `"clear"`, `"compact"`

**Example**:
```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "./validate-bash.sh" }]
      },
      {
        "matcher": "mcp__.*",
        "hooks": [{ "type": "command", "command": "./log-mcp-use.sh" }]
      }
    ]
  }
}
```

## Hook Locations

**Project hooks** (`.claude/settings.json`):
- Shared with team via git
- Apply to project-specific workflows
- Higher precedence than user hooks

**User hooks** (`~/.claude/settings.json`):
- Apply across all projects
- Personal automation preferences

**Plugin hooks** (`plugins/{name}/hooks/hooks.json`):
- Distributed via marketplace
- Auto-installed with plugin

## Environment Variables

**Available in hook scripts**:
- `CLAUDE_PROJECT_DIR` - Project root directory
- `CLAUDE_PLUGIN_ROOT` - Plugin installation directory (plugin hooks only)
- `CLAUDE_CODE_REMOTE` - Set to "true" in web environments
- `CLAUDE_ENV_FILE` - Path to env file for SessionStart hooks

**SessionStart env persistence**:
```bash
#!/bin/bash

if [ -n "$CLAUDE_ENV_FILE" ]; then
  echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
  echo 'export API_KEY=value' >> "$CLAUDE_ENV_FILE"
fi

exit 0
```

## Security Best Practices

**Critical**: Hooks execute arbitrary shell commands automatically.

1. **Validate inputs** - Never trust stdin JSON blindly
2. **Quote variables** - Always `"$VAR"` not `$VAR`
3. **Block traversal** - Check for `..` in paths
4. **Use absolute paths** - Specify full script locations
5. **Skip sensitive files** - Ignore `.env`, `.git/`, credentials
6. **Sanitize outputs** - Escape special characters for JSON
7. **Limit scope** - Don't grant more permissions than needed

## Debugging

**Run with debug flag**:
```bash
claude --debug
```

**Check transcript mode** (Ctrl-R):
- See hook execution details
- View stdin/stdout/stderr
- Check exit codes

**Common issues**:
- Hook not triggering: Check matcher pattern
- Permission errors: Verify allowed-tools in settings
- Timeout: Increase timeout field (default 60s)
- JSON errors: Validate stdin parsing

## Testing Workflow

1. **Write hook script** with proper stdin/stdout/stderr handling
2. **Test standalone** with mock JSON via stdin
3. **Add to settings.json** with appropriate matcher
4. **Trigger event** (edit file, submit prompt, etc.)
5. **Verify behavior** in transcript or debug output
6. **Iterate** based on results

## Official Documentation

For the complete, up-to-date hooks specification, use the `claude-code-docs-reference` skill or the `claude-code-guide` agent to look up official Anthropic documentation on hooks.

## Production Example

**Study the included `hooks/scripts/typecheck.sh`** for production patterns:
```bash
hooks/scripts/typecheck.sh
```

Key learnings:
- Robust error handling
- Conditional execution (skip non-TS files)
- Scoped operations (nearest package.json)
- Structured JSON output for Claude
- Multiple output modes (simple/balanced/advanced)
- Exit code discipline

## Improving This Skill

Notice gaps, unclear instructions, or better patterns while using this skill?

Use the `continuous-improvement` skill to propose enhancements:
1. Detect the issue
2. Propose specific changes
3. Git branch → commit → surface for approval
4. Update changelog

This skill should evolve with every use.
