From 5059050989b44ebb5d4f38099ee4ec6ca7d691d7 Mon Sep 17 00:00:00 2001 From: Error Date: Thu, 30 Oct 2025 13:13:51 -0500 Subject: [PATCH 1/5] core: add cwd parameter to bash tool for better directory control Users can now specify a working directory for bash commands using the cwd parameter, allowing commands to run in specific subdirectories without needing cd commands. This provides more precise control over where commands execute while maintaining security by restricting access to within the project directory. --- packages/opencode/src/tool/bash.ts | 26 ++++++++- packages/opencode/src/tool/bash.txt | 6 +- packages/opencode/test/tool/bash.test.ts | 71 ++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 2c377ee14c8..a1489737221 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -50,6 +50,7 @@ export const BashTool = Tool.define("bash", { parameters: z.object({ command: z.string().describe("The command to execute"), timeout: z.number().describe("Optional timeout in milliseconds").optional(), + cwd: z.string().describe("The working directory for the command. Must be within the project directory. If not specified, uses the project root directory.").optional(), description: z .string() .describe( @@ -63,6 +64,29 @@ export const BashTool = Tool.define("bash", { ) } const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) + + // Validate and resolve cwd parameter + let workingDirectory = Instance.directory + if (params.cwd) { + const resolvedCwd = await $`realpath ${params.cwd}` + .quiet() + .nothrow() + .text() + .then((x) => x.trim()) + + if (!resolvedCwd) { + throw new Error(`Invalid working directory: ${params.cwd}`) + } + + if (!Filesystem.contains(Instance.directory, resolvedCwd)) { + throw new Error( + `Working directory ${resolvedCwd} is outside of project directory ${Instance.directory}`, + ) + } + + workingDirectory = resolvedCwd + } + const tree = await parser().then((p) => p.parse(params.command)) const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash) @@ -158,7 +182,7 @@ export const BashTool = Tool.define("bash", { const proc = spawn(params.command, { shell: true, - cwd: Instance.directory, + cwd: workingDirectory, stdio: ["ignore", "pipe", "pipe"], detached: process.platform !== "win32", }) diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index 67c54677fce..171cb2764c8 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -19,15 +19,19 @@ Before executing the command, please follow these steps: Usage notes: - The command argument is required. - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes). + - You can specify an optional working directory with the cwd parameter. The directory must be within the project directory. If not specified, uses the project root directory. - It is very helpful if you write a clear, concise description of what this command does in 5-10 words. - If the output exceeds 30000 characters, output will be truncated before being returned to you. - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and List to read files. - If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed. - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings). - - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it. + - Try to maintain your current working directory throughout the session by using absolute paths, the cwd parameter, and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it. pytest /foo/bar/tests + + Run command in specific directory: command="npm test" cwd="/foo/bar" + cd /foo/bar && pytest tests diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 2919ccb0245..9524f5c9339 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -49,4 +49,75 @@ describe("tool.bash", () => { }, }) }) + + test("cwd parameter with valid directory", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const result = await bash.execute( + { + command: "pwd", + cwd: projectRoot, + description: "Get current working directory", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + expect(result.metadata.output).toContain(projectRoot) + }, + }) + }) + + test("cwd parameter outside project should fail", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + expect( + bash.execute( + { + command: "pwd", + cwd: "/tmp", + description: "Try to use cwd outside project", + }, + ctx, + ), + ).rejects.toThrow("Working directory") + }, + }) + }) + + test("cwd parameter with relative path", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const result = await bash.execute( + { + command: "pwd", + cwd: path.join(projectRoot, "src"), + description: "Use absolute path for cwd", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + expect(result.metadata.output).toContain("/src") + }, + }) + }) + + test("default behavior without cwd parameter", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const result = await bash.execute( + { + command: "pwd", + description: "Get default working directory", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + expect(result.metadata.output).toContain(projectRoot) + }, + }) + }) }) From b2063428a2a1a6a8f5fcab5104fc86f9790930f2 Mon Sep 17 00:00:00 2001 From: Aaron Beavers Date: Thu, 30 Oct 2025 13:52:35 -0500 Subject: [PATCH 2/5] docs --- docs/notes/2025.10.30.13.09.02.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/notes/2025.10.30.13.09.02.md diff --git a/docs/notes/2025.10.30.13.09.02.md b/docs/notes/2025.10.30.13.09.02.md new file mode 100644 index 00000000000..9ec6d13979f --- /dev/null +++ b/docs/notes/2025.10.30.13.09.02.md @@ -0,0 +1,5 @@ +[4:08 PM]AKTK: And now I've just started getting this too, did you find a cause/solution at all? +[4:10 PM]shuv: what was the prompt/session that triggered it? based on the token counts, it looks like it was trying to print the entire previous context back to you again +[4:13 PM]AKTK: Sent out via a subagent. It was running fine for a while, then just randomly goes 💩 and errors. Which then means it delegates the task back out and leaves the repo in an awful state (That /undo didn't fix the first time) +Image +Image From df886e35c1b75c2c97f03628d8e85c703bd89ec7 Mon Sep 17 00:00:00 2001 From: Err Date: Thu, 30 Oct 2025 15:59:38 -0500 Subject: [PATCH 3/5] fix: resolve bash tool paths relative to cwd --- packages/opencode/src/tool/bash.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index a1489737221..8b651ce6812 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -69,6 +69,7 @@ export const BashTool = Tool.define("bash", { let workingDirectory = Instance.directory if (params.cwd) { const resolvedCwd = await $`realpath ${params.cwd}` + .cwd(Instance.directory) .quiet() .nothrow() .text() @@ -113,6 +114,7 @@ export const BashTool = Tool.define("bash", { for (const arg of command.slice(1)) { if (arg.startsWith("-") || (command[0] === "chmod" && arg.startsWith("+"))) continue const resolved = await $`realpath ${arg}` + .cwd(workingDirectory) .quiet() .nothrow() .text() From 258df6c5d6b737f6f705d04bad1bd9904292b7b7 Mon Sep 17 00:00:00 2001 From: Aaron Beavers Date: Mon, 3 Nov 2025 12:39:49 -0600 Subject: [PATCH 4/5] address code review --- packages/opencode/src/tool/bash.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index 171cb2764c8..4e36432e5fb 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -25,7 +25,7 @@ Usage notes: - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and List to read files. - If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed. - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings). - - Try to maintain your current working directory throughout the session by using absolute paths, the cwd parameter, and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it. + - Try to maintain your current working directory throughout the session by using absolute paths, the cwd parameter, and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it. pytest /foo/bar/tests From a9b45486903367033b086c9452c910c6d137b87b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 9 Nov 2025 02:16:11 +0000 Subject: [PATCH 5/5] chore: format code --- docs/notes/2025.10.30.13.09.02.md | 2 +- packages/opencode/src/tool/bash.ts | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/notes/2025.10.30.13.09.02.md b/docs/notes/2025.10.30.13.09.02.md index 9ec6d13979f..88d9bcfc113 100644 --- a/docs/notes/2025.10.30.13.09.02.md +++ b/docs/notes/2025.10.30.13.09.02.md @@ -1,5 +1,5 @@ [4:08 PM]AKTK: And now I've just started getting this too, did you find a cause/solution at all? [4:10 PM]shuv: what was the prompt/session that triggered it? based on the token counts, it looks like it was trying to print the entire previous context back to you again -[4:13 PM]AKTK: Sent out via a subagent. It was running fine for a while, then just randomly goes 💩 and errors. Which then means it delegates the task back out and leaves the repo in an awful state (That /undo didn't fix the first time) +[4:13 PM]AKTK: Sent out via a subagent. It was running fine for a while, then just randomly goes 💩 and errors. Which then means it delegates the task back out and leaves the repo in an awful state (That /undo didn't fix the first time) Image Image diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 855c6e698ee..734089ddb6a 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -43,7 +43,12 @@ export const BashTool = Tool.define("bash", { parameters: z.object({ command: z.string().describe("The command to execute"), timeout: z.number().describe("Optional timeout in milliseconds").optional(), - cwd: z.string().describe("The working directory for the command. Must be within the project directory. If not specified, uses the project root directory.").optional(), + cwd: z + .string() + .describe( + "The working directory for the command. Must be within the project directory. If not specified, uses the project root directory.", + ) + .optional(), description: z .string() .describe( @@ -55,7 +60,7 @@ export const BashTool = Tool.define("bash", { throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`) } const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT) - + // Validate and resolve cwd parameter let workingDirectory = Instance.directory if (params.cwd) { @@ -65,20 +70,18 @@ export const BashTool = Tool.define("bash", { .nothrow() .text() .then((x) => x.trim()) - + if (!resolvedCwd) { throw new Error(`Invalid working directory: ${params.cwd}`) } - + if (!Filesystem.contains(Instance.directory, resolvedCwd)) { - throw new Error( - `Working directory ${resolvedCwd} is outside of project directory ${Instance.directory}`, - ) + throw new Error(`Working directory ${resolvedCwd} is outside of project directory ${Instance.directory}`) } - + workingDirectory = resolvedCwd } - + const tree = await parser().then((p) => p.parse(params.command)) if (!tree) { throw new Error("Failed to parse command")