151 lines
4.0 KiB
Ruby
Executable File
151 lines
4.0 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
require 'claude_hooks'
|
|
|
|
# PreToolUse Handler
|
|
#
|
|
# PURPOSE: Control and validate tool usage before execution
|
|
# TRIGGERS: Before Claude Code executes any tool (Bash, Write, Edit, etc.)
|
|
#
|
|
# COMMON USE CASES:
|
|
# - Block dangerous commands (rm -rf, chmod 777, etc.)
|
|
# - Require approval for sensitive operations
|
|
# - Log tool usage for security auditing
|
|
# - Apply rate limiting or usage quotas
|
|
# - Validate file paths and permissions
|
|
# - Add safety checks for system commands
|
|
#
|
|
# SETTINGS.JSON CONFIGURATION:
|
|
# {
|
|
# "hooks": {
|
|
# "PreToolUse": [{
|
|
# "matcher": "",
|
|
# "hooks": [{
|
|
# "type": "command",
|
|
# "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/entrypoints/pre_tool_use.rb"
|
|
# }]
|
|
# }]
|
|
# }
|
|
# }
|
|
|
|
class PreToolUseHandler < ClaudeHooks::PreToolUse
|
|
def call
|
|
log "Checking tool usage: #{tool_name} with input: #{tool_input}"
|
|
|
|
# Example: Block dangerous commands
|
|
# check_dangerous_commands
|
|
|
|
# Example: Validate file operations
|
|
# validate_file_operations
|
|
|
|
# Example: Apply rate limiting
|
|
# check_rate_limits
|
|
|
|
# Example: Log tool usage
|
|
# log_tool_usage
|
|
|
|
# Default: approve the tool usage
|
|
approve_tool!('Tool usage approved')
|
|
|
|
output_data
|
|
end
|
|
|
|
private
|
|
|
|
def check_dangerous_commands
|
|
case tool_name
|
|
when 'Bash'
|
|
command = tool_input['command'] || ''
|
|
|
|
dangerous_patterns = [
|
|
%r{rm\s+-rf\s+/}, # rm -rf /
|
|
/chmod\s+777/, # chmod 777
|
|
/sudo\s+rm/, # sudo rm
|
|
%r{>\s*/dev/sd[a-z]}, # writing to disk devices
|
|
/mkfs\./, # filesystem creation
|
|
%r{dd\s+if=.*of=/dev} # disk imaging to devices
|
|
]
|
|
|
|
dangerous_patterns.each do |pattern|
|
|
next unless command.match?(pattern)
|
|
|
|
block_tool!("Dangerous command detected: #{pattern}")
|
|
log "Blocked dangerous command: #{command}", level: :error
|
|
return
|
|
end
|
|
|
|
when 'Write', 'Edit'
|
|
file_path = tool_input['file_path'] || ''
|
|
|
|
# Block writes to system files
|
|
if file_path.start_with?('/etc/', '/usr/', '/bin/', '/sbin/')
|
|
ask_for_permission!("Attempting to modify system file: #{file_path}")
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
def validate_file_operations
|
|
return unless %w[Write Edit MultiEdit].include?(tool_name)
|
|
|
|
file_path = tool_input['file_path'] || ''
|
|
|
|
# Ensure file path is within project directory
|
|
current_dir = cwd || Dir.pwd
|
|
unless file_path.start_with?(current_dir)
|
|
log "File operation outside project directory: #{file_path}", level: :warn
|
|
ask_for_permission!('File operation outside project directory')
|
|
return
|
|
end
|
|
|
|
# Check for sensitive files
|
|
sensitive_files = [
|
|
'.env',
|
|
'.env.local',
|
|
'config/secrets.yml',
|
|
'private_key',
|
|
'id_rsa'
|
|
]
|
|
|
|
return unless sensitive_files.any? { |f| file_path.include?(f) }
|
|
|
|
ask_for_permission!("Modifying potentially sensitive file: #{File.basename(file_path)}")
|
|
nil
|
|
end
|
|
|
|
def check_rate_limits
|
|
# Example: Implement rate limiting for expensive operations
|
|
return unless tool_name == 'Bash'
|
|
|
|
command = tool_input['command'] || ''
|
|
|
|
# Limit compilation commands
|
|
return unless command.match?(/gcc|g\+\+|clang|rustc|go build/)
|
|
|
|
log 'Compilation command detected, checking rate limits'
|
|
# Could implement actual rate limiting logic here
|
|
end
|
|
|
|
def log_tool_usage
|
|
log "Tool: #{tool_name}"
|
|
log "Session: #{session_id}"
|
|
log "Working directory: #{cwd || Dir.pwd}"
|
|
|
|
# Log tool input (be careful with sensitive data)
|
|
return unless tool_input.is_a?(Hash)
|
|
|
|
sanitized_input = tool_input.reject { |k, _v| k.to_s.downcase.include?('password') }
|
|
log "Input keys: #{sanitized_input.keys.join(', ')}"
|
|
end
|
|
end
|
|
|
|
# Testing support - run this file directly to test with sample data
|
|
if __FILE__ == $PROGRAM_NAME
|
|
ClaudeHooks::CLI.test_runner(PreToolUseHandler) do |input_data|
|
|
input_data['tool_name'] = 'Bash'
|
|
input_data['tool_input'] = { 'command' => 'ls -la' }
|
|
input_data['session_id'] = 'test-session-01'
|
|
end
|
|
end
|