Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 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
46db975
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 30, 2025
93fae5b
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 30, 2025
8ce0254
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 30, 2025
1bb5b29
Merge branch 'dev' into feat/shell-advice
ariane-emory Dec 31, 2025
12609cc
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 1, 2026
f7a2e18
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 1, 2026
d310d56
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 1, 2026
d993548
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 2, 2026
57970db
Fix missing path import in bash.ts after merge
ariane-emory Jan 2, 2026
6cae64a
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 3, 2026
efe7716
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 3, 2026
82fd0ea
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 4, 2026
294a9f4
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 4, 2026
0fe47a4
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 4, 2026
0994f64
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 5, 2026
e8eac39
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 5, 2026
fb39a83
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 5, 2026
1a7efbf
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 6, 2026
0558cf9
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 6, 2026
5e1a6b1
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 6, 2026
2bb295f
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 6, 2026
50f4dec
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 6, 2026
d2650a9
Merge remote-tracking branch 'origin/dev' into feat/shell-advice
ariane-emory Jan 7, 2026
560ff18
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 7, 2026
77df498
Merge remote-tracking branch 'origin/dev' into feat/shell-advice
ariane-emory Jan 7, 2026
c3f2a20
Merge dev into feat/shell-advice
ariane-emory Jan 7, 2026
a3ddb42
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 8, 2026
cd88c33
Merge branch 'dev' into feat/shell-advice
ariane-emory Jan 9, 2026
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()
})
}
53 changes: 50 additions & 3 deletions packages/opencode/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { Log } from "../util/log"
import { Instance } from "../project/instance"
import { lazy } from "@/util/lazy"
import { Language } from "web-tree-sitter"
import { basename } from "path"

import { $ } from "bun"
import { Filesystem } from "@/util/filesystem"
import { fileURLToPath } from "url"
import { Flag } from "@/flag/flag.ts"
import { Shell } from "@/shell/shell"
import { iife } from "@/util/iife"

import { BashArity } from "@/permission/arity"
import { Truncate } from "./truncation"
Expand Down Expand Up @@ -52,11 +54,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 = 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: description
.replaceAll("${maxLines}", String(Truncate.MAX_LINES))
.replaceAll("${maxBytes}", String(Truncate.MAX_BYTES)),
parameters: z.object({
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 @@ -63,7 +63,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 @@ -91,7 +91,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 @@ -36,6 +36,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