home / skills / outfitter-dev / agents / claude-hooks

claude-hooks skill

/plugins/outfitter/skills/claude-hooks

This skill helps you author and validate Claude Code event hooks to automate workflows and enforce safety checks.

npx playbooks add skill outfitter-dev/agents --skill claude-hooks

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

Files (9)
SKILL.md
12.9 KB
---
name: claude-hooks
description: This skill should be used when creating hooks, automating workflows, or when "PreToolUse", "PostToolUse", "hooks.json", "event handler", or "create hook" are mentioned.
metadata:
  version: "2.0.0"
  related-skills:
    - claude-commands
    - claude-plugins
    - claude-agents
    - claude-config
---

# Claude Hook Authoring

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

## Hook Types

Three hook execution types:

| Type | Best For | Example |
|------|----------|---------|
| **command** | Deterministic checks, external tools, performance | Bash script validates paths |
| **prompt** | Complex reasoning, context-aware validation | LLM evaluates if action is safe |
| **agent** | Multi-step verification requiring tool access | Agent with Read/Grep tools verifies consistency |

**Command hooks** (for deterministic/fast checks):

```json
{
  "type": "command",
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh",
  "timeout": 10
}
```

**Prompt hooks** (recommended for complex logic):

```json
{
  "type": "prompt",
  "prompt": "Evaluate if this file write is safe: $TOOL_INPUT. Check for sensitive paths, credentials, path traversal. Return 'allow' or 'deny' with reason.",
  "timeout": 30
}
```

**Agent hooks** (for complex multi-step verification):

```json
{
  "type": "agent",
  "prompt": "Verify this code change maintains consistency with the existing codebase. Check imports, type signatures, and naming conventions. Use Read and Grep tools as needed.",
  "allowedTools": ["Read", "Grep", "Glob"],
  "timeout": 120
}
```

Agent hooks spawn a subagent with tool access for verification tasks that require reading files, searching code, or multi-step reasoning. Use when prompt hooks are insufficient.

## Hook Events

| Event | When | Can Block | Common Uses |
|-------|------|-----------|-------------|
| **PreToolUse** | Before tool executes | Yes | Validate commands, check paths, enforce policies |
| **PostToolUse** | After tool succeeds | No | Auto-format, run linters, update docs |
| **PostToolUseFailure** | After tool fails | No | Error logging, retry logic, notifications |
| **PermissionRequest** | Permission dialog shown | Yes | Auto-allow/deny based on rules |
| **UserPromptSubmit** | User submits prompt | No | Add context, log activity, augment prompts |
| **Notification** | Claude sends notification | No | External alerts, logging |
| **Stop** | Main agent finishes | No | Cleanup, completion notifications |
| **SubagentStart** | Subagent spawns | No | Track subagent usage |
| **SubagentStop** | Subagent finishes | No | Log results, trigger follow-ups |
| **Setup** | `--init`, `--init-only`, or `--maintenance` flags | No | Initialize environment, install dependencies |
| **PreCompact** | Before context compacts | No | Backup conversation, preserve context |
| **SessionStart** | Session starts/resumes | No | Load context, show status, init resources |
| **SessionEnd** | Session ends | No | Cleanup, save state, log metrics |

See [references/hook-types.md](references/hook-types.md) for detailed documentation of each event.

## Quick Start

### Auto-Format TypeScript

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.ts|*.tsx)",
        "hooks": [{
          "type": "command",
          "command": "biome check --write \"$file\"",
          "timeout": 10
        }]
      }
    ]
  }
}
```

### Block Dangerous Commands

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

**validate-bash.sh**:

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

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

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

exit 0
```

### Smart Validation with Prompt Hook

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{
          "type": "prompt",
          "prompt": "Analyze this file operation for safety. Check: 1) No sensitive paths (/etc, ~/.ssh), 2) No credentials in content, 3) No path traversal (..). Tool input: $TOOL_INPUT. Respond with JSON: {\"decision\": \"allow|deny\", \"reason\": \"...\"}",
          "timeout": 30
        }]
      }
    ]
  }
}
```

## Configuration Locations

| Location | Scope | Committed |
|----------|-------|-----------|
| `.claude/settings.json` | Project (team-shared) | Yes |
| `.claude/settings.local.json` | Project (local only) | No |
| `~/.claude/settings.json` | Personal (all projects) | No |
| `plugin/hooks/hooks.json` | Plugin | Yes |

### Plugin Format (hooks.json)

Uses wrapper structure:

```json
{
  "description": "Plugin hooks for auto-formatting",
  "hooks": {
    "PostToolUse": [...]
  }
}
```

### Settings Format (settings.json)

Direct structure (no wrapper):

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

## Matchers

Matchers determine which tool invocations trigger the hook. Case-sensitive.

```json
{"matcher": "Write"}                    // Exact match
{"matcher": "Edit|Write"}               // Multiple tools (OR)
{"matcher": "*"}                        // All tools
{"matcher": "Write(*.py)"}              // File pattern
{"matcher": "Write|Edit(*.ts|*.tsx)"}   // Multiple + pattern
{"matcher": "mcp__memory__.*"}          // MCP server tools
{"matcher": "mcp__github__create_issue"} // Specific MCP tool
```

**Lifecycle hooks** (SessionStart, SessionEnd, Stop, Notification) use special matchers:

```json
// SessionStart matchers
{"matcher": "startup"}   // Initial start
{"matcher": "resume"}    // --resume or --continue
{"matcher": "clear"}     // After /clear
{"matcher": "compact"}   // After compaction

// PreCompact matchers
{"matcher": "manual"}    // User triggered /compact
{"matcher": "auto"}      // Automatic compaction
```

See [references/matchers.md](references/matchers.md) for advanced patterns.

## Input Format

All hooks receive JSON on stdin:

```json
{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "PreToolUse",
  "permission_mode": "ask",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/project/src/file.ts",
    "content": "export const foo = 'bar';"
  }
}
```

**Event-specific fields**:
- Tool hooks: `tool_name`, `tool_input`, `tool_result` (PostToolUse)
- UserPromptSubmit: `user_prompt`
- Stop/SubagentStop: `reason`

**Prompt hooks** access fields via placeholders:
- `$ARGUMENTS` - Full context passed to the hook (general-purpose)
- `$TOOL_INPUT` - Tool input for tool-related events
- `$TOOL_RESULT` - Tool result (PostToolUse only)
- `$USER_PROMPT` - User prompt (UserPromptSubmit only)

### Reading Input

**Bash**:

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

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

**Bun/TypeScript**:

```typescript
#!/usr/bin/env bun
const input = await Bun.stdin.json();
const toolName = input.tool_name;
const filePath = input.tool_input?.file_path;
```

## Output Format

### Exit Codes (Simple)

```bash
exit 0   # Success, continue execution
exit 2   # Block operation (PreToolUse only), stderr shown to Claude
exit 1   # Warning, stderr shown to user, continues
```

### JSON Output (Advanced)

```json
{
  "continue": true,
  "suppressOutput": false,
  "systemMessage": "Context for Claude",
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow|deny|ask",
    "permissionDecisionReason": "Explanation",
    "updatedInput": {"modified": "field"}
  }
}
```

**PreToolUse** can modify tool input via `updatedInput` and control permissions via `permissionDecision`.

## Environment Variables

| Variable | Availability | Description |
|----------|--------------|-------------|
| `$CLAUDE_PROJECT_DIR` | All hooks | Project root directory |
| `$CLAUDE_PLUGIN_ROOT` | Plugin hooks | Plugin root (use for portable paths) |
| `$file` | PostToolUse (Write/Edit) | Path to affected file |
| `$CLAUDE_ENV_FILE` | SessionStart | Write env vars here to persist |
| `$CLAUDE_CODE_REMOTE` | All hooks | Set if running in remote context |

**Plugin hooks** should always use `${CLAUDE_PLUGIN_ROOT}` for portability:

```json
{
  "command": "${CLAUDE_PLUGIN_ROOT}/scripts/validate.sh"
}
```

**SessionStart** can persist environment variables:

```bash
#!/usr/bin/env bash
# Persist variables for the session
echo "export PROJECT_TYPE=nodejs" >> "$CLAUDE_ENV_FILE"
echo "export API_URL=https://api.example.com" >> "$CLAUDE_ENV_FILE"
```

## Component-Scoped Hooks

Skills, agents, and commands can define hooks in frontmatter. These hooks only run when the component is active.

**Supported events**: PreToolUse, PostToolUse, Stop

### Skill with Hooks

```yaml
---
name: my-skill
description: Skill with validation hooks
hooks:
  PreToolUse:
    - matcher: "Write|Edit"
      hooks:
        - type: prompt
          prompt: "Validate this write operation for the skill context..."
---
```

### Agent with Hooks

```yaml
---
name: security-reviewer
model: sonnet
hooks:
  PreToolUse:
    - matcher: "Bash"
      hooks:
        - type: command
          command: "${CLAUDE_PLUGIN_ROOT}/scripts/validate-bash.sh"
  Stop:
    - matcher: "*"
      hooks:
        - type: prompt
          prompt: "Verify the security review is complete..."
---
```

## Execution Model

**Parallel execution**: All matching hooks run in parallel, not sequentially.

```json
{
  "PreToolUse": [{
    "matcher": "Write",
    "hooks": [
      {"type": "command", "command": "check1.sh"},  // Runs in parallel
      {"type": "command", "command": "check2.sh"},  // Runs in parallel
      {"type": "prompt", "prompt": "Validate..."}   // Runs in parallel
    ]
  }]
}
```

**Implications**:
- Hooks cannot see each other's output
- Non-deterministic ordering
- Design for independence

**Hot-swap limitations**: Hook changes require restarting Claude Code. Editing `hooks.json` or hook scripts does not affect the current session.

## Security Best Practices

1. **Validate all input** - Check for path traversal, sensitive paths, injection
2. **Quote shell variables** - Always use `"$VAR"` not `$VAR`
3. **Set timeouts** - Prevent hanging hooks (default: 60s command, 30s prompt)
4. **Use absolute paths** - Via `$CLAUDE_PROJECT_DIR` or `${CLAUDE_PLUGIN_ROOT}`
5. **Handle errors gracefully** - Use `set -euo pipefail` in bash
6. **Don't log sensitive data** - Filter credentials, tokens, API keys

See [references/security.md](references/security.md) for detailed security patterns.

## Debugging

```bash
# Run Claude with debug output
claude --debug

# Test hook manually
echo '{"tool_name": "Write", "tool_input": {"file_path": "test.ts"}}' | ./.claude/hooks/my-hook.sh

# Check transcript for hook execution
# Press Ctrl+R in Claude Code to view transcript
```

**Common issues**:
- Hook not firing: Check matcher syntax, restart Claude Code
- Permission errors: `chmod +x script.sh`
- Timeout: Increase timeout value or optimize script

## Workflow Patterns

### Pre-Commit Quality Gate

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {"type": "command", "command": "./.claude/hooks/validate-paths.sh"},
          {"type": "command", "command": "./.claude/hooks/check-sensitive.sh"}
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit(*.ts)",
        "hooks": [
          {"type": "command", "command": "biome check --write \"$file\""},
          {"type": "command", "command": "tsc --noEmit \"$file\""}
        ]
      }
    ]
  }
}
```

### Context Injection

```json
{
  "hooks": {
    "SessionStart": [{
      "matcher": "startup",
      "hooks": [{
        "type": "command",
        "command": "echo \"Branch: $(git branch --show-current)\" && git status --short"
      }]
    }],
    "UserPromptSubmit": [{
      "matcher": "*",
      "hooks": [{
        "type": "command",
        "command": "echo \"Time: $(date '+%Y-%m-%d %H:%M %Z')\""
      }]
    }]
  }
}
```

## References

- [references/hook-types.md](references/hook-types.md) - Detailed documentation for each hook event
- [references/matchers.md](references/matchers.md) - Advanced matcher patterns and MCP tools
- [references/security.md](references/security.md) - Security best practices and validation patterns
- [references/schema.md](references/schema.md) - Complete configuration schema reference
- [references/examples.md](references/examples.md) - Real-world hook implementations

## External Resources

- [Official Hooks Reference](https://code.claude.com/docs/en/hooks)
- [Hooks Guide](https://code.claude.com/docs/en/hooks-guide)
- [Community Examples (disler)](https://github.com/disler/claude-code-hooks-mastery)
- [Claude Code Showcase](https://github.com/ChrisWiles/claude-code-showcase)

Overview

This skill provides rules files and configurations for authoring Claude event hooks to automate workflows, validate operations, and respond to Claude Code events. It centralizes hook types, events, input/output formats, and example patterns so teams can safely extend agent behavior. Use it to define project- or plugin-scoped hooks, matchers, and execution policies for PreToolUse, PostToolUse, SessionStart, and other lifecycle events.

How this skill works

The skill documents three hook execution types (command, prompt, agent) and how each runs against matching events. Hooks receive a standardized JSON stdin payload and return either exit codes or structured JSON to allow blocking, modifying inputs, or emitting context. Matchers control which tool invocations trigger hooks and hooks execute in parallel with independent outcomes.

When to use it

  • When you need to validate or block tool executions (PreToolUse).
  • When you want automatic formatting, linting, or post-processing after tool success (PostToolUse).
  • When permission dialogs must be auto-decided or audited (PermissionRequest).
  • When initializing environment, persisting session variables, or running startup tasks (SessionStart/Setup).
  • When implementing multi-step verification that requires file reads or searches (use agent hooks).

Best practices

  • Prefer prompt hooks for complex, context-aware validation and command hooks for fast deterministic checks.
  • Quote and validate all shell inputs; use set -euo pipefail and explicit timeouts to avoid hangs.
  • Use ${CLAUDE_PLUGIN_ROOT} and $CLAUDE_PROJECT_DIR for portable absolute paths across environments.
  • Design hooks to be independent: parallel execution means hooks cannot rely on each other’s output.
  • Avoid logging secrets; filter credentials and tokens from outputs and system messages.

Example use cases

  • Block dangerous bash commands (PreToolUse command hook) to prevent destructive operations.
  • Auto-format TypeScript files after successful edits (PostToolUse command hook invoking biome or prettier).
  • Prompt-based safety check that returns allow/deny JSON for file writes before execution.
  • Spawn a subagent with Read/Grep tools to verify large refactors or cross-file consistency (agent hook).
  • Persist session environment variables on startup and inject contextual metadata into user prompts.

FAQ

How do I block a tool operation?

Return exit code 2 from a command hook in PreToolUse or respond with permissionDecision: "deny" in JSON from a prompt hook.

Can hooks modify tool inputs?

Yes. PreToolUse can return updatedInput to change the tool input before execution.

Do hooks run sequentially?

No. All matching hooks run in parallel; design them to be independent.