🎯

hook-developer

🎯Skill

from parcadei/continuous-claude-v3

VibeIndex|
What it does

Develops and validates Claude Code hooks with comprehensive input/output schemas and registration patterns.

hook-developer

Installation

Install skill:
npx skills add https://github.com/parcadei/continuous-claude-v3 --skill hook-developer
14
Last UpdatedJan 26, 2026

Skill Details

SKILL.md

Complete Claude Code hooks reference - input/output schemas, registration, testing patterns

Overview

# Hook Developer

Complete reference for developing Claude Code hooks. Use this to write hooks with correct input/output schemas.

When to Use

  • Creating a new hook
  • Debugging hook input/output format
  • Understanding what fields are available
  • Setting up hook registration in settings.json
  • Learning what hooks can block vs inject context

Quick Reference

| Hook | Fires When | Can Block? | Primary Use |

|------|-----------|------------|-------------|

| PreToolUse | Before tool executes | YES | Block/modify tool calls |

| PostToolUse | After tool completes | Partial | React to tool results |

| UserPromptSubmit | User sends prompt | YES | Validate/inject context |

| PermissionRequest | Permission dialog shows | YES | Auto-approve/deny |

| SessionStart | Session begins | NO | Load context, set env vars |

| SessionEnd | Session ends | NO | Cleanup/save state |

| Stop | Agent finishes | YES | Force continuation |

| SubagentStart | Subagent spawns | NO | Pattern coordination |

| SubagentStop | Subagent finishes | YES | Force continuation |

| PreCompact | Before compaction | NO | Save state |

| Notification | Notification sent | NO | Custom alerts |

Hook type options: type: "command" (bash) or type: "prompt" (LLM evaluation)

---

Hook Input/Output Schemas

PreToolUse

Purpose: Block or modify tool execution before it happens.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "default|plan|acceptEdits|bypassPermissions",

"hook_event_name": "PreToolUse",

"tool_name": "string",

"tool_input": {

"file_path": "string",

"command": "string"

},

"tool_use_id": "string"

}

```

Output (JSON):

```json

{

"hookSpecificOutput": {

"hookEventName": "PreToolUse",

"permissionDecision": "allow|deny|ask",

"permissionDecisionReason": "string",

"updatedInput": {}

},

"continue": true,

"stopReason": "string",

"systemMessage": "string",

"suppressOutput": true

}

```

Exit code 2: Blocks tool, stderr shown to Claude.

Common matchers: Bash, Edit|Write, Read, Task, mcp__.*

---

PostToolUse

Purpose: React to tool execution results, provide feedback to Claude.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "string",

"hook_event_name": "PostToolUse",

"tool_name": "string",

"tool_input": {},

"tool_response": {

"filePath": "string",

"success": true,

"output": "string",

"exitCode": 0

},

"tool_use_id": "string"

}

```

CRITICAL: The response field is tool_response, NOT tool_result.

Output (JSON):

```json

{

"decision": "block",

"reason": "string",

"hookSpecificOutput": {

"hookEventName": "PostToolUse",

"additionalContext": "string"

},

"continue": true,

"stopReason": "string",

"suppressOutput": true

}

```

Blocking: "decision": "block" with "reason" prompts Claude to address the issue.

Common matchers: Edit|Write, Bash

---

UserPromptSubmit

Purpose: Validate user prompts, inject context before Claude processes.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "string",

"hook_event_name": "UserPromptSubmit",

"prompt": "string"

}

```

Output (Plain text):

```

Any stdout text is added to context for Claude.

```

Output (JSON):

```json

{

"decision": "block",

"reason": "string",

"hookSpecificOutput": {

"hookEventName": "UserPromptSubmit",

"additionalContext": "string"

}

}

```

Blocking: "decision": "block" erases prompt, shows "reason" to user only (not Claude).

Exit code 2: Blocks prompt, shows stderr to user only.

---

PermissionRequest

Purpose: Automate permission dialog decisions.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "string",

"hook_event_name": "PermissionRequest",

"tool_name": "string",

"tool_input": {}

}

```

Output:

```json

{

"hookSpecificOutput": {

"hookEventName": "PermissionRequest",

"decision": {

"behavior": "allow|deny",

"updatedInput": {},

"message": "string",

"interrupt": false

}

}

}

```

---

SessionStart

Purpose: Initialize session, load context, set environment variables.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "string",

"hook_event_name": "SessionStart",

"source": "startup|resume|clear|compact"

}

```

Environment variable: CLAUDE_ENV_FILE - write export VAR=value to persist env vars.

Output (Plain text or JSON):

```json

{

"hookSpecificOutput": {

"hookEventName": "SessionStart",

"additionalContext": "string"

},

"suppressOutput": true

}

```

Plain text stdout is added as context.

---

SessionEnd

Purpose: Cleanup, save state, log session.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "string",

"hook_event_name": "SessionEnd",

"reason": "clear|logout|prompt_input_exit|other"

}

```

Output: Cannot affect session (already ending). Use for cleanup only.

---

Stop

Purpose: Control when Claude stops, force continuation.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "string",

"hook_event_name": "Stop",

"stop_hook_active": false

}

```

CRITICAL: Check stop_hook_active: true to prevent infinite loops!

Output:

```json

{

"decision": "block",

"reason": "string"

}

```

Blocking: "decision": "block" forces Claude to continue with "reason" as prompt.

---

SubagentStart

Purpose: Run when a subagent (Task tool) is spawned.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "string",

"hook_event_name": "SubagentStart",

"agent_id": "string"

}

```

Output: Context injection only (cannot block).

---

SubagentStop

Purpose: Control when subagents (Task tool) stop.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "string",

"hook_event_name": "SubagentStop",

"stop_hook_active": false

}

```

Output: Same as Stop.

---

PreCompact

Purpose: Save state before context compaction.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "string",

"hook_event_name": "PreCompact",

"trigger": "manual|auto",

"custom_instructions": "string"

}

```

Matchers: manual, auto

Output:

```json

{

"continue": true,

"systemMessage": "string"

}

```

---

Notification

Purpose: Custom notification handling.

Input:

```json

{

"session_id": "string",

"transcript_path": "string",

"cwd": "string",

"permission_mode": "string",

"hook_event_name": "Notification",

"message": "string",

"notification_type": "permission_prompt|idle_prompt|auth_success|elicitation_dialog"

}

```

Matchers: permission_prompt, idle_prompt, auth_success, elicitation_dialog, *

Output:

```json

{

"continue": true,

"suppressOutput": true,

"systemMessage": "string"

}

```

---

Registration in settings.json

Standard Structure

```json

{

"hooks": {

"EventName": [

{

"matcher": "ToolPattern",

"hooks": [

{

"type": "command",

"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",

"timeout": 60

}

]

}

]

}

}

```

Matcher Patterns

| Pattern | Matches |

|---------|---------|

| Bash | Exactly Bash tool |

| Edit\|Write | Edit OR Write |

| Read. | Regex: Read |

| mcp__.__write. | MCP write tools |

| * | All tools |

Case-sensitive: Bash β‰  bash

Events Requiring Matchers

  • PreToolUse - YES (required)
  • PostToolUse - YES (required)
  • PermissionRequest - YES (required)
  • Notification - YES (optional)
  • SessionStart - YES (startup|resume|clear|compact)
  • PreCompact - YES (manual|auto)

Events Without Matchers

```json

{

"hooks": {

"UserPromptSubmit": [

{

"hooks": [{ "type": "command", "command": "/path/to/hook.sh" }]

}

]

}

}

```

---

Hook Types

Command Hooks (type: "command")

Default type. Executes bash commands or scripts.

```json

{

"type": "command",

"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",

"timeout": 60

}

```

Prompt-Based Hooks (type: "prompt")

Uses LLM (Haiku) for context-aware decisions. Best for Stop/SubagentStop.

```json

{

"type": "prompt",

"prompt": "Evaluate if Claude should stop. Context: $ARGUMENTS. Check if all tasks are complete.",

"timeout": 30

}

```

Response schema:

```json

{

"decision": "approve" | "block",

"reason": "Explanation",

"continue": false,

"stopReason": "Message to user",

"systemMessage": "Warning"

}

```

MCP Tool Naming

MCP tools use pattern mcp____:

| Pattern | Matches |

|---------|---------|

| mcp__memory__.* | All memory server tools |

| mcp__.__write. | All MCP write tools |

| mcp__github__.* | All GitHub tools |

---

Environment Variables

Available to All Hooks

| Variable | Description |

|----------|-------------|

| CLAUDE_PROJECT_DIR | Absolute path to project root |

| CLAUDE_CODE_REMOTE | "true" if remote/web, empty if local CLI |

SessionStart Only

| Variable | Description |

|----------|-------------|

| CLAUDE_ENV_FILE | Path to write export VAR=value lines |

Plugin Hooks Only

| Variable | Description |

|----------|-------------|

| CLAUDE_PLUGIN_ROOT | Absolute path to plugin directory |

---

Exit Codes

| Exit Code | Behavior | stdout | stderr |

|-----------|----------|--------|--------|

| 0 | Success | JSON processed | Ignored |

| 2 | Blocking error | IGNORED | Error message |

| Other | Non-blocking error | Ignored | Verbose mode |

Exit Code 2 by Hook

| Hook | Effect |

|------|--------|

| PreToolUse | Blocks tool, stderr to Claude |

| PostToolUse | stderr to Claude (tool already ran) |

| UserPromptSubmit | Blocks prompt, stderr to user only |

| Stop | Blocks stop, stderr to Claude |

---

Shell Wrapper Pattern

```bash

#!/bin/bash

set -e

cd "$CLAUDE_PROJECT_DIR/.claude/hooks"

cat | npx tsx src/my-hook.ts

```

Or for bundled:

```bash

#!/bin/bash

set -e

cd "$HOME/.claude/hooks"

cat | node dist/my-hook.mjs

```

---

TypeScript Handler Pattern

```typescript

import { readFileSync } from 'fs';

interface HookInput {

session_id: string;

hook_event_name: string;

tool_name?: string;

tool_input?: Record;

tool_response?: Record;

// ... other fields per hook type

}

function readStdin(): string {

return readFileSync(0, 'utf-8');

}

async function main() {

const input: HookInput = JSON.parse(readStdin());

// Process input

const output = {

decision: 'block', // or undefined to allow

reason: 'Why blocking'

};

console.log(JSON.stringify(output));

}

main().catch(console.error);

```

---

Testing Hooks

Manual Test Commands

```bash

# PostToolUse (Write)

echo '{"tool_name":"Write","tool_input":{"file_path":"test.md"},"tool_response":{"success":true},"session_id":"test"}' | \

.claude/hooks/my-hook.sh

# PreToolUse (Bash)

echo '{"tool_name":"Bash","tool_input":{"command":"ls"},"session_id":"test"}' | \

.claude/hooks/my-hook.sh

# SessionStart

echo '{"hook_event_name":"SessionStart","source":"startup","session_id":"test"}' | \

.claude/hooks/session-start.sh

# SessionEnd

echo '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"test"}' | \

.claude/hooks/session-end.sh

# UserPromptSubmit

echo '{"prompt":"test prompt","session_id":"test"}' | \

.claude/hooks/prompt-submit.sh

```

Rebuild After TypeScript Edits

```bash

cd .claude/hooks

npx esbuild src/my-hook.ts \

--bundle --platform=node --format=esm \

--outfile=dist/my-hook.mjs

```

---

Common Patterns

Block Dangerous Files (PreToolUse)

```python

#!/usr/bin/env python3

import json, sys

data = json.load(sys.stdin)

path = data.get('tool_input', {}).get('file_path', '')

BLOCKED = ['.env', 'secrets.json', '.git/']

if any(b in path for b in BLOCKED):

print(json.dumps({

"hookSpecificOutput": {

"hookEventName": "PreToolUse",

"permissionDecision": "deny",

"permissionDecisionReason": f"Blocked: {path} is protected"

}

}))

else:

print('{}')

```

Auto-Format Files (PostToolUse)

```bash

#!/bin/bash

INPUT=$(cat)

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

if [[ "$FILE" == .ts ]] || [[ "$FILE" == .tsx ]]; then

npx prettier --write "$FILE" 2>/dev/null

fi

echo '{}'

```

Inject Git Context (UserPromptSubmit)

```bash

#!/bin/bash

echo "Git status:"

git status --short 2>/dev/null || echo "(not a git repo)"

echo ""

echo "Recent commits:"

git log --oneline -5 2>/dev/null || echo "(no commits)"

```

Force Test Verification (Stop)

```python

#!/usr/bin/env python3

import json, sys, subprocess

data = json.load(sys.stdin)

# Prevent infinite loops

if data.get('stop_hook_active'):

print('{}')

sys.exit(0)

# Check if tests pass

result = subprocess.run(['npm', 'test'], capture_output=True)

if result.returncode != 0:

print(json.dumps({

"decision": "block",

"reason": "Tests are failing. Please fix before stopping."

}))

else:

print('{}')

```

---

Debugging Checklist

  • [ ] Hook registered in settings.json?
  • [ ] Shell script has +x permission?
  • [ ] Bundle rebuilt after TS changes?
  • [ ] Using tool_response not tool_result?
  • [ ] Output is valid JSON (or plain text)?
  • [ ] Checking stop_hook_active in Stop hooks?
  • [ ] Using $CLAUDE_PROJECT_DIR for paths?

---

Key Learnings from Past Sessions

  1. Field names matter - tool_response not tool_result
  2. Output format - decision: "block" + reason for blocking
  3. Exit code 2 - stderr goes to Claude/user, stdout IGNORED
  4. Rebuild bundles - TypeScript source edits don't auto-apply
  5. Test manually - echo '{}' | ./hook.sh before relying on it
  6. Check outputs first - ls .claude/cache/ before editing code
  7. Detached spawn hides errors - add logging to debug

See Also

  • /debug-hooks - Systematic debugging workflow
  • .claude/rules/hooks.md - Hook development rules

More from this repository10

🎯
agentica-claude-proxy🎯Skill

Enables seamless integration between Agentica agents and Claude Code CLI by managing proxy configurations, tool permissions, and response formatting.

🎯
git-commits🎯Skill

Manages git commits by removing Claude attribution, generating reasoning documentation, and ensuring clean commit workflows.

🎯
debug-hooks🎯Skill

Systematically diagnose and resolve hook registration, execution, and output issues in Claude Code projects by checking cache, settings, files, and manual testing.

🎯
migrate🎯Skill

Systematically researches, analyzes, plans, implements, and reviews migrations across frameworks, languages, and infrastructure with minimal risk.

🎯
system-overview🎯Skill

Generates a comprehensive summary of the current system's configuration, components, and key metrics across skills, agents, hooks, and other core systems.

🎯
background-agent-pings🎯Skill

Enables background agent execution with system-triggered progress notifications, avoiding manual polling and context flooding.

🎯
agentica-infrastructure🎯Skill

Provides comprehensive reference and infrastructure for building sophisticated multi-agent coordination patterns and workflows with precise API specifications and tracking mechanisms.

🎯
cli-reference🎯Skill

Provides comprehensive CLI commands and flags for interacting with Claude Code, enabling headless mode, automation, and session management.

🎯
braintrust-tracing🎯Skill

Traces and correlates Claude Code session events across parent and sub-agent interactions using comprehensive Braintrust instrumentation.

🎯
morph-apply🎯Skill

Rapidly edits files using AI-powered Morph Apply API with high accuracy and speed, without requiring full file context.