Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    tfunk1030

    hook-developer

    tfunk1030/hook-developer
    Coding
    1

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

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

    SKILL.md

    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
    SessionEnd Session ends NO Cleanup/save state
    Stop Agent finishes YES Force continuation
    SubagentStop Subagent finishes YES Force continuation
    PreCompact Before compaction NO Save state
    Notification Notification sent NO Custom alerts

    Hook Input/Output Schemas

    PreToolUse

    Purpose: Block or modify tool execution before it happens.

    Input:

    {
      "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):

    {
      "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:

    {
      "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):

    {
      "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:

    {
      "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):

    {
      "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:

    {
      "session_id": "string",
      "transcript_path": "string",
      "cwd": "string",
      "permission_mode": "string",
      "hook_event_name": "PermissionRequest",
      "tool_name": "string",
      "tool_input": {}
    }
    

    Output:

    {
      "hookSpecificOutput": {
        "hookEventName": "PermissionRequest",
        "decision": {
          "behavior": "allow|deny",
          "updatedInput": {},
          "message": "string",
          "interrupt": false
        }
      }
    }
    

    SessionStart

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

    Input:

    {
      "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):

    {
      "hookSpecificOutput": {
        "hookEventName": "SessionStart",
        "additionalContext": "string"
      },
      "suppressOutput": true
    }
    

    Plain text stdout is added as context.


    SessionEnd

    Purpose: Cleanup, save state, log session.

    Input:

    {
      "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:

    {
      "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:

    {
      "decision": "block",
      "reason": "string"
    }
    

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


    SubagentStop

    Purpose: Control when subagents (Task tool) stop.

    Input:

    {
      "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:

    {
      "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:

    {
      "continue": true,
      "systemMessage": "string"
    }
    

    Notification

    Purpose: Custom notification handling.

    Input:

    {
      "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:

    {
      "continue": true,
      "suppressOutput": true,
      "systemMessage": "string"
    }
    

    Registration in settings.json

    Standard Structure

    {
      "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

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

    Environment Variables

    Available to All Hooks

    Variable Description
    CLAUDE_PROJECT_DIR Absolute path to project root
    CLAUDE_CODE_REMOTE "true" if remote, empty if local

    SessionStart Only

    Variable Description
    CLAUDE_ENV_FILE Path to write export VAR=value lines

    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

    #!/bin/bash
    set -e
    cd "$CLAUDE_PROJECT_DIR/.claude/hooks"
    cat | npx tsx src/my-hook.ts
    

    Or for bundled:

    #!/bin/bash
    set -e
    cd "$HOME/.claude/hooks"
    cat | node dist/my-hook.mjs
    

    TypeScript Handler Pattern

    import { readFileSync } from 'fs';
    
    interface HookInput {
      session_id: string;
      hook_event_name: string;
      tool_name?: string;
      tool_input?: Record<string, unknown>;
      tool_response?: Record<string, unknown>;
      // ... 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

    # 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

    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)

    #!/usr/bin/env python3
    import json, sys
    
    data = json.load(sys.stdin)
    path = data.get('tool_input', {}).get('file_path', '')
    
    BLOCKED = ['.env', 'secrets.json']
    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)

    #!/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)

    #!/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)

    #!/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
    Recommended Servers
    Vercel Grep
    Vercel Grep
    OpenZeppelin
    OpenZeppelin
    Postman
    Postman
    Repository
    tfunk1030/vibe