From 68f7fa0bbbd84274f4e2f2acdb753901f9bdca19 Mon Sep 17 00:00:00 2001 From: qhx Date: Sun, 19 Jul 2026 18:44:23 +0800 Subject: [PATCH] feat(core): add shell command interception for destructive operations --- packages/core/src/tool/bash.ts | 29 ++++ packages/opencode/src/tool/shell.ts | 4 + packages/opencode/src/tool/shell/dangerous.ts | 140 ++++++++++++++++++ packages/opencode/src/tool/shell/shell.txt | 7 + 4 files changed, 180 insertions(+) create mode 100644 packages/opencode/src/tool/shell/dangerous.ts diff --git a/packages/core/src/tool/bash.ts b/packages/core/src/tool/bash.ts index 22423764bc67..6badaea74889 100644 --- a/packages/core/src/tool/bash.ts +++ b/packages/core/src/tool/bash.ts @@ -20,6 +20,24 @@ export const DEFAULT_TIMEOUT_MS = 2 * 60 * 1_000 export const MAX_TIMEOUT_MS = 10 * 60 * 1_000 export const MAX_CAPTURE_BYTES = 1024 * 1024 +const DANGEROUS_PATTERNS = [ + { pattern: /^rm\s/, reason: "File deletion via shell is irreversible" }, + { pattern: /^rmdir\s/, reason: "Directory deletion via shell is irreversible" }, + { pattern: /^mv\s/, reason: "mv can overwrite files and delete the source" }, + { pattern: /^Remove-Item\b/i, reason: "File deletion via PowerShell is blocked" }, + { pattern: /^Set-Content\b/i, reason: "Use the Write tool instead" }, + { pattern: /^Add-Content\b/i, reason: "Use the Write tool instead" }, + { pattern: /^Clear-Content\b/i, reason: "Content clearing is blocked" }, + { pattern: /^Move-Item\b/i, reason: "Move is blocked; can overwrite files" }, + { pattern: /^Rename-Item\b/i, reason: "Rename is blocked; can overwrite files" }, + { pattern: /^del\b/i, reason: "File deletion via cmd is irreversible" }, + { pattern: /^erase\b/i, reason: "File deletion via cmd is irreversible" }, + { pattern: /^rd\b/i, reason: "Directory deletion via cmd is irreversible" }, + { pattern: /^format\b/i, reason: "Format operations are blocked" }, + { pattern: /^diskpart\b/i, reason: "Disk operations are blocked" }, + { pattern: /sed\s+.*\b-i\b/, reason: "Use the Edit tool instead of sed -i" }, +] + export const Input = Schema.Struct({ command: Schema.String.annotate({ description: "Shell command string to execute" }), workdir: Schema.String.pipe(Schema.optional).annotate({ @@ -148,6 +166,17 @@ const layer = Layer.effectDiscard( source, }) + const cmdCheck = input.command.trim() + for (const { pattern, reason } of DANGEROUS_PATTERNS) { + if (pattern.test(cmdCheck)) { + return yield* Effect.fail( + new ToolFailure({ + message: `Command blocked: "${input.command}"\nReason: ${reason}\nSuggestion: Use the dedicated opencode tools (Write, Edit, Read) instead.`, + }), + ) + } + } + if ((yield* fs.stat(target.canonical)).type !== "Directory") return yield* Effect.fail(new Error(`Working directory is not a directory: ${target.canonical}`)) diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index 1e4423e01774..f2e92bc126e1 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -21,6 +21,7 @@ import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" import { ShellPrompt, type Parameters } from "./shell/prompt" import { BashArity } from "@/permission/arity" +import { checkCommand } from "./shell/dangerous" export { Parameters } from "./shell/prompt" @@ -617,6 +618,7 @@ export const ShellTool = Tool.define( } const timeout = params.timeout ?? defaultTimeoutMs const ps = Shell.ps(shell) + const shellKind = ShellID.toKind(name) yield* Effect.scoped( Effect.gen(function* () { const tree = yield* Effect.acquireRelease(parse(params.command, ps), (tree) => @@ -624,6 +626,8 @@ export const ShellTool = Tool.define( ) const scan = yield* collect(tree.rootNode, cwd, ps, shell, instanceCtx) if (!containsPath(cwd, instanceCtx)) scan.dirs.add(cwd) + const blocked = checkCommand(tree.rootNode, ps, shellKind === "cmd") + if (blocked) throw new Error(blocked) yield* ask(ctx, scan, params) }), ) diff --git a/packages/opencode/src/tool/shell/dangerous.ts b/packages/opencode/src/tool/shell/dangerous.ts new file mode 100644 index 000000000000..4991c6012a69 --- /dev/null +++ b/packages/opencode/src/tool/shell/dangerous.ts @@ -0,0 +1,140 @@ +import type { Node } from "web-tree-sitter" + +type ShellType = "bash" | "ps" | "cmd" + +interface BlockedEntry { + commands: string[] + reason: string +} + +const BLOCKED: Record = { + bash: [ + { + commands: ["rm", "rmdir"], + reason: + "File/directory deletion via shell is irreversible. Use the dedicated Read/Edit tools.", + }, + { + commands: ["mv"], + reason: + "mv can overwrite files and delete the source, which is irreversible. Use cp instead.", + }, + { + commands: ["ren", "rename"], + reason: + "Renaming can overwrite existing files. Use cp instead.", + }, + { + commands: ["sed"], + reason: + "sed -i performs in-place file editing. Use the Edit tool instead.", + }, + { + commands: ["truncate", "tee"], + reason: + "File modification via shell is blocked. Use the dedicated Write/Edit tools.", + }, + { + commands: ["dd", "format", "mkfs", "fdisk", "parted", "diskpart"], + reason: + "Disk-level operations are blocked as they can cause irreversible data loss.", + }, + ], + ps: [ + { + commands: ["remove-item", "ri", "rm", "del", "erase", "rd", "rmdir"], + reason: + "File/directory deletion via PowerShell is irreversible.", + }, + { + commands: ["move-item", "mi", "mv", "move"], + reason: + "Move operations can overwrite files and delete the source. Use Copy-Item instead.", + }, + { + commands: ["rename-item", "rni", "ren", "rename"], + reason: + "Renaming can overwrite existing files. Use Copy-Item instead.", + }, + { + commands: ["set-content", "sc", "add-content", "ac"], + reason: + "File content modification via PowerShell is blocked. Use the dedicated Write/Edit tools.", + }, + { + commands: ["clear-content", "clc", "clear-item", "cli"], + reason: + "Content clearing via PowerShell is blocked.", + }, + { + commands: ["format-volume", "format-disk"], + reason: + "Volume formatting is blocked as it causes irreversible data loss.", + }, + ], + cmd: [ + { + commands: ["del", "erase", "rd", "rmdir"], + reason: + "File/directory deletion via cmd is irreversible.", + }, + { + commands: ["move", "ren", "rename"], + reason: + "Move/rename operations can overwrite files. Use copy instead.", + }, + { + commands: ["format"], + reason: + "Format operation is blocked as it causes irreversible data loss.", + }, + ], +} + +function commands(node: Node) { + return node.descendantsOfType("command").filter((child): child is Node => Boolean(child)) +} + +export function checkCommand(root: Node, ps: boolean, cmd: boolean): string | undefined { + const shell: ShellType = cmd ? "cmd" : ps ? "ps" : "bash" + const entries = BLOCKED[shell] + + for (const node of commands(root)) { + const commandName = node.child(0) + const name = commandName?.text.toLowerCase().trim() + if (!name) continue + + for (const entry of entries) { + if (!entry.commands.includes(name)) continue + + if (name === "sed") { + if (!node.text.includes("-i")) continue + } + + return `Command blocked: "${node.text.trim()}" +Reason: ${entry.reason} +Suggestion: Use the dedicated opencode tools (Write, Edit, Read) instead of shell commands for file operations. +Do not attempt to bypass this restriction using indirect methods like eval, exec, or scripts.` + } + } + + if (!ps) { + const redirectStats = root.descendantsOfType("redirected_statement") + for (const rs of redirectStats) { + for (let i = 0; i < rs.childCount; i++) { + const child = rs.child(i) + if (!child || child.type !== "file_redirect") continue + const op = child.child(0)?.text + if (op === ">" || op === ">|" || op === ">>" || op === "&>") { + return `Command blocked: shell redirect "${ + op + }" writes to files +Reason: Writing to files via shell redirect is blocked. Use the Write tool for file creation and the Edit tool for modifications. +Suggestion: Use the dedicated opencode tools instead.` + } + } + } + } + + return undefined +} diff --git a/packages/opencode/src/tool/shell/shell.txt b/packages/opencode/src/tool/shell/shell.txt index 22bd8f6c1ac0..8fcc1851619e 100644 --- a/packages/opencode/src/tool/shell/shell.txt +++ b/packages/opencode/src/tool/shell/shell.txt @@ -8,6 +8,13 @@ Use `${tmp}` for temporary work outside the workspace. This directory has alread IMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead. +BLOCKED COMMANDS (will be rejected at runtime): +- File/directory deletion: rm, rmdir, Remove-Item, del, erase, rd +- File overwrite/move/rename: mv, Move-Item, ren, rename +- File editing via shell: Set-Content, >/>> redirect, sed -i, truncate, tee +- Disk-level operations: format, dd, diskpart, mkfs, fdisk, parted +These are blocked because they cause irreversible data loss or bypass opencode's dedicated file tools (Read/Write/Edit). Do NOT attempt to bypass this restriction using indirect methods like eval, exec, script files, npm scripts, or any other workarounds. + ${commandSection} # Git and GitHub