From c83d3db0106eba82f40c8114cbd0fe82b6f2d51c Mon Sep 17 00:00:00 2001
From: ops
Date: Fri, 2 Jan 2026 21:58:59 +0100
Subject: [PATCH] (fix) handle redirected_statement treesitter node in bash
permissions
---
packages/opencode/src/tool/bash.ts | 8 +++--
packages/opencode/test/tool/bash.test.ts | 43 ++++++++++++++++++++++++
2 files changed, 49 insertions(+), 2 deletions(-)
diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts
index e06a3f157cb..251b70248ad 100644
--- a/packages/opencode/src/tool/bash.ts
+++ b/packages/opencode/src/tool/bash.ts
@@ -91,6 +91,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)
@@ -131,8 +135,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(" ") + " *")
}
}
diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts
index 750ff8193e9..df3625a8710 100644
--- a/packages/opencode/test/tool/bash.test.ts
+++ b/packages/opencode/test/tool/bash.test.ts
@@ -230,6 +230,49 @@ 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> = []
+ const testCtx = {
+ ...ctx,
+ ask: async (req: Omit) => {
+ 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> = []
+ const testCtx = {
+ ...ctx,
+ ask: async (req: Omit) => {
+ 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 *")
+ },
+ })
+ })
})
describe("tool.bash truncation", () => {