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
8 changes: 6 additions & 2 deletions packages/opencode/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ export const BashTool = Tool.define("bash", async () => {

for (const node of tree.rootNode.descendantsOfType("command")) {
if (!node) continue

// Get full command text including redirects if present
let commandText = node.parent?.type === "redirected_statement" ? node.parent.text : node.text

const command = []
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i)
Expand Down Expand Up @@ -128,8 +132,8 @@ export const BashTool = Tool.define("bash", async () => {

// cd covered by above check
if (command.length && command[0] !== "cd") {
patterns.add(command.join(" "))
always.add(BashArity.prefix(command).join(" ") + "*")
patterns.add(commandText)
always.add(BashArity.prefix(command).join(" ") + " *")
}
}

Expand Down
44 changes: 44 additions & 0 deletions packages/opencode/test/tool/bash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { BashTool } from "../../src/tool/bash"
import { Instance } from "../../src/project/instance"
import { tmpdir } from "../fixture/fixture"
import type { PermissionNext } from "../../src/permission/next"
import { Wildcard } from "../../src/util/wildcard"

const ctx = {
sessionID: "test",
Expand Down Expand Up @@ -229,4 +230,47 @@ describe("tool.bash permissions", () => {
},
})
})

test("matches redirects in permission pattern", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const bash = await BashTool.init()
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
const testCtx = {
...ctx,
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
requests.push(req)
},
}
await bash.execute({ command: "cat > /tmp/output.txt", description: "Redirect ls output" }, testCtx)
const bashReq = requests.find((r) => r.permission === "bash")
expect(bashReq).toBeDefined()
expect(bashReq!.patterns).toContain("cat > /tmp/output.txt")
},
})
})

test("always pattern has space before wildcard to not include different commands", async () => {
await using tmp = await tmpdir({ git: true })
await Instance.provide({
directory: tmp.path,
fn: async () => {
const bash = await BashTool.init()
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
const testCtx = {
...ctx,
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
requests.push(req)
},
}
await bash.execute({ command: "ls -la", description: "List" }, testCtx)
const bashReq = requests.find((r) => r.permission === "bash")
expect(bashReq).toBeDefined()
const pattern = bashReq!.always[0]
expect(pattern).toBe("ls *")
},
})
})
})