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", () => {