Creating Hooks

Build event-driven hooks in Claude Code for validation, setup, and automation. Use when you need to validate inputs, check environment state, or automate tasks at specific lifecycle events.

7 stars

Best use case

Creating Hooks is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Build event-driven hooks in Claude Code for validation, setup, and automation. Use when you need to validate inputs, check environment state, or automate tasks at specific lifecycle events.

Teams using Creating Hooks should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/creating-hooks/SKILL.md --create-dirs "https://raw.githubusercontent.com/jack-michaud/faire/main/jack-software/skills/creating-hooks/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/creating-hooks/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How Creating Hooks Compares

Feature / AgentCreating HooksStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Build event-driven hooks in Claude Code for validation, setup, and automation. Use when you need to validate inputs, check environment state, or automate tasks at specific lifecycle events.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Creating Hooks

## Overview

Hooks are event-driven scripts that execute at specific points in Claude Code's lifecycle. They receive JSON input with session data and event-specific information, enabling validation, environment checks, and automated workflows.

## When to Use

- Validate tool inputs before execution (PreToolUse)
- Verify outputs after tool completion (PostToolUse)
- Check environment state before processing prompts (UserPromptSubmit)
- Initialize resources at session startup (SessionStart)
- Clean up resources at session end (SessionEnd)

## Hook Types

### PreToolUse
Runs before tool execution. Use for:
- Environment validation (check dependencies exist)
- Input validation (verify paths, parameters)
- Permission checks (ensure access rights)
- State verification (git status, working directory)

### PostToolUse
Runs after tool completion. Use for:
- Output validation (verify file changes)
- Quality checks (run linters, formatters)
- Side effects (update logs, metrics)
- Failure detection (check for errors)

### UserPromptSubmit
Runs before processing user input. Use for:
- Input sanitization
- Context injection
- Usage tracking
- Cost estimation

### SessionStart
Runs at session initialization. Use for:
- Environment setup
- Dependency checks
- Configuration loading
- Initialization logging

### SessionEnd
Runs at session termination. Use for:
- Cleanup tasks
- Result archiving
- Metrics reporting
- Resource deallocation

## JSON Input Structure

### Common Fields (All Events)

```json
{
  "session_id": "unique-session-identifier",
  "transcript_path": "/path/to/conversation.json",
  "cwd": "/current/working/directory",
  "hook_event_name": "PreToolUse|PostToolUse|UserPromptSubmit|SessionStart|SessionEnd"
}
```

### Event-Specific Fields

**PreToolUse:**
```json
{
  "tool_name": "Bash",
  "tool_input": {
    "command": "pytest tests/",
    "description": "Run test suite"
  }
}
```

**PostToolUse:**
```json
{
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "/path/to/file.py",
    "old_string": "...",
    "new_string": "..."
  },
  "tool_response": {
    "success": true,
    "message": "File edited successfully"
  }
}
```

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

**SessionStart:**
```json
{
  "source": "startup|resume"
}
```

**SessionEnd:**
```json
{
  "reason": "user_exit|error|timeout"
}
```

## Configuration

### Path Resolution with CLAUDE_PROJECT_DIR

**Always use `$CLAUDE_PROJECT_DIR` to reference hook scripts.** Claude Code sets this environment variable to your project root, ensuring hooks work regardless of the current working directory.

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
          }
        ]
      }
    ]
  }
}
```

**Why this matters:**
- Claude's CWD can change during execution
- Relative paths like `./scripts/hook.sh` become fragile
- `$CLAUDE_PROJECT_DIR` always points to your project root
- Ensures hooks work from any directory

**Note:** The environment variable is only available when Claude Code spawns the hook command.

### Settings File Integration

Add hooks to `.claude/settings.json`:

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/scripts/pre-tool-hook.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/scripts/validate-edits.sh"
          }
        ]
      }
    ]
  }
}
```

### Matcher Patterns

- `*` - Match all tools
- `Edit` - Match specific tool
- `Edit|Write` - Match multiple tools
- `Bash(git:*)` - Match tool with pattern

## Process

1. **Identify the trigger event** - Which lifecycle point needs automation?
2. **Design hook script** - What validation or action is needed?
3. **Parse JSON input** - Extract relevant fields from stdin
4. **Implement logic** - Perform checks or automation
5. **Return exit code** - 0 for success, non-zero blocks execution
6. **Add to settings.json** - Configure hook with matcher
7. **Test hook** - Trigger event and verify behavior

## Examples

### Example 1: Pre-Tool Git Status Check

**Use case:** Warn if working directory is dirty before file operations

```bash
#!/bin/bash
# scripts/pre-tool-git-check.sh

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')

if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
  if ! git diff-index --quiet HEAD --; then
    echo "⚠️  Warning: Uncommitted changes in working directory"
    echo "Consider committing before editing files"
  fi
fi

exit 0  # Don't block, just warn
```

**Configuration:**
```json
{
  "PreToolUse": [{
    "matcher": "Edit|Write",
    "hooks": [{"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/scripts/pre-tool-git-check.sh"}]
  }]
}
```

### Example 2: Post-Tool Code Formatting

**Use case:** Auto-format Python files after editing

```bash
#!/bin/bash
# scripts/post-edit-format.sh

INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')

if [[ "$TOOL_NAME" == "Edit" && "$FILE_PATH" == *.py ]]; then
  black "$FILE_PATH" --quiet
  echo "✅ Formatted $FILE_PATH with black"
fi

exit 0
```

### Example 3: Session Start Environment Check

**Use case:** Verify dependencies exist before starting

```bash
#!/bin/bash
# scripts/session-start-check.sh

MISSING=()

command -v python >/dev/null || MISSING+=("python")
command -v git >/dev/null || MISSING+=("git")
command -v jq >/dev/null || MISSING+=("jq")

if [ ${#MISSING[@]} -gt 0 ]; then
  echo "❌ Missing dependencies: ${MISSING[*]}"
  exit 1  # Block session
fi

echo "✅ All dependencies available"
exit 0
```

## Best Practices

### Path Configuration
- ✅ **Do**: Always use `$CLAUDE_PROJECT_DIR` for hook script paths
- ✅ **Do**: Quote the path: `"$CLAUDE_PROJECT_DIR"/scripts/hook.sh`
- ❌ **Don't**: Use relative paths like `./scripts/hook.sh` (breaks if CWD changes)
- ❌ **Don't**: Use absolute paths like `/home/user/project/...` (not portable)

### Design
- ✅ **Do**: Keep hooks fast (<1 second)
- ✅ **Do**: Use specific matchers to reduce overhead
- ✅ **Do**: Return non-zero to block execution
- ❌ **Don't**: Perform expensive operations in hooks
- ❌ **Don't**: Block on warnings (use exit 0)

### Error Handling
- ✅ **Do**: Provide clear error messages
- ✅ **Do**: Log hook failures for debugging
- ✅ **Do**: Handle missing JSON fields gracefully
- ❌ **Don't**: Fail silently
- ❌ **Don't**: Assume JSON structure without validation

### JSON Parsing
- ✅ **Do**: Use `jq` for robust JSON parsing
- ✅ **Do**: Provide default values for optional fields
- ✅ **Do**: Validate required fields exist
- ❌ **Don't**: Use regex to parse JSON
- ❌ **Don't**: Assume fields are always present

### Performance
- ✅ **Do**: Exit early when hook doesn't apply
- ✅ **Do**: Cache expensive checks when possible
- ✅ **Do**: Use narrow matchers to reduce invocations
- ❌ **Don't**: Run hooks on every tool unconditionally
- ❌ **Don't**: Perform network requests without caching

## Integration Patterns

### With Git
```bash
# Check for uncommitted changes
git diff-index --quiet HEAD --

# Get current branch
git branch --show-current

# Check if file is tracked
git ls-files --error-unmatch "$FILE_PATH"
```

### With Linters
```bash
# Python
pylint "$FILE_PATH" --score=no --msg-template='{msg_id}: {msg}'

# JavaScript
eslint "$FILE_PATH" --format=compact

# Go
golint "$FILE_PATH"
```

### With Testing
```bash
# Run tests related to changed file
pytest "tests/test_${FILENAME}" --quiet

# Fast syntax check only
python -m py_compile "$FILE_PATH"
```

## Common Use Cases

### Validation Hooks
- Verify environment variables set
- Check file permissions
- Validate input parameters
- Ensure dependencies installed

### Quality Hooks
- Run linters on edited files
- Format code automatically
- Check test coverage
- Validate commit messages

### Workflow Hooks
- Update documentation
- Regenerate configuration
- Sync database schemas
- Trigger CI/CD pipelines

### Monitoring Hooks
- Log tool usage
- Track session metrics
- Report errors
- Update dashboards

## Anti-patterns

- ❌ **Don't**: Use relative or absolute paths for hook commands
  - ✅ **Do**: Use `$CLAUDE_PROJECT_DIR` for portable, reliable paths

- ❌ **Don't**: Use hooks for long-running tasks
  - ✅ **Do**: Keep hooks under 1 second

- ❌ **Don't**: Block on non-critical checks
  - ✅ **Do**: Use exit 0 for warnings

- ❌ **Don't**: Parse JSON with string manipulation
  - ✅ **Do**: Use `jq` for reliable parsing

- ❌ **Don't**: Match all tools without filtering
  - ✅ **Do**: Use specific matchers for relevant tools

- ❌ **Don't**: Ignore hook failures silently
  - ✅ **Do**: Provide clear feedback to user

## Constraints

- Never inline complex regex directly in `hooks.json` — double-escaping across JSON and bash layers corrupts the pattern; always delegate to a script file
- Never use `\s` in grep ERE patterns — use `[[:space:]]` instead; `\s` is not supported
- Never rely on `\b` word boundaries in grep ERE — they are unreliable; use explicit character class delimiters instead
- Never test blocking hooks without also testing false positives alongside bypass vectors — a hook that over-blocks is as broken as one that under-blocks
- Always define long regex as a `PATTERN='...'` variable in the script rather than embedding it inline in the `grep` call — it avoids a third layer of quoting and is far easier to read and update

## Resources

- **Official Docs**: https://docs.claude.com/en/docs/claude-code/hooks
- **Settings Reference**: https://docs.claude.com/en/docs/claude-code/settings
- **JSON Parsing**: `man jq` or https://jqlang.github.io/jq/

Custom docs written on creating-hooks topics:

- `resources/testing-pretooluse-blocking-hooks.md`

Related Skills

Creating Slash Commands

7
from jack-michaud/faire

Build custom slash commands in Claude Code with YAML frontmatter, permissions, and best practices. Use when creating automation workflows, project-specific commands, or standardizing repetitive tasks.

Creating New Plugins

7
from jack-michaud/faire

Follow a structured approach to create new Claude Code plugins and register them in your marketplace. Use when building plugins for commands, skills, hooks, or MCP servers.

Creating Skills

7
from jack-michaud/faire

A meta-skill for documenting and creating reusable skills for Claude Code agents. Use when you discover a technique, pattern, or workflow worth documenting for reuse across projects.

Ticket Workflow

7
from jack-michaud/faire

Autonomous ticket-to-production lifecycle with promptlet-driven phases. Use when executing /ticket commands or working on ticket-driven development.

setup-sprite

7
from jack-michaud/faire

Set up a reproducible remote dev environment using sprites with credential-free git sync. Use when user asks to "set up a sprite", "create a remote dev environment", "use sprites", or mentions wanting a reproducible remote environment for a project.

Writing python services

7
from jack-michaud/faire

Writing a class with encapsulated logic that interfaces with an external system. Logging, APIs, etc.

stacked-pr-review

7
from jack-michaud/faire

Use when addressing PR review comments on stacked jj branches. Triggered by phrases like "address review comments", "fix PR feedback", "respond to review on stacked branch", or "update the PR with reviewer suggestions".

Python Code Style

7
from jack-michaud/faire

Use when writing python code. Can be used for code review.

Plan

7
from jack-michaud/faire

My planning skill that has additional instructions. Always use when entering plan mode.

Modal

7
from jack-michaud/faire

No description provided.

merging-pr-stack

7
from jack-michaud/faire

Use when merging a stack of stacked PRs with jj. Triggered by phrases like "merge the PR stack", "merge the stacked PRs", "land these PRs", or "merge the stack into main".

Create Claude Channel

7
from jack-michaud/faire

Step-by-step procedure for building a Claude Code channel integration. Use when creating a new channel plugin that connects Claude Code to external systems via MCP, named pipes, webhooks, or chat platforms.