8.3 KiB

Hook API Reference

Hook Input/Output Protocol

All hooks communicate with Claude Code via JSON over stdin/stdout.

Input Format

Hooks receive a JSON object via stdin containing:

{
  "tool": "string",           // Tool being used (e.g., "Bash", "Read", "Edit")
  "parameters": {},           // Tool-specific parameters
  "success": boolean,         // Tool execution result (PostToolUse only)
  "error": "string",          // Error message if failed (PostToolUse only)
  "execution_time": number,   // Execution time in seconds (PostToolUse only)
  "prompt": "string"          // User prompt text (UserPromptSubmit only)
}

Output Format

Hooks must output a JSON response to stdout:

{
  "allow": boolean,           // Required: true to allow, false to block
  "message": "string"         // Optional: message to display to user
}

Exit Codes

  • 0 - Allow operation (success)
  • 1 - Block operation (for PreToolUse hooks only)
  • Any other code - Treated as hook error, operation allowed

Hook Types

UserPromptSubmit

Trigger: When user submits a prompt to Claude
Purpose: Monitor session state, trigger backups

Input:

{
  "prompt": "Create a new Python script that..."
}

Expected behavior:

  • Always return "allow": true
  • Update context usage estimates
  • Trigger backups if thresholds exceeded
  • Update session tracking

Example response:

{
  "allow": true,
  "message": "Context usage: 67%"
}

PreToolUse

Trigger: Before Claude executes any tool
Purpose: Validate operations, block dangerous commands

PreToolUse[Bash]

Input:

{
  "tool": "Bash",
  "parameters": {
    "command": "rm -rf /tmp/myfiles",
    "description": "Clean up temporary files"
  }
}

Expected behavior:

  • Return "allow": false and exit code 1 to block dangerous commands
  • Return "allow": true with warnings for suspicious commands
  • Check against learned failure patterns
  • Suggest alternatives when blocking

Block response:

{
  "allow": false,
  "message": "⛔ Command blocked: Dangerous pattern detected"
}

Warning response:

{
  "allow": true,
  "message": "⚠️ Command may fail (confidence: 85%)\n💡 Suggestion: Use 'python3' instead of 'python'"
}

PreToolUse[Edit]

Input:

{
  "tool": "Edit",
  "parameters": {
    "file_path": "/etc/passwd",
    "old_string": "user:x:1000",
    "new_string": "user:x:0"
  }
}

Expected behavior:

  • Validate file paths for security
  • Check for system file modifications
  • Prevent path traversal attacks

PreToolUse[Write]

Input:

{
  "tool": "Write", 
  "parameters": {
    "file_path": "../../sensitive_file.txt",
    "content": "malicious content"
  }
}

Expected behavior:

  • Validate file paths
  • Check file extensions
  • Detect potential security issues

PostToolUse

Trigger: After Claude executes any tool
Purpose: Learn from outcomes, log activity

Input:

{
  "tool": "Bash",
  "parameters": {
    "command": "pip install requests",
    "description": "Install requests library"
  },
  "success": false,
  "error": "bash: pip: command not found",
  "execution_time": 0.125
}

Expected behavior:

  • Always return "allow": true
  • Update learning patterns based on success/failure
  • Log execution for analysis
  • Update session state

Example response:

{
  "allow": true,
  "message": "Logged Bash execution (learned failure pattern)"
}

Stop

Trigger: When Claude session ends
Purpose: Finalize session, create documentation

Input:

{}

Expected behavior:

  • Always return "allow": true
  • Create LAST_SESSION.md
  • Update ACTIVE_TODOS.md
  • Save learned patterns
  • Generate recovery information if needed

Example response:

{
  "allow": true,
  "message": "Session finalized. Modified 5 files, used 23 tools."
}

Error Handling

Hook Errors

If a hook script:

  • Exits with non-zero code (except 1 for PreToolUse)
  • Produces invalid JSON
  • Times out (> 5 seconds default)
  • Crashes or cannot be executed

Then Claude Code will:

  • Log the error
  • Allow the operation to proceed
  • Display a warning message

Recovery Behavior

Hooks are designed to fail safely:

  • Operations are never blocked due to hook failures
  • Invalid responses are treated as "allow"
  • Missing hooks are ignored
  • Malformed JSON output allows operation

Performance Requirements

Execution Time

  • UserPromptSubmit: < 100ms recommended
  • PreToolUse: < 50ms recommended (blocking user)
  • PostToolUse: < 200ms recommended (can be async)
  • Stop: < 1s recommended (cleanup operations)

Memory Usage

  • Hooks should use < 50MB memory
  • Avoid loading large datasets on each execution
  • Use caching for expensive operations

I/O Operations

  • Minimize file I/O in PreToolUse hooks
  • Batch write operations where possible
  • Use async operations for non-blocking hooks

Security Considerations

Input Validation

Always validate hook input:

function validateInput(inputData) {
    if (typeof inputData !== 'object' || inputData === null || Array.isArray(inputData)) {
        throw new Error('Input must be JSON object');
    }
    
    const tool = inputData.tool || '';
    if (typeof tool !== 'string') {
        throw new Error('Tool must be string');
    }
    
    // Validate other fields...
}

Output Sanitization

Ensure hook output is safe:

function safeMessage(text) {
    // Remove potential injection characters
    return text.replace(/\x00/g, '').replace(/\r/g, '').replace(/\n/g, '\\n');
}

const response = {
    allow: true,
    message: safeMessage(userInput)
};

File Path Validation

For hooks that access files:

const path = require('path');

function validateFilePath(filePath) {
    // Convert to absolute path
    const absPath = path.resolve(filePath);
    
    // Check if within project boundaries
    const projectRoot = path.resolve('.');
    if (!absPath.startsWith(projectRoot)) {
        throw new Error('Path outside project directory');
    }
    
    // Check for system files
    const systemPaths = ['/etc', '/usr', '/var', '/sys', '/proc'];
    for (const sysPath of systemPaths) {
        if (absPath.startsWith(sysPath)) {
            throw new Error('System file access denied');
        }
    }
}

Testing Hooks

Unit Testing

Test hooks with sample inputs:

const { spawn } = require('child_process');

function testCommandValidator() {
    // Test dangerous command
    const inputData = {
        tool: 'Bash',
        parameters: { command: 'rm -rf /' }
    };
    
    const process = spawn('node', ['hooks/command-validator.js'], {
        stdio: 'pipe'
    });
    
    process.stdin.write(JSON.stringify(inputData));
    process.stdin.end();
    
    process.on('exit', (code) => {
        console.assert(code === 1, 'Should block'); // Should block
    });
    
    process.stdout.on('data', (data) => {
        const response = JSON.parse(data.toString());
        console.assert(response.allow === false);
    });
}

Integration Testing

Test with Claude Code directly:

# Test in development environment
echo '{"tool": "Bash", "parameters": {"command": "ls"}}' | node hooks/command-validator.js

# Test hook registration
claude-hooks status

Performance Testing

Measure hook execution time:

const { spawn } = require('child_process');

function benchmarkHook(hookScript, inputData, iterations = 100) {
    const times = [];
    let completed = 0;
    
    for (let i = 0; i < iterations; i++) {
        const start = Date.now();
        const process = spawn('node', [hookScript], {
            stdio: 'pipe'
        });
        
        process.stdin.write(JSON.stringify(inputData));
        process.stdin.end();
        
        process.on('exit', () => {
            times.push(Date.now() - start);
            completed++;
            
            if (completed === iterations) {
                const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
                const maxTime = Math.max(...times);
                
                console.log(`Average: ${avgTime.toFixed(1)}ms, Max: ${maxTime.toFixed(1)}ms`);
            }
        });
    }
}