Marketplace

claude-hook-authoring

Creates event hooks for Claude Code automation with proper configuration, matchers, input/output handling, and security best practices. Covers all 9 hook types (PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd). Use when building automation, creating hooks, setting up event handlers, or when users mention hooks, automation, event handlers, or tool interception.

$ Installieren

git clone https://github.com/outfitter-dev/agents /tmp/agents && cp -r /tmp/agents/agent-kit/skills/claude-hook-authoring ~/.claude/skills/agents

// tip: Run this command in your terminal to install the skill


name: claude-hook-authoring description: Creates event hooks for Claude Code automation with proper configuration, matchers, input/output handling, and security best practices. Covers all 9 hook types (PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd). Use when building automation, creating hooks, setting up event handlers, or when users mention hooks, automation, event handlers, or tool interception. version: 1.0.0

Claude Hook Authoring

Create event hooks that automate workflows and respond to Claude Code events.

Overview

Event hooks are shell commands or scripts that run automatically in response to Claude Code events:

  • PreToolUse: Before a tool executes (can block/approve)
  • PostToolUse: After a tool completes
  • UserPromptSubmit: When user submits a prompt
  • Notification: When Claude sends notifications
  • Stop: When main agent finishes
  • SubagentStop: When subagent finishes
  • PreCompact: Before conversation compacts
  • SessionStart: When session starts/resumes
  • SessionEnd: When session ends

Quick Start

Basic PostToolUse Hook

Auto-format Python files after writing:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write(*.py)",
        "hooks": [
          {
            "type": "command",
            "command": "black \"$file\""
          }
        ]
      }
    ]
  }
}

Add to .claude/settings.json or ~/.claude/settings.json.

Validation Hook

Prevent problematic bash commands:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-bash.sh",
            "timeout": 5
          }
        ]
      }
    ]
  }
}

validate-bash.sh:

#!/usr/bin/env bash
set -euo pipefail

# Read hook input from stdin
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Validate command
if echo "$COMMAND" | grep -qE '\brm\s+-rf\s+/'; then
  echo "❌ Dangerous command blocked: rm -rf /" >&2
  exit 2  # Exit 2 = block and show error to Claude
fi

# Approve
exit 0

Hook Configuration

Structure

{
  "hooks": {
    "<EventName>": [
      {
        "matcher": "<ToolPattern>",
        "hooks": [
          {
            "type": "command",
            "command": "<shell-command>",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

Location

Project hooks (.claude/settings.json):

{
  "hooks": {
    "PostToolUse": [...]
  }
}

Personal hooks (~/.claude/settings.json):

{
  "hooks": {
    "PostToolUse": [...]
  }
}

Plugin hooks (plugin/hooks/hooks.json or plugin/.claude-plugin/plugin.json):

{
  "hooks": {
    "PostToolUse": [...]
  }
}

Matchers

Matchers determine which tool invocations trigger the hook.

Simple Matchers

{"matcher": "Write"}        // Only Write tool
{"matcher": "Edit"}         // Only Edit tool
{"matcher": "Bash"}         // Only Bash tool

Regex Matchers

{"matcher": "Edit|Write"}        // Edit OR Write
{"matcher": "Notebook.*"}        // Any Notebook tool
{"matcher": "Write|Edit|Notebook.*"}  // Multiple tools

Wildcard Matcher

{"matcher": "*"}  // ALL tools

File Pattern Matchers

{"matcher": "Write(*.py)"}      // Write Python files
{"matcher": "Edit(*.ts)"}       // Edit TypeScript files
{"matcher": "Write(*.md)"}      // Write Markdown files
{"matcher": "Write|Edit(*.js)"} // Write or Edit JS files

MCP Tool Matchers

{"matcher": "mcp__memory__.*"}     // Any memory MCP tool
{"matcher": "mcp__github__.*"}     // Any GitHub MCP tool
{"matcher": "mcp__.*__.*"}         // Any MCP tool

Hook Input

Hooks receive JSON on stdin with event context:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/path/to/file.txt",
    "content": "file content"
  }
}

Reading Input in Bash

#!/usr/bin/env bash
set -euo pipefail

# Read and parse JSON input
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Use the data
echo "Tool: $TOOL_NAME"
echo "File: $FILE_PATH"

Reading Input in Bun/TypeScript

#!/usr/bin/env bun
import { stdin } from "process";

// Read stdin
const chunks: Buffer[] = [];
for await (const chunk of stdin) {
  chunks.push(chunk);
}
const input = JSON.parse(Buffer.concat(chunks).toString());

// Access data
const toolName = input.tool_name;
const filePath = input.tool_input?.file_path;

console.log(`Tool: ${toolName}`);
console.log(`File: ${filePath}`);

Hook Output

Exit Codes (Simple)

#!/usr/bin/env bash

# Success (continue)
exit 0

# Blocking error (show to Claude)
echo "Error: validation failed" >&2
exit 2

# Non-blocking error (show to user)
echo "Warning: check failed" >&2
exit 1

Behavior:

  • Exit 0: Success, stdout shown to user
  • Exit 2: Blocking error, stderr shown to Claude
  • Other: Non-blocking error, stderr shown to user

JSON Output (Advanced)

{
  "continue": true,
  "stopReason": "Optional message",
  "suppressOutput": false,
  "systemMessage": "Warning message",
  "decision": "block",
  "reason": "Explanation",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Dangerous operation",
    "additionalContext": "Context for Claude"
  }
}

Hook Types

PreToolUse

Runs before tool executes. Can block or approve operations.

Common uses:

  • Validate bash commands
  • Check file paths
  • Enforce security policies
  • Add context before execution
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{
          "type": "command",
          "command": "./.claude/hooks/check-file-policy.sh"
        }]
      }
    ]
  }
}

PostToolUse

Runs after tool completes successfully.

Common uses:

  • Auto-format code
  • Update documentation
  • Run linters
  • Trigger builds
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.ts)",
        "hooks": [{
          "type": "command",
          "command": "biome check --write \"$file\""
        }]
      }
    ]
  }
}

UserPromptSubmit

Runs when user submits a prompt.

Common uses:

  • Add current time/date
  • Add environment context
  • Log user activity
  • Pre-process prompts
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "*",
        "hooks": [{
          "type": "command",
          "command": "./.claude/hooks/add-context.sh"
        }]
      }
    ]
  }
}

Notification

Runs when Claude sends notifications.

Common uses:

  • Send to external systems
  • Log notifications
  • Trigger alerts
{
  "hooks": {
    "Notification": [
      {
        "matcher": "*",
        "hooks": [{
          "type": "command",
          "command": "./.claude/hooks/log-notification.sh"
        }]
      }
    ]
  }
}

Stop

Runs when main Claude agent finishes responding.

Common uses:

  • Clean up resources
  • Send completion notifications
  • Update external systems
{
  "hooks": {
    "Stop": [
      {
        "matcher": "*",
        "hooks": [{
          "type": "command",
          "command": "./.claude/hooks/on-completion.sh"
        }]
      }
    ]
  }
}

SubagentStop

Runs when a subagent finishes.

Common uses:

  • Track subagent usage
  • Log subagent results
  • Trigger follow-up actions
{
  "hooks": {
    "SubagentStop": [
      {
        "matcher": "*",
        "hooks": [{
          "type": "command",
          "command": "./.claude/hooks/on-subagent-done.sh"
        }]
      }
    ]
  }
}

PreCompact

Runs before conversation compacts.

Matchers:

  • manual: User-triggered (/compact)
  • auto: Automatic compact
{
  "hooks": {
    "PreCompact": [
      {
        "matcher": "manual",
        "hooks": [{
          "type": "command",
          "command": "./.claude/hooks/before-compact.sh"
        }]
      }
    ]
  }
}

SessionStart

Runs when session starts or resumes.

Matchers:

  • startup: Claude Code starts
  • resume: Session resumes (--resume)
  • clear: After /clear
  • compact: After compact
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [{
          "type": "command",
          "command": "echo 'Welcome!' && git status"
        }]
      }
    ]
  }
}

SessionEnd

Runs when session ends.

Reasons:

  • clear: User ran /clear
  • logout: User logged out
  • prompt_input_exit: Exited during prompt
  • other: Other reasons
{
  "hooks": {
    "SessionEnd": [
      {
        "matcher": "*",
        "hooks": [{
          "type": "command",
          "command": "./.claude/hooks/cleanup.sh"
        }]
      }
    ]
  }
}

Security Best Practices

1. Validate All Input

#!/usr/bin/env bash
set -euo pipefail

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

# Check for path traversal
if echo "$FILE_PATH" | grep -q '\.\.'; then
  echo "❌ Path traversal detected" >&2
  exit 2
fi

# Check for sensitive paths
if echo "$FILE_PATH" | grep -qE '^/etc/|^/root/|\.env$'; then
  echo "❌ Sensitive path blocked" >&2
  exit 2
fi

2. Quote Shell Variables

# ❌ WRONG - vulnerable to injection
rm $FILE_PATH

# ✅ CORRECT - properly quoted
rm "$FILE_PATH"

3. Use Absolute Paths

{
  "hooks": {
    "PostToolUse": [{
      "hooks": [{
        "type": "command",
        "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format.sh"
      }]
    }]
  }
}

4. Set Timeouts

{
  "hooks": [{
    "type": "command",
    "command": "./slow-operation.sh",
    "timeout": 30
  }]
}

5. Handle Errors Gracefully

#!/usr/bin/env bash
set -euo pipefail

# Always validate input exists
if ! command -v jq &>/dev/null; then
  echo "Error: jq not installed" >&2
  exit 1
fi

# Catch errors
if ! INPUT=$(cat 2>&1); then
  echo "Error: Failed to read stdin" >&2
  exit 1
fi

Common Patterns

See REFERENCE.md for:

  • Advanced hook configurations
  • Complex matchers
  • Hook chaining
  • Error handling patterns
  • Integration examples

See EXAMPLES.md for:

  • Real-world hook implementations
  • Auto-formatting workflows
  • Validation patterns
  • CI/CD integration
  • Team workflows

Utilities

# Create hook with template
./scripts/scaffold-hook.sh format-typescript

# Validate hook configuration
./scripts/validate-hook.sh .claude/settings.json

# Test hook script with sample input
./scripts/test-hook.ts .claude/hooks/my-hook.sh

Debugging Hooks

Enable Debug Mode

claude --debug

Check Hook Output

Use transcript mode (Ctrl+R) to see hook execution and output.

Test Hook Manually

# Create sample input
cat > /tmp/hook-input.json << 'EOF'
{
  "tool_name": "Write",
  "tool_input": {
    "file_path": "test.ts",
    "content": "console.log('test');"
  }
}
EOF

# Test hook
cat /tmp/hook-input.json | ./.claude/hooks/my-hook.sh

Common Issues

Hook not firing:

  • Check matcher syntax
  • Verify hook is in correct settings file
  • Restart Claude Code

Permission errors:

  • Make script executable: chmod +x script.sh
  • Check file paths are correct
  • Verify $CLAUDE_PROJECT_DIR is set

Timeout errors:

  • Increase timeout value
  • Optimize script performance
  • Check for hanging commands

Environment Variables

Available in hook scripts:

  • $CLAUDE_PROJECT_DIR: Project root directory
  • $file: File path (PostToolUse hooks)
  • Custom variables from settings.json

Related Skills

  • claude-command-authoring: Combine commands with hooks
  • claude-plugin-authoring: Package hooks into plugins
  • claude-config-management: Manage hook configuration