const DESTRUCTIVE_PATTERNS: RegExp[] = [ /\brm\b/i, /\brmdir\b/i, /\bmv\b/i, /\bcp\b/i, /\bmkdir\b/i, /\btouch\b/i, /\bchmod\b/i, /\bchown\b/i, /\bchgrp\b/i, /\bln\b/i, /\btee\b/i, /\btruncate\b/i, /\bdd\b/i, /\bshred\b/i, /(^|[^<])>(?!>)/, />>/, /<<[-\w]*/, /`/, /\$\(/, /\bnpm\s+(install|uninstall|update|ci|link|publish|cache|exec|create)\b/i, /\bnpm\s+audit\s+fix\b/i, /\byarn\s+(add|remove|install|up|upgrade|set|dlx|create|init)\b/i, /\bpnpm\s+(add|remove|install|update|up|dlx|create|init)\b/i, /\bpip\s+(install|uninstall)\b/i, /\bapt(-get)?\s+(install|remove|purge|update|upgrade)\b/i, /\bbrew\s+(install|uninstall|upgrade|tap|untap)\b/i, /\bgit\s+(add|commit|push|pull|merge|rebase|reset|checkout|switch|restore|stash|cherry-pick|revert|tag|init|clone|fetch|remote\s+(add|remove|rename|set-head|set-branches|set-url|prune)|config\s+--(?:add|replace-all|unset|unset-all)|branch\s+(?!$|--show-current\b|-a\b|-r\b|--all\b|--remotes\b|--list\b|--contains\b|--no-contains\b|--merged\b|--no-merged\b|--points-at\b|--format\b|--sort\b))/i, /\bsudo\b/i, /\bsu\b/i, /\bkill\b/i, /\bpkill\b/i, /\bkillall\b/i, /\breboot\b/i, /\bshutdown\b/i, /\bsystemctl\s+(start|stop|restart|enable|disable|reload)\b/i, /\bservice\s+\S+\s+(start|stop|restart|reload)\b/i, /\b(vim?|nano|emacs|code|subl|mate)\b/i, /\|\s*(sh|bash|zsh|fish)\b/i, /\bcurl\b.*\s(-X|--request)\s+(POST|PUT|PATCH|DELETE)\b/i, /\bcurl\b.*\s(-d|--data|--data-raw|--data-binary|-F|--form|--upload-file|-T|-o|--output)\b/i, /\bwget\b.*\s(--post-data|--method=POST|--body-data|--output-document(=|\s+)(?!-\b))/i, /\bgh\s+(repo\s+(create|delete|edit|rename|archive|unarchive|clone)|pr\s+(create|merge|close|reopen|edit|ready)|issue\s+(create|edit|close|reopen|delete)|secret\s+(set|delete)|variable\s+(set|delete)|release\s+(create|delete|edit)|auth\s+login)\b/i, /\bterraform\s+(apply|destroy|import|init|taint|untaint|state\s+(mv|rm|replace-provider)|workspace\s+(new|delete)|force-unlock)\b/i, /\bkubectl\s+(apply|create|delete|edit|patch|replace|scale|annotate|label|cordon|uncordon|drain|taint|rollout\s+restart|set\b|exec\b|cp\b|attach\b)\b/i, /\bdocker\s+(run|exec|build|compose\s+(up|down|restart|build|run)|rm|rmi|tag|push|pull|login|logout|container\s+(run|exec|rm|restart|stop|start)|image\s+(rm|build|pull|push)|network\s+(create|rm)|volume\s+(create|rm))\b/i, ]; const SAFE_SEGMENT_PATTERNS: RegExp[] = [ /^cat\b/i, /^head\b/i, /^tail\b/i, /^less\b/i, /^more\b/i, /^grep\b/i, /^rg\b/i, /^find\b/i, /^fd\b/i, /^ls\b/i, /^tree\b/i, /^pwd\b/i, /^wc\b/i, /^sort\b/i, /^uniq\b/i, /^diff\b/i, /^file\b/i, /^stat\b/i, /^du\b/i, /^df\b/i, /^which\b/i, /^whereis\b/i, /^type\b/i, /^env\b/i, /^printenv\b/i, /^uname\b/i, /^whoami\b/i, /^id\b/i, /^date\b/i, /^cal\b/i, /^uptime\b/i, /^ps\b/i, /^top\b/i, /^htop\b/i, /^jq\b/i, /^awk\b/i, /^sed\s+-n\b/i, /^bat\b/i, /^help\b/i, /^man\b/i, /^\S+\s+--help(?:\s|$)/i, /^\S+\s+-h(?:\s|$)/i, /^\S+\s+help(?:\s|$)/i, /^\S+\s+--version(?:\s|$)/i, /^curl\b/i, /^wget\s+-O\s*-\b/i, /^node\s+(?:--version|-v)\b/i, /^python\s+(?:--version|-V)\b/i, /^python3\s+(?:--version|-V)\b/i, /^git\s+status(?:\s|$)/i, /^git\s+log(?:\s|$)/i, /^git\s+diff(?:\s|$)/i, /^git\s+show(?:\s|$)/i, /^git\s+grep(?:\s|$)/i, /^git\s+ls-files(?:\s|$)/i, /^git\s+ls-tree(?:\s|$)/i, /^git\s+rev-parse(?:\s|$)/i, /^git\s+describe(?:\s|$)/i, /^git\s+config\s+--(?:get|get-all)\b/i, /^git\s+branch\s*$/i, /^git\s+branch\s+(?:--show-current|-a|-r|--all|--remotes)(?:\s+(?:--show-current|-a|-r|--all|--remotes))*\s*$/i, /^git\s+branch\s+--list(?:[=\s].*)?$/i, /^git\s+branch\s+--(?:contains|no-contains|merged|no-merged|points-at|format|sort)\b.*$/i, /^git\s+remote\s*$/i, /^git\s+remote\s+-v\s*$/i, /^git\s+remote\s+show\s+\S+(?:\s+\S+)*\s*$/i, /^git\s+remote\s+get-url\s+\S+\s*$/i, /^npm\s+(?:list|ls|view|info|search|outdated)(?:\s|$)/i, /^npm\s+audit(?:\s+--[\w-]+(?:[=\s][^\s]+)?)*\s*$/i, /^yarn\s+(?:list|info|why)(?:\s|$)/i, /^yarn\s+audit(?:\s+--[\w-]+(?:[=\s][^\s]+)?)*\s*$/i, /^pnpm\s+list(?:\s|$)/i, /^pnpm\s+why(?:\s|$)/i, /^pnpm\s+audit(?:\s+--[\w-]+(?:[=\s][^\s]+)?)*\s*$/i, ]; function stripLeadingEnv(segment: string): string { let remaining = segment.trim(); const assignmentPattern = /^[A-Za-z_][A-Za-z0-9_]*=(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\s]+)/; while (remaining.length > 0) { const match = remaining.match(assignmentPattern); if (!match) break; remaining = remaining.slice(match[0].length).trim(); } return remaining; } function normalizeSegment(segment: string): string { let normalized = segment.trim(); normalized = normalized.replace(/^[!(\s]+/, "").trim(); normalized = stripLeadingEnv(normalized); return normalized; } function splitIntoSegments(command: string): string[] { const segments: string[] = []; let current = ""; let quote: "single" | "double" | "backtick" | null = null; let escaped = false; const pushCurrent = () => { const normalized = normalizeSegment(current); if (normalized) segments.push(normalized); current = ""; }; for (let i = 0; i < command.length; i++) { const char = command[i]; const next = command[i + 1]; if (escaped) { current += char; escaped = false; continue; } if (char === "\\") { current += char; escaped = true; continue; } if (quote !== null) { current += char; if ( (quote === "single" && char === "'") || (quote === "double" && char === '"') || (quote === "backtick" && char === "`") ) { quote = null; } continue; } if (char === "'") { quote = "single"; current += char; continue; } if (char === '"') { quote = "double"; current += char; continue; } if (char === "`") { quote = "backtick"; current += char; continue; } if ((char === "&" && next === "&") || (char === "|" && next === "|")) { pushCurrent(); i++; continue; } if (char === "|" || char === ";" || char === "\n") { pushCurrent(); continue; } current += char; } pushCurrent(); return segments; } const GENERIC_CLI_PATTERN = /^(?:[./~@A-Za-z0-9][./~@A-Za-z0-9:_-]*)(?:\s+.*)?$/; function isSafeSegment(segment: string): boolean { return SAFE_SEGMENT_PATTERNS.some((pattern) => pattern.test(segment)) || GENERIC_CLI_PATTERN.test(segment); } export function isSafeCommand(command: string): boolean { const trimmed = command.trim(); if (!trimmed) return false; if (DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(trimmed))) { return false; } const segments = splitIntoSegments(trimmed); if (segments.length === 0) return false; return segments.every((segment) => isSafeSegment(segment)); }