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.
- 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 usesdk.d.ts- TypeScript definitions- Binary is compiled/bundled, source code not directly accessible
-
CLI Binary (
cli.js)- Compiled JavaScript executable
- Handles command-line parsing and execution
- Manages hooks, tools, and permissions
- Communicates with Anthropic API
-
SDK Interface (
sdk.mjs,sdk.d.ts)- Provides programmatic access
- Defines types for messages, hooks, options
- Supports MCP (Model Context Protocol) servers
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- Claude Code triggers hook →
- Serializes hook data to JSON →
- Pipes JSON to hook command's STDIN →
- Hook reads from STDIN and processes
// 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;
};- PreToolUse - Before tool execution
- PostToolUse - After tool completion
- UserPromptSubmit - When user submits prompt
- SessionStart - Session initialization
- SessionEnd - Session termination
- Stop - Main agent finishes
- SubagentStop - Subagent finishes
- PreCompact - Before context compaction
- Notification - System notifications
#!/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')# This will NOT work - $HOOK_INPUT is always empty
echo "Data: $HOOK_INPUT" >> log.txt # Always empty!{
"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"
}
]
}
]
}
}-
Wrong Hook Data Mechanism
- SDK examples show
$HOOK_INPUTenvironment variable - Reality: Data comes via STDIN as JSON
- Result: Hooks trigger but no data accessible
- SDK examples show
-
CLI Flag Limitations
--printflag disables hooks entirely- SDK uses SubprocessCLI with
--printflag - Result: Hooks don't trigger in SDK mode
-
Settings Parameter Works
- SDK correctly passes settings to CLI
- All formats work: Hash, JSON string, file path
- Hooks configuration reaches CLI successfully
-
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)'" }] }] } }
-
Consider Removing --print Flag
- Trade-off: Hooks work but interactive mode engaged
- Alternative: Use different output parsing approach
- 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
$HOOK_INPUTenvironment variable (always empty)- Hooks with
--printflag - Environment variable approach for data passing
-
Rewrite Hook Commands
- Change from
$HOOK_INPUTto STDIN reading - Use proper JSON parsing from STDIN
- Test with direct CLI before SDK integration
- Change from
-
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
-
Testing Approach
- Test hooks with CLI directly first
- Verify STDIN data reception
- Then integrate with SDK
-
SDK Enhancement Options
- Remove
--printflag to enable hooks - Implement custom output parsing
- Or accept hooks limitation with current approach
- Remove
-
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
-
Documentation Gap: The
$HOOK_INPUTenvironment variable appears in many examples but doesn't actually exist in the implementation -
STDIN is King: All hook data flows through STDIN as JSON, following Unix pipe philosophy
-
CLI Modes Matter: Different CLI flags (
--print,--verbose, etc.) affect hook behavior -
Compiled Nature: Claude Code CLI is a compiled/bundled application, making source inspection difficult
-
Settings Work Fine: The Ruby SDK's settings parameter implementation is correct and working
-
Hook Debugging
- Use
--debug hooksflag for troubleshooting - Log STDIN content in hook scripts
- Test with simple echo commands first
- Use
-
Performance
- Hooks have 60-second timeout
- Run in parallel (can affect order)
- Consider async webhook delivery
-
Security
- Hooks execute with full system privileges
- Validate and sanitize data from hooks
- Use absolute paths in commands
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.