Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
84a4102
feat: attemp to inject shell advice in bash tool description, needs t…
ariane-emory Nov 28, 2025
cac1c5e
fix: safer windows corner case
ariane-emory Nov 28, 2025
e444456
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice-ta…
ariane-emory Nov 29, 2025
2d2adce
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice-ta…
ariane-emory Nov 29, 2025
daa6d17
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice-ta…
ariane-emory Nov 29, 2025
21a3816
...
ariane-emory Nov 30, 2025
a7f6cc9
fix: revise Bash tool test to account for the changes.
ariane-emory Nov 30, 2025
083a2ce
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Nov 30, 2025
7511cf2
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Nov 30, 2025
ac56976
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Nov 30, 2025
ebcedac
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 1, 2025
92c5e69
chore: format code
actions-user Dec 1, 2025
4c93e68
Update Nix flake.lock and hashes
actions-user Dec 1, 2025
f1de95e
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 1, 2025
5630a29
Merge upstream/dev into feat/shell-advice
ariane-emory Dec 1, 2025
880db56
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 2, 2025
281fa16
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 2, 2025
23a96fb
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 2, 2025
6e56b0d
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 2, 2025
33645e9
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 3, 2025
9920506
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 3, 2025
053ae51
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 3, 2025
04948f8
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 3, 2025
e1112ce
Merge remote-tracking branch 'upstream/dev' into feat/shell-advice
ariane-emory Dec 4, 2025
fa3132a
Merge dev branch preserving shell-specific Bash tool description feature
ariane-emory Dec 12, 2025
69b28ac
Merge branch 'dev' into wip/feat/shell-advice
ariane-emory Dec 12, 2025
91fc9cd
Merge dev branch into repair/feat/shell-advice
ariane-emory Dec 13, 2025
6753987
feat: enable fish and nu shell support in Bash tool
ariane-emory Dec 13, 2025
0ece1f7
tidy: remove TODO.
ariane-emory Dec 13, 2025
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
7 changes: 0 additions & 7 deletions packages/opencode/src/shell/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export namespace Shell {
}
}
}
const BLACKLIST = new Set(["fish", "nu"])

function fallback() {
if (process.platform === "win32") {
Expand All @@ -58,10 +57,4 @@ export namespace Shell {
if (s) return s
return fallback()
})

export const acceptable = lazy(() => {
const s = process.env.SHELL
if (s && !BLACKLIST.has(process.platform === "win32" ? path.win32.basename(s) : path.basename(s))) return s
return fallback()
})
}
52 changes: 49 additions & 3 deletions packages/opencode/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { fileURLToPath } from "url"
import { Flag } from "@/flag/flag.ts"
import path from "path"
import { Shell } from "@/shell/shell"
import { iife } from "@/util/iife"

const MAX_OUTPUT_LENGTH = Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30_000
const DEFAULT_TIMEOUT = Flag.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
Expand Down Expand Up @@ -52,11 +53,56 @@ const parser = lazy(async () => {
// TODO: we may wanna rename this tool so it works better on other shells

export const BashTool = Tool.define("bash", async () => {
const shell = Shell.acceptable()
log.info("bash tool using shell", { shell })
const shell = (() => {
const s = process.env.SHELL
if (s) return s

if (process.platform === "darwin") {
return "/bin/zsh"
}

if (process.platform === "win32") {
return process.env.COMSPEC || true
}

const bash = Bun.which("bash")
if (bash) return bash

return true
})()

const shellName = (() => {
if (typeof shell === "boolean") {
// When shell is true (fallback), assume appropriate default for platform
return process.platform === "win32" ? "cmd" : "bash"
}
if (typeof shell === "string") {
let name = path.basename(shell)
// Handle Windows paths (both forward and back slashes)
if (shell.includes("\\") || shell.includes("/")) {
// Extract the last part after both types of separators
const parts = shell.split(/[\\/]/)
name = parts[parts.length - 1]
}
// Handle Windows executables
if (name.toLowerCase().endsWith(".exe")) {
return name.slice(0, -4)
}
return name
}
return "bash"
})()

log.info("bash tool using shell", { shell, shellName })

const description = `**Shell**: You are executing commands in \`${shellName}\`. Ensure your command syntax is compatible with this shell.

${DESCRIPTION.replace(/\$\{shellName\} command/g, `${shellName} command`)
.replace(/\$\{shellName\} commands/g, `${shellName} commands`)
.replaceAll("${directory}", Instance.directory)}`

return {
description: DESCRIPTION.replaceAll("${directory}", Instance.directory),
description,
parameters: z.object({
command: z.string().describe("The command to execute"),
timeout: z.number().describe("Optional timeout in milliseconds").optional(),
Expand Down
6 changes: 3 additions & 3 deletions packages/opencode/src/tool/bash.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
Executes a given ${shellName} command in a persistent shell session with optional timeout, ensuring proper handling and security measures.

All commands run in ${directory} by default. Use the `workdir` parameter if you need to run a command in a different directory.

Expand Down Expand Up @@ -81,7 +81,7 @@ IMPORTANT: ONLY COMMIT IF THE USER ASKS YOU TO.

If and only if the user asks you to create a new git commit, follow these steps carefully:

1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following bash commands in parallel, each using the Bash tool:
1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following ${shellName} commands in parallel, each using the Bash tool:
- Run a git status command to see all untracked files.
- Run a git diff command to see both staged and unstaged changes that will be committed.
- Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
Expand Down Expand Up @@ -120,7 +120,7 @@ Use the gh command via the Bash tool for ALL GitHub-related tasks including work

IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:

1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following bash commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch:
1. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following ${shellName} commands in parallel using the Bash tool, in order to understand the current state of the branch since it diverged from the main branch:
- Run a git status command to see all untracked files
- Run a git diff command to see both staged and unstaged changes that will be committed
- Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
Expand Down
109 changes: 109 additions & 0 deletions packages/opencode/test/tool/bash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,115 @@ describe("tool.bash", () => {
},
})
})

test("description includes shell information", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const bash = await BashTool.init()
expect(bash.description).toContain("**Shell**:")
expect(bash.description).toContain("Ensure your command syntax is compatible with this shell")
// Should contain a shell name (bash, zsh, fish, etc.)
const shellMatch = bash.description.match(/You are executing commands in `([^`]+)`/)
expect(shellMatch).toBeTruthy()
expect(shellMatch?.[1]).toBeTruthy()
},
})
})

test("shell name detection is platform-aware", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const bash = await BashTool.init()
const shellMatch = bash.description.match(/You are executing commands in `([^`]+)`/)
const detectedShell = shellMatch?.[1]

expect(detectedShell).toBeTruthy()

// Verify detected shell is appropriate for the platform
if (process.platform === "win32") {
expect(["cmd", "powershell"]).toContain(detectedShell!)
} else {
expect(["bash", "zsh", "fish", "ksh", "csh", "tcsh", "dash"]).toContain(detectedShell!)
}
},
})
})

test("description uses dynamic shell-specific language", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const bash = await BashTool.init()
const shellMatch = bash.description.match(/You are executing commands in `([^`]+)`/)
const detectedShell = shellMatch?.[1]

expect(detectedShell).toBeTruthy()

// Should contain shell-specific command references
if (detectedShell) {
expect(bash.description).toContain(`${detectedShell} command`)
expect(bash.description).toContain(`${detectedShell} commands`)
}

// Should NOT contain references to other shells (shell-agnostic validation)
const commonShells = ["bash", "zsh", "fish", "ksh", "csh", "tcsh", "dash", "cmd", "powershell"]
for (const otherShell of commonShells) {
if (otherShell !== detectedShell) {
expect(bash.description).not.toContain(`${otherShell} command`)
expect(bash.description).not.toContain(`${otherShell} commands`)
}
}

// Should still contain "Bash tool" references (tool name)
expect(bash.description).toContain("Bash tool")
},
})
})

test("shell-specific language works for different shell types", async () => {
// Test with different shell environments
const originalShell = process.env.SHELL

await Instance.provide({
directory: projectRoot,
fn: async () => {
try {
// Mock zsh shell environment
process.env.SHELL = "/bin/zsh"
const bashZsh = await BashTool.init()
expect(bashZsh.description).toContain("zsh command")
expect(bashZsh.description).toContain("zsh commands")

// Mock bash shell environment
process.env.SHELL = "/bin/bash"
const bashBash = await BashTool.init()
expect(bashBash.description).toContain("bash command")
expect(bashBash.description).toContain("bash commands")

// Mock ksh shell environment
process.env.SHELL = "/bin/ksh"
const bashKsh = await BashTool.init()
expect(bashKsh.description).toContain("ksh command")
expect(bashKsh.description).toContain("ksh commands")

// Mock fish shell environment (fish is now supported, not blacklisted)
process.env.SHELL = "/usr/bin/fish"
const bashFish = await BashTool.init()
expect(bashFish.description).toContain("fish command")
expect(bashFish.description).toContain("fish commands")
} finally {
// Restore original shell
if (originalShell) {
process.env.SHELL = originalShell
} else {
delete process.env.SHELL
}
}
},
})
})
})

describe("tool.bash permissions", () => {
Expand Down