Skip to content

Latest commit

 

History

History
288 lines (222 loc) · 8.2 KB

File metadata and controls

288 lines (222 loc) · 8.2 KB

Claude Code Deep Analysis & Hooks Implementation Guide

Executive Summary

After extensive analysis of the Claude Code CLI (v1.0.108) and its interaction with the Ruby SDK, I've identified a critical misunderstanding about hooks implementation. Claude Code passes hook data via STDIN as JSON, not through the $HOOK_INPUT environment variable. This explains why hooks trigger successfully but the Ruby SDK examples expecting $HOOK_INPUT to be populated always find it empty.

🏗️ Claude Code Architecture

Package Structure

  • Location: NPM package @anthropic-ai/claude-code
  • Version: 1.0.108
  • Main Files:
    • cli.js - Minified/compiled CLI entry point (9.2MB)
    • sdk.mjs - SDK module for programmatic use
    • sdk.d.ts - TypeScript definitions
    • Binary is compiled/bundled, source code not directly accessible

Key Components

  1. CLI Binary (cli.js)

    • Compiled JavaScript executable
    • Handles command-line parsing and execution
    • Manages hooks, tools, and permissions
    • Communicates with Anthropic API
  2. SDK Interface (sdk.mjs, sdk.d.ts)

    • Provides programmatic access
    • Defines types for messages, hooks, options
    • Supports MCP (Model Context Protocol) servers

🪝 Hooks System - The Real Story

How Hooks Actually Work

CRITICAL FINDING: Hooks receive data via STDIN as JSON, not environment variables!

# What the documentation says (CORRECT):
echo '{"hook_event_name":"PostToolUse",...}' | your_hook_command

# What we thought (INCORRECT):
HOOK_INPUT='{"hook_event_name":"PostToolUse",...}' your_hook_command

Hook Data Flow

  1. Claude Code triggers hook
  2. Serializes hook data to JSON
  3. Pipes JSON to hook command's STDIN
  4. Hook reads from STDIN and processes

Hook Events & Data Structure

// From sdk.d.ts - Hook input types
type BaseHookInput = {
  session_id: string;
  transcript_path: string;
  cwd: string;
  permission_mode?: string;
};

type PostToolUseHookInput = BaseHookInput & {
  hook_event_name: 'PostToolUse';
  tool_name: string;
  tool_input: unknown;
  tool_response: unknown;
};

Available Hook Events

  1. PreToolUse - Before tool execution
  2. PostToolUse - After tool completion
  3. UserPromptSubmit - When user submits prompt
  4. SessionStart - Session initialization
  5. SessionEnd - Session termination
  6. Stop - Main agent finishes
  7. SubagentStop - Subagent finishes
  8. PreCompact - Before context compaction
  9. Notification - System notifications

Correct Hook Implementation

✅ Correct - Reading from STDIN:

#!/usr/bin/env ruby
require 'json'

# Read JSON from STDIN
input_data = JSON.parse(STDIN.read)

# Process hook data
tool_name = input_data['tool_name']
session_id = input_data['session_id']

# Hook logic here...
#!/usr/bin/env python3
import json
import sys

# Read JSON from stdin
input_data = json.load(sys.stdin)

# Process hook data
tool_name = input_data.get('tool_name')
session_id = input_data.get('session_id')

❌ Incorrect - Expecting HOOK_INPUT:

# This will NOT work - $HOOK_INPUT is always empty
echo "Data: $HOOK_INPUT" >> log.txt  # Always empty!

Hook Configuration in settings.json

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash|Write|Edit",  // Regex pattern
        "hooks": [
          {
            "type": "command",
            "command": "ruby /path/to/hook.rb"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": ".*",  // Match all tools
        "hooks": [
          {
            "type": "command",
            "command": "python3 /path/to/validator.py"
          }
        ]
      }
    ]
  }
}

🔧 Ruby SDK Integration Issues & Solutions

Current Problems

  1. Wrong Hook Data Mechanism

    • SDK examples show $HOOK_INPUT environment variable
    • Reality: Data comes via STDIN as JSON
    • Result: Hooks trigger but no data accessible
  2. CLI Flag Limitations

    • --print flag disables hooks entirely
    • SDK uses SubprocessCLI with --print flag
    • Result: Hooks don't trigger in SDK mode
  3. Settings Parameter Works

    • SDK correctly passes settings to CLI
    • All formats work: Hash, JSON string, file path
    • Hooks configuration reaches CLI successfully

Required Fixes

  1. Update Hook Examples

    # AllSpark webhook hook should be:
    hooks_config = {
      "hooks" => {
        "PostToolUse" => [{
          "matcher" => ".*",
          "hooks" => [{
            "type" => "command",
            # Must read from stdin, not $HOOK_INPUT
            "command" => "ruby -rjson -e 'data=JSON.parse(STDIN.read); system(\"curl -X POST webhook_url -d \" + data.to_json)'"
          }]
        }]
      }
    }
  2. Consider Removing --print Flag

    • Trade-off: Hooks work but interactive mode engaged
    • Alternative: Use different output parsing approach

📊 Test Results Summary

What Works ✅

  • Hooks trigger successfully
  • Settings parameter passes correctly (all formats)
  • Hook commands execute
  • JSON data available via STDIN
  • Multiple hooks run in parallel
  • Tool matching with regex patterns

What Doesn't Work ❌

  • $HOOK_INPUT environment variable (always empty)
  • Hooks with --print flag
  • Environment variable approach for data passing

🎯 Recommendations for AllSpark Builder

Immediate Actions

  1. Rewrite Hook Commands

    • Change from $HOOK_INPUT to STDIN reading
    • Use proper JSON parsing from STDIN
    • Test with direct CLI before SDK integration
  2. Hook Implementation Pattern

    # Create a Ruby script that reads from STDIN
    File.write('/tmp/allspark_hook.rb', <<~RUBY)
      #!/usr/bin/env ruby
      require 'json'
      require 'net/http'
      
      data = JSON.parse(STDIN.read)
      
      # Send to webhook
      uri = URI('http://webhook.url')
      http = Net::HTTP.new(uri.host, uri.port)
      request = Net::HTTP::Post.new(uri.path)
      request.body = data.to_json
      request['Content-Type'] = 'application/json'
      http.request(request)
    RUBY
  3. Testing Approach

    • Test hooks with CLI directly first
    • Verify STDIN data reception
    • Then integrate with SDK

Long-term Solutions

  1. SDK Enhancement Options

    • Remove --print flag to enable hooks
    • Implement custom output parsing
    • Or accept hooks limitation with current approach
  2. Alternative Architectures

    • Use SDK's callback-based hooks instead of CLI hooks
    • Implement hooks at SDK level, not CLI level
    • Direct API integration bypassing CLI

📝 Key Learnings

  1. Documentation Gap: The $HOOK_INPUT environment variable appears in many examples but doesn't actually exist in the implementation

  2. STDIN is King: All hook data flows through STDIN as JSON, following Unix pipe philosophy

  3. CLI Modes Matter: Different CLI flags (--print, --verbose, etc.) affect hook behavior

  4. Compiled Nature: Claude Code CLI is a compiled/bundled application, making source inspection difficult

  5. Settings Work Fine: The Ruby SDK's settings parameter implementation is correct and working

🔮 Future Considerations

  1. Hook Debugging

    • Use --debug hooks flag for troubleshooting
    • Log STDIN content in hook scripts
    • Test with simple echo commands first
  2. Performance

    • Hooks have 60-second timeout
    • Run in parallel (can affect order)
    • Consider async webhook delivery
  3. Security

    • Hooks execute with full system privileges
    • Validate and sanitize data from hooks
    • Use absolute paths in commands

Conclusion

The Ruby SDK's hooks implementation is fundamentally correct in passing configuration to the CLI. The issue is a misunderstanding about how hook data is transmitted (STDIN vs environment variable). By updating hook commands to read from STDIN instead of expecting $HOOK_INPUT, the AllSpark Builder integration can fully utilize Claude Code's hooks system for monitoring tool execution and capturing events.

The $HOOK_INPUT environment variable was likely a documentation error or legacy approach that no longer exists in the current implementation. All official examples and the actual CLI implementation use STDIN for hook data transmission.