Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/opencode/src/tool/safety.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* SafetyValidator — Pre-execution shell safety validator
*
* Checks shell/bash commands against known dangerous patterns before execution.
* Integrated directly into tool/registry.ts execute flow (no plugin hook needed).
*/

export interface SafetyResult {
allowed: boolean
level: "BLOCK" | "WARN" | "LOG"
description: string
suggestion?: string
}

const DANGER_RULES: Array<{
pattern: RegExp
level: "BLOCK" | "WARN" | "LOG"
description: string
suggestion?: string
}> = [
{ pattern: /\brm\s+-rf\b/, level: "BLOCK", description: "Recursive force delete", suggestion: "Use rm -ri for interactive confirmation" },
{ pattern: /\bdd\s+if=/, level: "BLOCK", description: "Raw disk write", suggestion: "Verify target device" },
{ pattern: /\bmkfs\./, level: "BLOCK", description: "Filesystem format", suggestion: "Double-check device name" },
{ pattern: /:\s*\(\s*\)\s*\{/, level: "BLOCK", description: "Shell fork bomb pattern" },
{ pattern: /\/dev\/null.*>.*\/dev\/sd/, level: "BLOCK", description: "Raw disk device write" },
{ pattern: /\bsudo\b/, level: "WARN", description: "Elevated privileges", suggestion: "Confirm sudo is needed" },
{ pattern: /\bchmod\s+777\b/, level: "WARN", description: "World-writable permissions", suggestion: "Use chmod 755 or stricter" },
{ pattern: /\bchmod\s+-R\b/, level: "WARN", description: "Recursive permission change" },
{ pattern: /\bgit\s+push\s+--force\b/, level: "WARN", description: "Force push" },
{ pattern: /\bgit\s+reset\s+--hard\b/, level: "WARN", description: "Hard reset" },
{ pattern: /\bdocker\s+rm\s+-f\b/, level: "WARN", description: "Force remove container" },
{ pattern: /\bnpm\s+(unpublish|deprecate)\b/, level: "WARN", description: "Registry mutation" },
{ pattern: /\bcurl\b.*\|\s*(ba)?sh\b/, level: "LOG", description: "Pipe to shell" },
{ pattern: /\bwget\b.*\|\s*(ba)?sh\b/, level: "LOG", description: "Pipe to shell" },
]

export function safetyCheck(command: string): SafetyResult | null {
for (const rule of DANGER_RULES) {
if (rule.pattern.test(command)) {
return {
allowed: rule.level !== "BLOCK",
level: rule.level,
description: rule.description,
suggestion: rule.suggestion,
}
}
}
return null
}
48 changes: 48 additions & 0 deletions packages/opencode/test/tool/safety.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test"
import { safetyCheck } from "../../src/tool/safety"

describe("safetyCheck", () => {
test("BLOCK: rm -rf detected", () => {
const r = safetyCheck("rm -rf /usr/local")
expect(r?.level).toBe("BLOCK")
expect(r?.allowed).toBe(false)
expect(r?.description).toContain("Recursive force delete")
})
test("BLOCK: dd if= detected", () => {
const r = safetyCheck("dd if=/dev/zero of=/dev/sda")
expect(r?.level).toBe("BLOCK")
})
test("BLOCK: mkfs detected", () => {
const r = safetyCheck("mkfs.ext4 /dev/sda1")
expect(r?.level).toBe("BLOCK")
})
test("BLOCK: fork bomb detected", () => {
const r = safetyCheck(":(){ :|:& };:")
expect(r?.level).toBe("BLOCK")
})
test("WARN: sudo detected", () => {
const r = safetyCheck("sudo systemctl restart nginx")
expect(r?.level).toBe("WARN")
expect(r?.allowed).toBe(true)
})
test("WARN: chmod 777 detected", () => {
const r = safetyCheck("chmod 777 /var/www")
expect(r?.level).toBe("WARN")
})
test("WARN: force push detected", () => {
const r = safetyCheck("git push --force origin main")
expect(r?.level).toBe("WARN")
})
test("WARN: npm deprecate detected", () => {
const r = safetyCheck("npm deprecate my-package")
expect(r?.level).toBe("WARN")
})
test("LOG: curl pipe sh detected", () => {
const r = safetyCheck("curl https://x.com | sh")
expect(r?.level).toBe("LOG")
})
test("safe command: ls -la passes", () => {
const r = safetyCheck("ls -la")
expect(r).toBeNull()
})
})
Loading