home / skills / laurigates / claude-plugins / hooks-configuration

This skill helps you configure and enforce Claude Code hooks to automate workflows and guarantee deterministic behavior across sessions.

npx playbooks add skill laurigates/claude-plugins --skill hooks-configuration

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
10.6 KB
---
model: haiku
name: hooks-configuration
description: |
  Claude Code hooks configuration and development. Covers hook lifecycle events,
  configuration patterns, input/output schemas, and common automation use cases.
  Use when user mentions hooks, automation, PreToolUse, PostToolUse, SessionStart,
  SubagentStart, or needs to enforce consistent behavior in Claude Code workflows.
allowed-tools: Bash(bash *), Bash(cat *), Read, Write, Edit, Glob, Grep, TodoWrite
created: 2025-12-16
modified: 2026-02-02
reviewed: 2026-02-02
---

# Claude Code Hooks Configuration

Expert knowledge for configuring and developing Claude Code hooks to automate workflows and enforce best practices.

## Core Concepts

**What Are Hooks?**
Hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle. Unlike relying on Claude to "decide" to run something, hooks provide **deterministic, guaranteed execution**.

**Why Use Hooks?**

- Enforce code formatting automatically
- Block dangerous commands before execution
- Inject context at session start
- Log commands for audit trails
- Send notifications when tasks complete

## Hook Lifecycle Events

| Event                | When It Fires              | Key Use Cases                              |
| -------------------- | -------------------------- | ------------------------------------------ |
| **SessionStart**     | Session begins/resumes     | Environment setup, context loading         |
| **UserPromptSubmit** | User submits prompt        | Input validation, context injection        |
| **PreToolUse**       | Before tool execution      | Permission control, blocking dangerous ops |
| **PostToolUse**      | After tool completes       | Auto-formatting, logging, validation       |
| **Stop**             | Agent finishes             | Notifications, git reminders               |
| **SubagentStart**    | Subagent is about to start | Input modification, context injection      |
| **SubagentStop**     | Subagent finishes          | Task completion evaluation                 |
| **PreCompact**       | Before context compaction  | Transcript backup                          |
| **Notification**     | Claude sends notification  | Custom alerts                              |
| **SessionEnd**       | Session terminates         | Cleanup, state persistence                 |

## Configuration

### File Locations

Hooks are configured in settings files:

- **`~/.claude/settings.json`** - User-level (applies everywhere)
- **`.claude/settings.json`** - Project-level (committed to repo)
- **`.claude/settings.local.json`** - Local project (not committed)

Claude Code merges all matching hooks from all files.

### Frontmatter Hooks (Skills and Commands)

Hooks can also be defined directly in skill and command frontmatter using the `hooks` field:

```yaml
---
name: my-skill
description: A skill with hooks
allowed-tools: Bash, Read
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "echo 'Pre-tool hook from skill'"
          timeout: 10
---
```

This allows skills and commands to define their own hooks that are active only when that skill/command is in use.

### Basic Structure

```json
{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "your-command-here",
            "timeout": 30
          }
        ]
      }
    ]
  }
}
```

### Matcher Patterns

- **Exact match**: `"Bash"` - matches exactly "Bash" tool
- **Regex patterns**: `"Edit|Write"` - matches either tool
- **Wildcards**: `"Notebook.*"` - matches tools starting with "Notebook"
- **All tools**: `"*"` - matches everything
- **MCP tools**: `"mcp__server__tool"` - targets MCP server tools

## Input Schema

Hooks receive JSON via stdin with these common fields:

```json
{
  "session_id": "unique-session-id",
  "transcript_path": "/path/to/conversation.json",
  "cwd": "/current/working/directory",
  "permission_mode": "mode",
  "hook_event_name": "PreToolUse"
}
```

**PreToolUse additional fields:**

```json
{
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}
```

**PostToolUse additional fields:**

```json
{
  "tool_name": "Bash",
  "tool_input": { ... },
  "tool_response": { ... }
}
```

**SubagentStart additional fields:**

```json
{
  "subagent_type": "Explore",
  "subagent_prompt": "original prompt text",
  "subagent_model": "claude-opus"
}
```

## Output Schema

### Exit Codes

- **0**: Success (command allowed)
- **2**: Blocking error (stderr shown to Claude, operation blocked)
- **Other**: Non-blocking error (logged in verbose mode)

### JSON Response (optional)

**PreToolUse:**

```json
{
  "permissionDecision": "allow|deny|ask",
  "permissionDecisionReason": "explanation",
  "updatedInput": { "modified": "input" }
}
```

**Stop/SubagentStop:**

```json
{
  "decision": "block",
  "reason": "required explanation for continuing"
}
```

**SubagentStart (input modification):**

```json
{
  "updatedPrompt": "modified prompt text to inject context or modify behavior"
}
```

**SessionStart:**

```json
{
  "additionalContext": "Information to inject into session"
}
```

## Common Hook Patterns

### Block Dangerous Commands (PreToolUse)

```bash
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Block rm -rf /
if echo "$COMMAND" | grep -Eq 'rm\s+(-rf|-fr)\s+/'; then
    echo "BLOCKED: Refusing to run destructive command on root" >&2
    exit 2
fi

exit 0
```

### Auto-Format After Edits (PostToolUse)

```bash
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ "$FILE" == *.py ]]; then
    ruff format "$FILE" 2>/dev/null
    ruff check --fix "$FILE" 2>/dev/null
elif [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then
    prettier --write "$FILE" 2>/dev/null
fi

exit 0
```

### Remind About Built-in Tools (PreToolUse)

```bash
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -Eq '^\s*cat\s+[^|><]'; then
    echo "REMINDER: Use the Read tool instead of 'cat'" >&2
    exit 2
fi

exit 0
```

### Load Context at Session Start (SessionStart)

```bash
#!/bin/bash
GIT_STATUS=$(git status --short 2>/dev/null | head -5)
BRANCH=$(git branch --show-current 2>/dev/null)

cat << EOF
{
  "additionalContext": "Current branch: $BRANCH\nPending changes:\n$GIT_STATUS"
}
EOF
```

### Inject Context for Subagents (SubagentStart)

```bash
#!/bin/bash
INPUT=$(cat)
SUBAGENT_TYPE=$(echo "$INPUT" | jq -r '.subagent_type // empty')
ORIGINAL_PROMPT=$(echo "$INPUT" | jq -r '.subagent_prompt // empty')

# Add project context to Explore agents
if [ "$SUBAGENT_TYPE" = "Explore" ]; then
    PROJECT_INFO="Project uses TypeScript with Bun. Main source in src/."
    cat << EOF
{
  "updatedPrompt": "$PROJECT_INFO\n\n$ORIGINAL_PROMPT"
}
EOF
fi

exit 0
```

### Desktop Notification on Stop (Stop)

```bash
#!/bin/bash
# Linux
notify-send "Claude Code" "Task completed" 2>/dev/null

# macOS
osascript -e 'display notification "Task completed" with title "Claude Code"' 2>/dev/null

exit 0
```

### Audit Logging (PostToolUse)

```bash
#!/bin/bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // "N/A"')

echo "$(date -Iseconds) | $TOOL | $COMMAND" >> ~/.claude/audit.log
exit 0
```

## Configuration Examples

### Anti-Pattern Detection

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash $CLAUDE_PROJECT_DIR/hooks-plugin/hooks/bash-antipatterns.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}
```

### Auto-Format Python Files

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'FILE=$(cat | jq -r \".tool_input.file_path\"); [[ \"$FILE\" == *.py ]] && ruff format \"$FILE\"'"
          }
        ]
      }
    ]
  }
}
```

### Git Reminder on Stop

```json
{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'changes=$(git status --porcelain | wc -l); [ $changes -gt 0 ] && echo \"Reminder: $changes uncommitted changes\"'"
          }
        ]
      }
    ]
  }
}
```

## Handling Blocked Commands

When a PreToolUse hook blocks a command:

| Situation                 | Action                                                           |
| ------------------------- | ---------------------------------------------------------------- |
| Hook suggests alternative | Use the suggested tool/approach                                  |
| Alternative won't work    | Ask user to run command manually                                 |
| User says "proceed"       | Still blocked - explain and provide command for manual execution |

**Critical**: User permission does NOT bypass hooks. Retrying a blocked command will fail again.

**When command is legitimately needed:**

1. Explain why the command is required
2. Describe alternatives considered and why they won't work
3. Provide exact command for user to run manually
4. Let user decide

Example response:

```
The hook blocked `git reset --hard abc123` because it's usually unnecessary.

I considered:
- `git pull`: Won't work because we need to discard local commits, not merge
- `git restore`: Only handles file changes, not commit history

If you need to proceed, please run manually:
git reset --hard abc123

This is needed because [specific justification].
```

## Best Practices

**Script Development:**

1. Always read input from stdin with `cat`
2. Use `jq` for JSON parsing
3. Quote all variables to prevent injection
4. Exit with code 2 to block, 0 to allow
5. Write blocking messages to stderr
6. Keep hooks fast (< 5 seconds)

**Configuration:**

1. Use `$CLAUDE_PROJECT_DIR` for portable paths
2. Set appropriate timeouts (default: 60s)
3. Use specific matchers over wildcards
4. Test hooks manually before enabling

**Security:**

1. Validate all inputs
2. Use absolute paths
3. Avoid touching `.env` or `.git/` directly
4. Review hook code before deployment

## Debugging

**Verify hook registration:**

```
/hooks
```

**Enable debug logging:**

```bash
claude --debug
```

**Test hooks manually:**

```bash
echo '{"tool_input": {"command": "cat file.txt"}}' | bash your-hook.sh
echo $?  # Check exit code
```

## Available Hooks in This Plugin

- **bash-antipatterns.sh**: Detects when Claude uses shell commands instead of built-in tools (cat, grep, sed, timeout, etc.)

See `hooks/README.md` for full documentation.

Overview

This skill provides practical guidance and ready-to-use patterns for configuring and developing Claude Code hooks to automate workflows and enforce consistent behavior. It covers lifecycle events, input/output schemas, matcher patterns, common shell hook examples, and secure development practices. Use these patterns to make hook behavior deterministic and auditable across sessions and projects.

How this skill works

Hooks are shell commands that run at defined lifecycle events (SessionStart, PreToolUse, PostToolUse, SubagentStart, Stop, etc.). The skill explains how Claude Code merges hooks from user, project, and local settings, how hooks receive JSON on stdin, and what exit codes or JSON responses signal. Examples show blocking patterns, input modification, auto-formatting, notifications, and audit logging.

When to use it

  • Enforce permission checks before running shell tools (PreToolUse).
  • Automatically format or lint files after edits (PostToolUse).
  • Inject project context at session or subagent start (SessionStart, SubagentStart).
  • Block destructive or discouraged commands (PreToolUse).
  • Log commands and responses for audit trails (PostToolUse).

Best practices

  • Read JSON from stdin and parse with jq; quote variables to avoid injection.
  • Keep hooks fast (under ~5s) and set explicit timeouts in config.
  • Prefer specific matcher patterns over wildcards; use $CLAUDE_PROJECT_DIR for portable paths.
  • Return exit code 2 and write block messages to stderr to stop operations.
  • Validate inputs and use absolute paths; avoid modifying .env or .git/ directly.

Example use cases

  • Block rm -rf or other destructive shell commands with a PreToolUse hook that exits 2 and explains why.
  • Auto-format Python and TypeScript files on PostToolUse using ruff and prettier commands.
  • Inject git branch and pending changes into session context on SessionStart for quick context awareness.
  • Append audit entries to ~/.claude/audit.log after any tool run to build an operation history.
  • Modify subagent prompts for Explore tasks to add project-specific constraints or environment notes.

FAQ

What happens when a PreToolUse hook blocks a command?

Hooks that block must exit 2 and write the reason to stderr. The command is prevented from running; user permission does not bypass the hook. Provide an explanation and, if necessary, a manual command to run outside the automated flow.

How do I test hooks safely?

Echo a representative JSON payload into your hook script (cat from stdin) and check the exit code. Use claude --debug to enable verbose logging and verify hook registration and behavior.