diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 39d3684eca..bfc07a0b37 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -90,11 +90,7 @@ export const BashTool = Tool.define("bash", async () => { parameters: z.object({ command: z.string().describe("The command to execute"), timeout: z.number().describe("Optional timeout in milliseconds").optional(), - workdir: z - .string() - .default(Instance.directory) - .describe("The working directory to execute the command in") - .optional(), + workdir: z.string().default(Instance.directory).describe("The working directory to execute the command in"), description: z .string() .describe( @@ -102,6 +98,7 @@ export const BashTool = Tool.define("bash", async () => { ), }), async execute(params, ctx) { + const cwd = params.workdir || Instance.directory if (params.timeout !== undefined && params.timeout < 0) { throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`) } @@ -111,6 +108,37 @@ export const BashTool = Tool.define("bash", async () => { throw new Error("Failed to parse command") } const agent = await Agent.get(ctx.agent) + + const checkExternalDirectory = async (dir: string) => { + if (Filesystem.contains(Instance.directory, dir)) return + const title = `This command references paths outside of ${Instance.directory}` + if (agent.permission.external_directory === "ask") { + await Permission.ask({ + type: "external_directory", + pattern: [dir, path.join(dir, "*")], + sessionID: ctx.sessionID, + messageID: ctx.messageID, + callID: ctx.callID, + title, + metadata: { + command: params.command, + }, + }) + } else if (agent.permission.external_directory === "deny") { + throw new Permission.RejectedError( + ctx.sessionID, + "external_directory", + ctx.callID, + { + command: params.command, + }, + `${title} so this command is not allowed to be executed.`, + ) + } + } + + await checkExternalDirectory(cwd) + const permissions = agent.permission.bash const askPatterns = new Set() @@ -137,6 +165,7 @@ export const BashTool = Tool.define("bash", async () => { for (const arg of command.slice(1)) { if (arg.startsWith("-") || (command[0] === "chmod" && arg.startsWith("+"))) continue const resolved = await $`realpath ${arg}` + .cwd(cwd) .quiet() .nothrow() .text() @@ -149,32 +178,7 @@ export const BashTool = Tool.define("bash", async () => { ? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\") : resolved - if (!Filesystem.contains(Instance.directory, normalized)) { - const parentDir = path.dirname(normalized) - if (agent.permission.external_directory === "ask") { - await Permission.ask({ - type: "external_directory", - pattern: [parentDir, path.join(parentDir, "*")], - sessionID: ctx.sessionID, - messageID: ctx.messageID, - callID: ctx.callID, - title: `This command references paths outside of ${Instance.directory}`, - metadata: { - command: params.command, - }, - }) - } else if (agent.permission.external_directory === "deny") { - throw new Permission.RejectedError( - ctx.sessionID, - "external_directory", - ctx.callID, - { - command: params.command, - }, - `This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`, - ) - } - } + await checkExternalDirectory(path.dirname(normalized)) } } } @@ -220,7 +224,7 @@ export const BashTool = Tool.define("bash", async () => { const proc = spawn(params.command, { shell, - cwd: params.workdir || Instance.directory, + cwd, env: { ...process.env, }, diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index 6caafa0706..1989074b71 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -19,12 +19,16 @@ 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). + - The `workdir` parameter specifies the working directory for the command. Defaults to the current working directory. Prefer setting `workdir` over using `cd` in your commands. - 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. + - Avoid using `cd` to change directories when possible. Instead, set the `workdir` parameter or use absolute paths in your commands. + + workdir="/foo/bar", command="pytest tests" + pytest /foo/bar/tests