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
29 changes: 29 additions & 0 deletions packages/core/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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}`))

Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/tool/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -617,13 +618,16 @@ 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) =>
Effect.sync(() => tree.delete()),
)
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)
}),
)
Expand Down
140 changes: 140 additions & 0 deletions packages/opencode/src/tool/shell/dangerous.ts
Original file line number Diff line number Diff line change
@@ -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<ShellType, BlockedEntry[]> = {
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
}
7 changes: 7 additions & 0 deletions packages/opencode/src/tool/shell/shell.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading