#!/usr/bin/env bun /** * Claude Code "Before Tools" Hook - Command Validation Script * * This script validates commands before execution to prevent harmful operations. * It receives command data via stdin and returns exit code 0 (allow) or 1 (block). * * Usage: Called automatically by Claude Code PreToolUse hook * Manual test: echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | bun validate-command.js */ // Comprehensive dangerous command patterns database const SECURITY_RULES = { // Critical system destruction commands CRITICAL_COMMANDS: [ 'del', 'format', 'mkfs', 'shred', 'dd', 'fdisk', 'parted', 'gparted', 'cfdisk', ], // Privilege escalation and system access PRIVILEGE_COMMANDS: [ 'sudo', 'su', 'passwd', 'chpasswd', 'usermod', 'chmod', 'chown', 'chgrp', 'setuid', 'setgid', ], // Network and remote access tools NETWORK_COMMANDS: [ 'nc', 'netcat', 'nmap', 'telnet', 'ssh-keygen', 'iptables', 'ufw', 'firewall-cmd', 'ipfw', ], // System service and process manipulation SYSTEM_COMMANDS: [ 'systemctl', 'service', 'kill', 'killall', 'pkill', 'mount', 'umount', 'swapon', 'swapoff', ], // Dangerous regex patterns DANGEROUS_PATTERNS: [ // File system destruction - block rm -rf with absolute paths /rm\s+.*-rf\s*\/\s*$/i, // rm -rf ending at root directory /rm\s+.*-rf\s*\/\w+/i, // rm -rf with any absolute path /rm\s+.*-rf\s*\/etc/i, // rm -rf in /etc /rm\s+.*-rf\s*\/usr/i, // rm -rf in /usr /rm\s+.*-rf\s*\/bin/i, // rm -rf in /bin /rm\s+.*-rf\s*\/sys/i, // rm -rf in /sys /rm\s+.*-rf\s*\/proc/i, // rm -rf in /proc /rm\s+.*-rf\s*\/boot/i, // rm -rf in /boot /rm\s+.*-rf\s*\/home\/[^\/]*\s*$/i, // rm -rf entire home directory /rm\s+.*-rf\s*\.\.+\//i, // rm -rf with parent directory traversal /rm\s+.*-rf\s*\*.*\*/i, // rm -rf with multiple wildcards /rm\s+.*-rf\s*\$\w+/i, // rm -rf with variables (could be dangerous) />\s*\/dev\/(sda|hda|nvme)/i, /dd\s+.*of=\/dev\//i, /shred\s+.*\/dev\//i, /mkfs\.\w+\s+\/dev\//i, // Fork bomb and resource exhaustion /:\(\)\{\s*:\|:&\s*\};:/, /while\s+true\s*;\s*do.*done/i, /for\s*\(\(\s*;\s*;\s*\)\)/i, // Command injection and chaining /;\s*(rm|dd|mkfs|format)/i, /&&\s*(rm|dd|mkfs|format)/i, /\|\|\s*(rm|dd|mkfs|format)/i, // Remote code execution /\|\s*(sh|bash|zsh|fish)$/i, /(wget|curl)\s+.*\|\s*(sh|bash)/i, /(wget|curl)\s+.*-O-.*\|\s*(sh|bash)/i, // Command substitution with dangerous commands /`.*rm.*`/i, /\$\(.*rm.*\)/i, /`.*dd.*`/i, /\$\(.*dd.*\)/i, // Sensitive file access /cat\s+\/etc\/(passwd|shadow|sudoers)/i, />\s*\/etc\/(passwd|shadow|sudoers)/i, /echo\s+.*>>\s*\/etc\/(passwd|shadow|sudoers)/i, // Network exfiltration /\|\s*nc\s+\S+\s+\d+/i, /curl\s+.*-d.*\$\(/i, /wget\s+.*--post-data.*\$\(/i, // Log manipulation />\s*\/var\/log\//i, /rm\s+\/var\/log\//i, /echo\s+.*>\s*~?\/?\.bash_history/i, // Backdoor creation /nc\s+.*-l.*-e/i, /nc\s+.*-e.*-l/i, /ncat\s+.*--exec/i, /ssh-keygen.*authorized_keys/i, // Crypto mining and malicious downloads /(wget|curl).*\.(sh|py|pl|exe|bin).*\|\s*(sh|bash|python)/i, /(xmrig|ccminer|cgminer|bfgminer)/i, // Hardware direct access /cat\s+\/dev\/(mem|kmem)/i, /echo\s+.*>\s*\/dev\/(mem|kmem)/i, // Kernel module manipulation /(insmod|rmmod|modprobe)\s+/i, // Cron job manipulation /crontab\s+-e/i, /echo\s+.*>>\s*\/var\/spool\/cron/i, // Environment variable exposure /env\s*\|\s*grep.*PASSWORD/i, /printenv.*PASSWORD/i, ], // Paths that should never be written to PROTECTED_PATHS: [ '/etc/', '/usr/', '/bin/', '/sbin/', '/boot/', '/sys/', '/proc/', '/dev/', '/root/', ], }; // Allowlist of safe commands (when used appropriately) const SAFE_COMMANDS = [ 'ls', 'dir', 'pwd', 'whoami', 'date', 'echo', 'cat', 'head', 'tail', 'grep', 'find', 'wc', 'sort', 'uniq', 'cut', 'awk', 'sed', 'git', 'npm', 'pnpm', 'node', 'bun', 'python', 'pip', 'cd', 'cp', 'mv', 'mkdir', 'touch', 'ln', ]; class CommandValidator { constructor() { this.logFile = '/Users/david/.claude/security.log'; } /** * Main validation function */ validate(command, toolName = 'Unknown') { const result = { isValid: true, severity: 'LOW', violations: [], sanitizedCommand: command, }; if (!command || typeof command !== 'string') { result.isValid = false; result.violations.push('Invalid command format'); return result; } // Normalize command for analysis const normalizedCmd = command.trim().toLowerCase(); const cmdParts = normalizedCmd.split(/\s+/); const mainCommand = cmdParts[0]; // Check against critical commands if (SECURITY_RULES.CRITICAL_COMMANDS.includes(mainCommand)) { result.isValid = false; result.severity = 'CRITICAL'; result.violations.push(`Critical dangerous command: ${mainCommand}`); } // Check privilege escalation commands if (SECURITY_RULES.PRIVILEGE_COMMANDS.includes(mainCommand)) { result.isValid = false; result.severity = 'HIGH'; result.violations.push(`Privilege escalation command: ${mainCommand}`); } // Check network commands if (SECURITY_RULES.NETWORK_COMMANDS.includes(mainCommand)) { result.isValid = false; result.severity = 'HIGH'; result.violations.push(`Network/remote access command: ${mainCommand}`); } // Check system commands if (SECURITY_RULES.SYSTEM_COMMANDS.includes(mainCommand)) { result.isValid = false; result.severity = 'HIGH'; result.violations.push(`System manipulation command: ${mainCommand}`); } // Check dangerous patterns for (const pattern of SECURITY_RULES.DANGEROUS_PATTERNS) { if (pattern.test(command)) { result.isValid = false; result.severity = 'CRITICAL'; result.violations.push(`Dangerous pattern detected: ${pattern.source}`); } } // Check for protected path access (but allow common redirections like /dev/null) for (const path of SECURITY_RULES.PROTECTED_PATHS) { if (command.includes(path)) { // Allow common safe redirections if ( path === '/dev/' && (command.includes('/dev/null') || command.includes('/dev/stderr') || command.includes('/dev/stdout')) ) { continue; } result.isValid = false; result.severity = 'HIGH'; result.violations.push(`Access to protected path: ${path}`); } } // Additional safety checks if (command.length > 2000) { result.isValid = false; result.severity = 'MEDIUM'; result.violations.push('Command too long (potential buffer overflow)'); } // Check for binary/encoded content if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]/.test(command)) { result.isValid = false; result.severity = 'HIGH'; result.violations.push('Binary or encoded content detected'); } return result; } /** * Log security events */ async logSecurityEvent(command, toolName, result, sessionId = null) { const timestamp = new Date().toISOString(); const logEntry = { timestamp, sessionId, toolName, command: command.substring(0, 500), // Truncate for logs blocked: !result.isValid, severity: result.severity, violations: result.violations, source: 'claude-code-hook', }; try { // Write to log file const logLine = JSON.stringify(logEntry) + '\n'; await Bun.write(this.logFile, logLine, { createPath: true, flag: 'a' }); // Also output to stderr for immediate visibility console.error( `[SECURITY] ${result.isValid ? 'ALLOWED' : 'BLOCKED'}: ${command.substring(0, 100)}` ); } catch (error) { console.error('Failed to write security log:', error); } } /** * Check if command matches any allowed patterns from settings */ isExplicitlyAllowed(command, allowedPatterns = []) { for (const pattern of allowedPatterns) { // Convert Claude Code permission pattern to regex // e.g., "Bash(git *)" becomes /^git\s+.*$/ if (pattern.startsWith('Bash(') && pattern.endsWith(')')) { const cmdPattern = pattern.slice(5, -1); // Remove "Bash(" and ")" const regex = new RegExp('^' + cmdPattern.replace(/\*/g, '.*') + '$', 'i'); if (regex.test(command)) { return true; } } } return false; } } /** * Main execution function */ async function main() { const validator = new CommandValidator(); try { // Read hook input from stdin const stdin = process.stdin; const chunks = []; for await (const chunk of stdin) { chunks.push(chunk); } const input = Buffer.concat(chunks).toString(); if (!input.trim()) { console.error('No input received from stdin'); process.exit(1); } // Parse Claude Code hook JSON format let hookData; try { hookData = JSON.parse(input); } catch (error) { console.error('Invalid JSON input:', error.message); process.exit(1); } const toolName = hookData.tool_name || 'Unknown'; const toolInput = hookData.tool_input || {}; const sessionId = hookData.session_id || null; // Only validate Bash commands for now if (toolName !== 'Bash') { console.log(`Skipping validation for tool: ${toolName}`); process.exit(0); } const command = toolInput.command; if (!command) { console.error('No command found in tool input'); process.exit(1); } // Validate the command const result = validator.validate(command, toolName); // Log the security event await validator.logSecurityEvent(command, toolName, result, sessionId); // Output result and exit with appropriate code if (result.isValid) { console.log('Command validation passed'); process.exit(0); // Allow execution } else { console.error(`Command validation failed: ${result.violations.join(', ')}`); console.error(`Severity: ${result.severity}`); process.exit(2); // Block execution (Claude Code requires exit code 2) } } catch (error) { console.error('Validation script error:', error); // Fail safe - block execution on any script error process.exit(2); } } // Execute main function main().catch(error => { console.error('Fatal error:', error); process.exit(2); });