Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/notes/2025.10.30.13.09.02.md
Original file line number Diff line number Diff line change
@@ -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
31 changes: 30 additions & 1 deletion packages/opencode/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +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(),
description: z
.string()
.describe(
Expand All @@ -54,6 +60,28 @@ 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) {
const resolvedCwd = await $`realpath ${params.cwd}`
.cwd(Instance.directory)
.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))
if (!tree) {
throw new Error("Failed to parse command")
Expand Down Expand Up @@ -84,6 +112,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()
Expand Down Expand Up @@ -138,7 +167,7 @@ export const BashTool = Tool.define("bash", {

const proc = spawn(params.command, {
shell: true,
cwd: Instance.directory,
cwd: workingDirectory,
env: {
...process.env,
},
Expand Down
6 changes: 5 additions & 1 deletion packages/opencode/src/tool/bash.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<good-example>
pytest /foo/bar/tests
</good-example>
<good-example>
Run command in specific directory: command="npm test" cwd="/foo/bar"
</good-example>
<bad-example>
cd /foo/bar && pytest tests
</bad-example>
Expand Down
71 changes: 71 additions & 0 deletions packages/opencode/test/tool/bash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
})
})
})