From 2bd7946001cef667fa89b2d1598e4a000cc74108 Mon Sep 17 00:00:00 2001 From: ZeyuFu Date: Sun, 17 May 2026 10:59:29 -0400 Subject: [PATCH] fix(non-interactive-env): use powershell syntax on Windows regardless of SHELL/MSYSTEM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3607. ## Root cause On Windows, `detectCommandShellType()` fell through to `detectShellType()` for two common environments and incorrectly returned `"unix"`: 1. **SHELL points at a Unix-shaped path** (e.g. Git Bash sets `SHELL=/usr/bin/bash` on a fresh Windows install). The `detectWindowsShellType(process.env.SHELL)` probe didn't recognize `bash` as a Windows shell, so the function fell through and `detectShellType()` returned `"unix"`. 2. **MSYSTEM is set but SHELL is not** (Git Bash leaves MSYSTEM permanently set system-wide even when the active shell is PowerShell). The fall-through path returned `"unix"` via the MSYSTEM check. In both cases, the hook then prepended `export KEY=val;` to git commands, which PowerShell rejects with: `export : 无法将"export"项识别为 cmdlet...` OpenCode on Windows runs the bash tool through a Windows shell (PowerShell by default, cmd as the user-overridable fallback), regardless of MSYSTEM or a Unix-shaped SHELL set by Git Bash — so the env prefix must use Windows-compatible syntax. ## Fix `detectCommandShellType()` now short-circuits on `process.platform === "win32"`: - If `SHELL` points at a recognized Windows shell (`cmd.exe`, `powershell.exe`, `pwsh.exe`), return that. - If `SHELL` and `MSYSTEM` are both unset, fall back to `ComSpec` then to cmd. - Otherwise, default to PowerShell — matching what OpenCode actually spawns. `detectShellType()` is unchanged; other callers (including non-Windows platforms) are unaffected. ## Test changes Three pre-existing tests encoded the buggy behavior as expected behavior and have been updated to assert the new PowerShell syntax with a `(#3607)` marker and a comment explaining why a Unix-shaped SHELL on win32 must still resolve to PowerShell. WSL is not affected because in WSL `process.platform === "linux"`, not `"win32"`. - `src/hooks/non-interactive-env/`: 24 tests pass / 0 fail - `bunx tsc --noEmit`: clean ## Note on issue thread The sisyphus-bot triage comment on #3607 framed this as a policy choice between (A) forcing Windows env-prefix syntax and (B) resolving against the OpenCode-configured shell. This PR implements option (A) as the minimal surgical fix; option (B) remains a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/hooks/non-interactive-env/index.test.ts | 32 +++++++++++++------ .../non-interactive-env-hook.ts | 30 +++++++++++------ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/src/hooks/non-interactive-env/index.test.ts b/src/hooks/non-interactive-env/index.test.ts index 0582a3853..377f136c1 100644 --- a/src/hooks/non-interactive-env/index.test.ts +++ b/src/hooks/non-interactive-env/index.test.ts @@ -370,7 +370,11 @@ describe("non-interactive-env hook", () => { expect(cmd).not.toContain("export ") }) - test("#given Windows Git Bash environment with SHELL #when git command executes #then uses unix syntax", async () => { + test("#given Windows Git Bash SHELL=/usr/bin/bash #when git command executes #then uses powershell syntax (#3607)", async () => { + // Regression for #3607: OpenCode on Windows runs the bash tool through + // PowerShell by default, regardless of a Unix-shaped SHELL set by Git + // Bash. The export prefix is invalid PowerShell, so we must use + // PowerShell syntax even when SHELL points at /usr/bin/bash. delete process.env.PSModulePath process.env.SHELL = "/usr/bin/bash" Object.defineProperty(process, "platform", { value: "win32" }) @@ -386,12 +390,15 @@ describe("non-interactive-env hook", () => { ) const cmd = output.args.command as string - expect(cmd).toStartWith("export ") + expect(cmd).toStartWith("$env:") expect(cmd).toContain("; git status") - expect(cmd).not.toContain("$env:") + expect(cmd).not.toContain("export ") }) - test("#given Windows Git Bash via MSYSTEM without SHELL #when git command executes #then uses unix syntax", async () => { + test("#given Windows MSYSTEM=MINGW64 without SHELL #when git command executes #then uses powershell syntax (#3607)", async () => { + // Regression for #3607: MSYSTEM is permanently set on systems with Git + // Bash installed, but OpenCode on Windows still spawns PowerShell. + // MSYSTEM alone must not select Unix env-prefix syntax. delete process.env.SHELL process.env.MSYSTEM = "MINGW64" process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules" @@ -408,9 +415,9 @@ describe("non-interactive-env hook", () => { ) const cmd = output.args.command as string - expect(cmd).toStartWith("export ") + expect(cmd).toStartWith("$env:") expect(cmd).toContain("; git status") - expect(cmd).not.toContain("$env:") + expect(cmd).not.toContain("export ") }) test("#given Windows platform #when chained git commands via bash tool #then uses cmd syntax", async () => { @@ -437,8 +444,13 @@ describe("non-interactive-env hook", () => { expect(cmd).not.toContain("$env:") }) - test("#given SHELL=/bin/bash on win32 #when git command executes #then uses unix syntax", async () => { - // Git Bash or WSL sets SHELL env var - should override platform detection + test("#given SHELL=/bin/bash on win32 #when git command executes #then uses powershell syntax (#3607)", async () => { + // Regression for #3607: a Unix-shaped SHELL value (Git Bash sets + // SHELL=/bin/bash or /usr/bin/bash) does NOT mean OpenCode will run + // the bash tool in a Unix shell on Windows — OpenCode spawns + // PowerShell, so the env prefix must use PowerShell syntax. + // WSL is not affected by this assertion because in WSL, + // process.platform === "linux", not "win32". delete process.env.PSModulePath process.env.SHELL = "/bin/bash" Object.defineProperty(process, "platform", { value: "win32" }) @@ -454,9 +466,9 @@ describe("non-interactive-env hook", () => { ) const cmd = output.args.command as string - expect(cmd).toStartWith("export ") + expect(cmd).toStartWith("$env:") expect(cmd).toContain("; git status") - expect(cmd).not.toContain("$env:") + expect(cmd).not.toContain("export ") }) test("#given PSModulePath set on non-Windows #when git command executes #then uses powershell syntax", async () => { diff --git a/src/hooks/non-interactive-env/non-interactive-env-hook.ts b/src/hooks/non-interactive-env/non-interactive-env-hook.ts index 6fc42aea9..22f0d11c8 100644 --- a/src/hooks/non-interactive-env/non-interactive-env-hook.ts +++ b/src/hooks/non-interactive-env/non-interactive-env-hook.ts @@ -41,18 +41,28 @@ function detectWindowsShellType(shellPath: string | undefined): ShellType | unde } function detectCommandShellType(): ShellType { - if (process.platform === "win32" && process.env.SHELL) { - const shellType = detectWindowsShellType(process.env.SHELL) - if (shellType) { - return shellType + if (process.platform !== "win32") { + return detectShellType() + } + + // OpenCode on Windows runs the bash tool through a Windows shell + // (PowerShell by default, with cmd as the user-overridable fallback), + // regardless of MSYSTEM or a Unix-shaped SHELL value set by Git Bash. + // Map any explicit Windows shell we can recognize; otherwise default + // to PowerShell so the prepended env-var syntax matches the shell that + // actually executes the command. See #3607. + const fromShell = detectWindowsShellType(process.env.SHELL) + if (fromShell) { + return fromShell + } + if (!process.env.SHELL && !process.env.MSYSTEM) { + const fromComSpec = detectWindowsShellType(process.env.ComSpec) + if (fromComSpec) { + return fromComSpec } + return "cmd" } - - if (process.platform === "win32" && !process.env.SHELL && !process.env.MSYSTEM) { - return detectWindowsShellType(process.env.ComSpec) ?? "cmd" - } - - return detectShellType() + return "powershell" } export function createNonInteractiveEnvHook(_ctx: PluginInput) {