From 55387b0678f842487a5c7635385566ba5d308e94 Mon Sep 17 00:00:00 2001 From: ASidorenkoCode Date: Fri, 13 Feb 2026 02:59:47 +0100 Subject: [PATCH] fix: prevent undeletable `nul` file on Windows when using Git Bash On Windows, OpenCode uses Git Bash as the shell. When the AI model generates Windows-style `> nul` or `2>nul` redirection, Git Bash interprets this literally and creates a file named `nul` instead of routing to the null device. Since `nul` is a reserved Windows device name, the file cannot be deleted through normal Windows file operations. This adds a `sanitizeNullRedirect()` helper that rewrites null-device redirections to match the actual shell: `>nul` becomes `>/dev/null` for Git Bash, and vice versa for cmd.exe/PowerShell. Applied in both the bash tool and the shell execution path. Also adds guidance to the bash tool description so models prefer `/dev/null`. Co-Authored-By: Claude Opus 4.6 --- packages/opencode/src/session/prompt.ts | 18 ++++++++++-------- packages/opencode/src/shell/shell.ts | 25 +++++++++++++++++++++++++ packages/opencode/src/tool/bash.ts | 3 ++- packages/opencode/src/tool/bash.txt | 1 + 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 99d44cd850f..d061de40a8a 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -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 = { nu: { - args: ["-c", input.command], + args: ["-c", sanitizedCommand], }, fish: { - args: ["-c", input.command], + args: ["-c", sanitizedCommand], }, zsh: { args: [ @@ -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)} `, ], }, @@ -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}`], }, } diff --git a/packages/opencode/src/shell/shell.ts b/packages/opencode/src/shell/shell.ts index 2e8d48bfd92..c9c4a2cfe46 100644 --- a/packages/opencode/src/shell/shell.ts +++ b/packages/opencode/src/shell/shell.ts @@ -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") + } } diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 67559b78c08..b1e824204c0 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -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: { diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index 47e9378e755..ed097f4210a 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -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 && `. Use the `workdir` parameter to change directories instead. Use workdir="/foo/bar" with command: pytest tests