Skip to content

Commit 88ffff3

Browse files
committed
(fix) handle redirected_statement treesitter node in bash permissions
1 parent aedd760 commit 88ffff3

File tree

2 files changed

+49
-2
lines changed

2 files changed

+49
-2
lines changed

packages/opencode/src/tool/bash.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ export const BashTool = Tool.define("bash", async () => {
9191

9292
for (const node of tree.rootNode.descendantsOfType("command")) {
9393
if (!node) continue
94+
95+
// Get full command text including redirects if present
96+
let commandText = node.parent?.type === "redirected_statement" ? node.parent.text : node.text
97+
9498
const command = []
9599
for (let i = 0; i < node.childCount; i++) {
96100
const child = node.child(i)
@@ -131,8 +135,8 @@ export const BashTool = Tool.define("bash", async () => {
131135

132136
// cd covered by above check
133137
if (command.length && command[0] !== "cd") {
134-
patterns.add(command.join(" "))
135-
always.add(BashArity.prefix(command).join(" ") + "*")
138+
patterns.add(commandText)
139+
always.add(BashArity.prefix(command).join(" ") + " *")
136140
}
137141
}
138142

packages/opencode/test/tool/bash.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,49 @@ describe("tool.bash permissions", () => {
231231
},
232232
})
233233
})
234+
235+
test("matches redirects in permission pattern", async () => {
236+
await using tmp = await tmpdir({ git: true })
237+
await Instance.provide({
238+
directory: tmp.path,
239+
fn: async () => {
240+
const bash = await BashTool.init()
241+
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
242+
const testCtx = {
243+
...ctx,
244+
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
245+
requests.push(req)
246+
},
247+
}
248+
await bash.execute({ command: "cat > /tmp/output.txt", description: "Redirect ls output" }, testCtx)
249+
const bashReq = requests.find((r) => r.permission === "bash")
250+
expect(bashReq).toBeDefined()
251+
expect(bashReq!.patterns).toContain("cat > /tmp/output.txt")
252+
},
253+
})
254+
})
255+
256+
test("always pattern has space before wildcard to not include different commands", async () => {
257+
await using tmp = await tmpdir({ git: true })
258+
await Instance.provide({
259+
directory: tmp.path,
260+
fn: async () => {
261+
const bash = await BashTool.init()
262+
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
263+
const testCtx = {
264+
...ctx,
265+
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
266+
requests.push(req)
267+
},
268+
}
269+
await bash.execute({ command: "ls -la", description: "List" }, testCtx)
270+
const bashReq = requests.find((r) => r.permission === "bash")
271+
expect(bashReq).toBeDefined()
272+
const pattern = bashReq!.always[0]
273+
expect(pattern).toBe("ls *")
274+
},
275+
})
276+
})
234277
})
235278

236279
describe("tool.bash truncation", () => {

0 commit comments

Comments
 (0)