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
18 changes: 10 additions & 8 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1574,12 +1574,14 @@ NOTE: At any point in time through this workflow you should feel free to ask the
process.platform === "win32" ? path.win32.basename(shell, ".exe") : path.basename(shell)
).toLowerCase()

const sanitizedCommand = Shell.sanitizeNullRedirect(input.command, shell)

const invocations: Record<string, { args: string[] }> = {
nu: {
args: ["-c", input.command],
args: ["-c", sanitizedCommand],
},
fish: {
args: ["-c", input.command],
args: ["-c", sanitizedCommand],
},
zsh: {
args: [
Expand All @@ -1588,7 +1590,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
`
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
eval ${JSON.stringify(input.command)}
eval ${JSON.stringify(sanitizedCommand)}
`,
],
},
Expand All @@ -1599,25 +1601,25 @@ NOTE: At any point in time through this workflow you should feel free to ask the
`
shopt -s expand_aliases
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
eval ${JSON.stringify(input.command)}
eval ${JSON.stringify(sanitizedCommand)}
`,
],
},
// Windows cmd
cmd: {
args: ["/c", input.command],
args: ["/c", sanitizedCommand],
},
// Windows PowerShell
powershell: {
args: ["-NoProfile", "-Command", input.command],
args: ["-NoProfile", "-Command", sanitizedCommand],
},
pwsh: {
args: ["-NoProfile", "-Command", input.command],
args: ["-NoProfile", "-Command", sanitizedCommand],
},
// Fallback: any shell that doesn't match those above
// - No -l, for max compatibility
"": {
args: ["-c", `${input.command}`],
args: ["-c", `${sanitizedCommand}`],
},
}

Expand Down
25 changes: 25 additions & 0 deletions packages/opencode/src/shell/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,29 @@ export namespace Shell {
if (s && !BLACKLIST.has(process.platform === "win32" ? path.win32.basename(s) : path.basename(s))) return s
return fallback()
})

const UNIX_SHELLS = new Set(["bash", "sh", "zsh", "fish", "nu"])

export function isUnixLike(shell: string): boolean {
const base = path.basename(shell).toLowerCase().replace(/\.exe$/, "")
return UNIX_SHELLS.has(base)
}

/**
* On Windows, when using Git Bash the AI model may generate Windows-style
* `> nul` / `2>nul` redirections. Git Bash interprets these literally and
* creates a file called "nul" instead of discarding output.
*
* This helper rewrites null-device redirections so they match the shell
* that will actually execute the command.
*/
export function sanitizeNullRedirect(command: string, shell: string): string {
if (process.platform !== "win32") return command
if (isUnixLike(shell)) {
// Git Bash / Unix shell: replace Windows NUL with /dev/null
return command.replace(/(\d?>)\s*(?:nul|NUL)\b/g, "$1/dev/null")
}
// cmd.exe / PowerShell: replace /dev/null with NUL
return command.replace(/(\d?>)\s*\/dev\/null\b/g, "$1NUL")
}
}
3 changes: 2 additions & 1 deletion packages/opencode/src/tool/bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ export const BashTool = Tool.define("bash", async () => {
}

const shellEnv = await Plugin.trigger("shell.env", { cwd }, { env: {} })
const proc = spawn(params.command, {
const command = Shell.sanitizeNullRedirect(params.command, shell)
const proc = spawn(command, {
shell,
cwd,
env: {
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/tool/bash.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Usage notes:
- If the commands depend on each other and must run sequentially, use a single Bash call with '&&' to chain them together (e.g., `git add . && git commit -m "message" && git push`). For instance, if one operation must complete before another starts (like mkdir before cp, Write before Bash for git operations, or git add before git commit), run these operations sequentially instead.
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
- DO NOT use newlines to separate commands (newlines are ok in quoted strings)
- IMPORTANT: On Windows, the bash tool uses Git Bash. Always use /dev/null for output redirection (e.g., `command 2>/dev/null`), NOT the Windows `nul` device. Using `> nul` or `2>nul` will create a literal file named "nul" instead of discarding output.
- AVOID using `cd <directory> && <command>`. Use the `workdir` parameter to change directories instead.
<good-example>
Use workdir="/foo/bar" with command: pytest tests
Expand Down
Loading