Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 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
586ea9d
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 21, 2025
ddb8e84
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 22, 2025
cfd3b58
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 23, 2025
c3f0fbe
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 23, 2025
83ffc95
Merge branch 'feat/shell-advice' of github.com:ariane-emory/opencode …
ariane-emory Dec 23, 2025
2980f51
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 23, 2025
97fbf36
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 23, 2025
21dac83
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 23, 2025
3425ccf
Merge remote-tracking branch 'origin/dev' into feat/shell-advice
ariane-emory Dec 23, 2025
58ea13f
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 24, 2025
e982979
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 24, 2025
0ebb0d2
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 24, 2025
7f4f801
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 24, 2025
3e17386
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 25, 2025
0926809
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 25, 2025
c94d8dd
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 25, 2025
ffb5758
Merge dev into feat/shell-advice, resolving conflicts
ariane-emory Dec 26, 2025
d1060f8
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 27, 2025
a801149
Merge dev into feat/shell-advice
ariane-emory Dec 27, 2025
b5a72ad
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 27, 2025
6587035
fix: minimize bash.txt changes to only essential shell name substitut…
ariane-emory Dec 28, 2025
08ab0e6
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 28, 2025
206a22b
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 28, 2025
b9b21ce
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 28, 2025
9cd56a9
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 29, 2025
f9e5af6
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 29, 2025
4f8935f
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 29, 2025
db1f61f
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 29, 2025
f4ce6f3
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 30, 2025
cecb983
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 30, 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 @@ -51,11 +52,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. AVOID using `cd <directory> && <command>` patterns - use `workdir` instead.

Expand Down Expand Up @@ -64,7 +64,7 @@ Git Safety Protocol:
- CRITICAL: If you already pushed to remote, NEVER amend unless user explicitly requests it (requires force push)
- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.

1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel, each using the Bash tool:
1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. 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 @@ -92,7 +92,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 can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. 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 can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. 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
100 changes: 100 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,106 @@ 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 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