#!/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); });