src / safety.ts
type Rule = {
pattern: RegExp
reason: string
}
const RULES: Rule[] = [
// ── Destructive filesystem ──
{ pattern: /\brm\b.*-[^\s]*[rf]/, reason: "recursive/forced rm" },
{ pattern: /\bmkfs\b/, reason: "filesystem format" },
{ pattern: /\bdd\b\s+.*\bof=/, reason: "dd write" },
{ pattern: />\s*\/dev\//, reason: "write to device" },
// ── System control ──
{ pattern: /\b(shutdown|reboot|halt|poweroff|init\s+[0-6])\b/, reason: "system power control" },
{ pattern: /\bsystemctl\s+(stop|disable|mask)\b/, reason: "service disruption" },
{ pattern: /\blaunchctl\s+(unload|remove)\b/, reason: "service disruption (macOS)" },
{ pattern: /\bkillall\b/, reason: "mass process kill" },
// ── Permissions / ownership (broad scope) ──
{ pattern: /\bchmod\b.*-[^\s]*R/, reason: "recursive permission change" },
{ pattern: /\bchown\b.*-[^\s]*R/, reason: "recursive ownership change" },
{ pattern: /\bchmod\b.*\s+\//, reason: "permission change on absolute path" },
// ── Pipe-to-shell / eval ──
{ pattern: /\bcurl\b.*\|\s*(ba)?sh\b/, reason: "curl pipe to shell" },
{ pattern: /\bwget\b.*\|\s*(ba)?sh\b/, reason: "wget pipe to shell" },
{ pattern: /\beval\b/, reason: "eval execution" },
{ pattern: /\$\(.*\)\s*\|\s*(ba)?sh/, reason: "subshell pipe to shell" },
// ── Fork bomb patterns ──
{ pattern: /:\(\)\s*\{.*\|.*&\s*\}\s*;/, reason: "fork bomb" },
{ pattern: /\bwhile\s+true.*do.*fork\b/i, reason: "fork loop" },
// ── Exfiltration-shaped ──
{ pattern: /\bcurl\b.*-[^\s]*(X\s*POST|d\s)/, reason: "curl POST (data exfil)" },
{ pattern: /\bcurl\b.*--data/, reason: "curl data upload" },
{ pattern: /\bscp\b.*@/, reason: "scp to remote host" },
{ pattern: /\brsync\b.*@/, reason: "rsync to remote host" },
{ pattern: /\bnc\b.*-[^\s]*[le]/, reason: "netcat listener" },
// ── Sensitive file access ──
{ pattern: /\bcat\b.*\.(pem|key|env|ssh|pgpass|netrc)\b/, reason: "read sensitive file" },
{ pattern: /\/etc\/(shadow|passwd|sudoers)\b/, reason: "system credential file access" },
{ pattern: /~\/\.ssh\//, reason: "SSH directory access" },
// ── History / credential scraping ──
{ pattern: /\bhistory\b/, reason: "shell history access" },
{ pattern: /\bkeychain\b/i, reason: "keychain access" },
{ pattern: /\bsecurity\s+find-(generic|internet)-password\b/, reason: "macOS credential access" },
// ── Disk / partition ops ──
{ pattern: /\bfdisk\b/, reason: "partition table modification" },
{ pattern: /\bdiskutil\s+(erase|partitionDisk|unmount)\b/, reason: "disk utility destructive op" },
]
type SafetyResult =
| { safe: true }
| { safe: false; reason: string; rule: string }
export function isSafe(command: string): SafetyResult {
// normalize: collapse whitespace, strip leading sudo
const normalized = command
.replace(/\s+/g, " ")
.trim()
.replace(/^sudo\s+/, "")
for (const { pattern, reason } of RULES) {
if (pattern.test(normalized)) {
return { safe: false, reason, rule: pattern.source }
}
}
return { safe: true }
}