223 lines
5.9 KiB
Ruby
Executable File
223 lines
5.9 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
require 'claude_hooks'
|
|
|
|
# PostToolUse Handler
|
|
#
|
|
# PURPOSE: Process and analyze tool execution results
|
|
# TRIGGERS: After Claude Code executes any tool (Bash, Write, Edit, etc.)
|
|
#
|
|
# COMMON USE CASES:
|
|
# - Parse and format tool output
|
|
# - Extract errors and warnings from command results
|
|
# - Update project state based on tool results
|
|
# - Log execution metrics and performance data
|
|
# - Trigger follow-up actions based on results
|
|
# - Cache or store important tool outputs
|
|
#
|
|
# SETTINGS.JSON CONFIGURATION:
|
|
# {
|
|
# "hooks": {
|
|
# "PostToolUse": [{
|
|
# "matcher": "",
|
|
# "hooks": [{
|
|
# "type": "command",
|
|
# "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/entrypoints/post_tool_use.rb"
|
|
# }]
|
|
# }]
|
|
# }
|
|
# }
|
|
|
|
class PostToolUseHandler < ClaudeHooks::PostToolUse
|
|
def call
|
|
log "Processing tool result: #{tool_name}"
|
|
|
|
# Example: Analyze tool results
|
|
# analyze_tool_results
|
|
|
|
# Example: Extract errors and warnings
|
|
# extract_errors_and_warnings
|
|
|
|
# Example: Update project state
|
|
# update_project_state
|
|
|
|
# Example: Log execution metrics
|
|
# log_execution_metrics
|
|
|
|
output_data
|
|
end
|
|
|
|
private
|
|
|
|
def analyze_tool_results
|
|
case tool_name
|
|
when 'Bash'
|
|
# Analyze command execution results
|
|
analyze_bash_results
|
|
|
|
when 'Write', 'Edit', 'MultiEdit'
|
|
# Track file modifications
|
|
analyze_file_modifications
|
|
|
|
when 'Grep', 'Glob'
|
|
# Analyze search results
|
|
analyze_search_results
|
|
end
|
|
end
|
|
|
|
def analyze_bash_results
|
|
return unless tool_result.is_a?(Hash)
|
|
|
|
exit_code = tool_result['exit_code'] || 0
|
|
stdout = tool_result['stdout'] || ''
|
|
stderr = tool_result['stderr'] || ''
|
|
|
|
if exit_code != 0
|
|
log "Command failed with exit code: #{exit_code}", level: :error
|
|
log "stderr: #{stderr}", level: :error if stderr && !stderr.empty?
|
|
else
|
|
log 'Command executed successfully'
|
|
end
|
|
|
|
# Example: Parse specific command outputs
|
|
command = tool_input['command'] || ''
|
|
|
|
if command.include?('npm test') || command.include?('yarn test')
|
|
parse_test_results(stdout)
|
|
elsif command.include?('git status')
|
|
parse_git_status(stdout)
|
|
elsif command.include?('npm install') || command.include?('yarn install')
|
|
parse_install_results(stdout, stderr)
|
|
end
|
|
end
|
|
|
|
def analyze_file_modifications
|
|
file_path = tool_input['file_path'] || ''
|
|
log "File modified: #{file_path}"
|
|
|
|
# Example: Track important file changes
|
|
if file_path.end_with?('package.json')
|
|
log 'Package.json modified - dependencies may have changed'
|
|
elsif file_path.end_with?('.env')
|
|
log 'Environment file modified', level: :warn
|
|
elsif file_path.match?(/\.(js|ts|jsx|tsx)$/)
|
|
log 'Source code file modified'
|
|
end
|
|
end
|
|
|
|
def analyze_search_results
|
|
return unless tool_result.is_a?(Hash)
|
|
|
|
results = tool_result['results'] || []
|
|
log "Search returned #{results.length} results"
|
|
|
|
# Example: Log interesting search patterns
|
|
if results.length > 100
|
|
log 'Large search result set - consider narrowing search', level: :warn
|
|
elsif results.empty?
|
|
log 'No search results found'
|
|
end
|
|
end
|
|
|
|
def extract_errors_and_warnings
|
|
return unless tool_result.is_a?(Hash)
|
|
|
|
output_text = [
|
|
tool_result['stdout'],
|
|
tool_result['stderr']
|
|
].compact.join("\n")
|
|
|
|
# Extract common error patterns
|
|
error_patterns = [
|
|
/error:/i,
|
|
/exception:/i,
|
|
/failed:/i,
|
|
/cannot find/i,
|
|
/permission denied/i
|
|
]
|
|
|
|
warning_patterns = [
|
|
/warning:/i,
|
|
/deprecated:/i,
|
|
/caution:/i
|
|
]
|
|
|
|
error_patterns.each do |pattern|
|
|
if output_text.match?(pattern)
|
|
log 'Error detected in tool output', level: :error
|
|
break
|
|
end
|
|
end
|
|
|
|
warning_patterns.each do |pattern|
|
|
if output_text.match?(pattern)
|
|
log 'Warning detected in tool output', level: :warn
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
def update_project_state
|
|
# Example: Update cached project information based on tool results
|
|
case tool_name
|
|
when 'Bash'
|
|
command = tool_input['command'] || ''
|
|
|
|
if command.include?('git checkout') && tool_result['exit_code'].zero?
|
|
log 'Git branch changed - project state updated'
|
|
elsif command.include?('npm install') && tool_result['exit_code'].zero?
|
|
log 'Dependencies installed - project state updated'
|
|
end
|
|
end
|
|
end
|
|
|
|
def log_execution_metrics
|
|
# Example: Log performance and usage metrics
|
|
return unless tool_result.is_a?(Hash)
|
|
|
|
duration = tool_result['duration_ms']
|
|
log "Tool execution time: #{duration}ms" if duration
|
|
|
|
# Track resource usage
|
|
return unless tool_name == 'Bash'
|
|
|
|
command = tool_input['command'] || ''
|
|
log "Executed command: #{command[0..50]}#{'...' if command.length > 50}"
|
|
end
|
|
|
|
def parse_test_results(stdout)
|
|
if stdout.include?('passing') || stdout.include?('✓')
|
|
log 'Tests appear to be passing'
|
|
elsif stdout.include?('failing') || stdout.include?('✗')
|
|
log 'Tests appear to be failing', level: :warn
|
|
end
|
|
end
|
|
|
|
def parse_git_status(stdout)
|
|
if stdout.include?('nothing to commit')
|
|
log 'Git working directory is clean'
|
|
elsif stdout.include?('Changes not staged')
|
|
log 'Unstaged changes detected'
|
|
end
|
|
end
|
|
|
|
def parse_install_results(stdout, stderr)
|
|
if stderr&.include?('WARN')
|
|
log 'Package installation completed with warnings', level: :warn
|
|
elsif stdout&.include?('added')
|
|
log 'Packages installed successfully'
|
|
end
|
|
end
|
|
end
|
|
|
|
# Testing support - run this file directly to test with sample data
|
|
if __FILE__ == $PROGRAM_NAME
|
|
ClaudeHooks::CLI.test_runner(PostToolUseHandler) do |input_data|
|
|
input_data['tool_name'] = 'Bash'
|
|
input_data['tool_input'] = { 'command' => 'npm test' }
|
|
input_data['tool_result'] = { 'exit_code' => 0, 'stdout' => 'All tests passing' }
|
|
input_data['session_id'] = 'test-session-01'
|
|
end
|
|
end
|