From 796a646c632af6152acb4605aadcb4e6aa2231c7 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 12 Apr 2026 02:29:28 +0900 Subject: [PATCH] fix(non-interactive-env): use detectShellType instead of hardcoded win32 check (#3310, #3338) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardcoded platform check forced PowerShell env syntax on Windows regardless of the actual shell. Replaced with detectShellType() so Git Bash, WSL, and similar environments use unix prefixes while native PowerShell continues to get powershell prefixes. 🤖 Generated with OhMyOpenCode assistance https://github.com/code-yeongyu/oh-my-opencode --- src/hooks/non-interactive-env/index.test.ts | 111 ++++++++++++++++-- .../non-interactive-env-hook.ts | 3 +- 2 files changed, 104 insertions(+), 10 deletions(-) diff --git a/src/hooks/non-interactive-env/index.test.ts b/src/hooks/non-interactive-env/index.test.ts index 9841e6e4f..4e3419eb4 100644 --- a/src/hooks/non-interactive-env/index.test.ts +++ b/src/hooks/non-interactive-env/index.test.ts @@ -251,6 +251,7 @@ describe("non-interactive-env hook", () => { }) test("#given Windows with PowerShell env #when bash tool git command executes #then uses powershell syntax", async () => { + delete process.env.SHELL process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules" Object.defineProperty(process, "platform", { value: "win32" }) @@ -272,7 +273,7 @@ describe("non-interactive-env hook", () => { expect(cmd).not.toContain("export ") }) - test("#given Windows without SHELL env #when bash tool git command executes #then uses powershell syntax", async () => { + test("#given Windows without SHELL env #when bash tool git command executes #then uses cmd syntax", async () => { delete process.env.PSModulePath delete process.env.SHELL Object.defineProperty(process, "platform", { value: "win32" }) @@ -288,10 +289,10 @@ describe("non-interactive-env hook", () => { ) const cmd = output.args.command as string - expect(cmd).toStartWith("$env:") - expect(cmd).toContain("; git log") - expect(cmd).not.toContain("set ") - expect(cmd).toContain("$env:GIT_EDITOR=':'") + expect(cmd).toStartWith("set ") + expect(cmd).toContain(" && git log") + expect(cmd).toContain('GIT_EDITOR=":"') + expect(cmd).not.toContain("$env:") expect(cmd).not.toContain("export ") }) @@ -317,7 +318,7 @@ describe("non-interactive-env hook", () => { expect(cmd.length).toBeGreaterThan("git status".length) }) - test("#given Windows platform #when chained git commands via bash tool #then uses powershell syntax", async () => { + test("#given Windows platform #when chained git commands via bash tool #then uses cmd syntax", async () => { delete process.env.PSModulePath delete process.env.SHELL Object.defineProperty(process, "platform", { value: "win32" }) @@ -333,10 +334,102 @@ describe("non-interactive-env hook", () => { ) const cmd = output.args.command as string - expect(cmd).toStartWith("$env:") - expect(cmd).toContain("; git add file && git commit") - expect(cmd).toContain("$env:GIT_EDITOR=':'") + expect(cmd).toStartWith("set ") + expect(cmd).toContain(" && git add file && git commit") + expect(cmd).toContain('GIT_EDITOR=":"') expect(cmd).not.toContain("export ") + 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 + delete process.env.PSModulePath + process.env.SHELL = "/bin/bash" + Object.defineProperty(process, "platform", { value: "win32" }) + + const hook = createNonInteractiveEnvHook(mockCtx) + const output: { args: Record; message?: string } = { + args: { command: "git status" }, + } + + await hook["tool.execute.before"]( + { tool: "bash", sessionID: "test", callID: "1" }, + output + ) + + const cmd = output.args.command as string + expect(cmd).toStartWith("export ") + expect(cmd).toContain("; git status") + expect(cmd).not.toContain("$env:") + }) + + test("#given PSModulePath set on non-Windows #when git command executes #then uses powershell syntax", async () => { + // PowerShell detection via PSModulePath should work regardless of platform + delete process.env.SHELL + process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules" + Object.defineProperty(process, "platform", { value: "linux" }) + + const hook = createNonInteractiveEnvHook(mockCtx) + const output: { args: Record; message?: string } = { + args: { command: "git log" }, + } + + await hook["tool.execute.before"]( + { tool: "bash", sessionID: "test", callID: "1" }, + output + ) + + const cmd = output.args.command as string + expect(cmd).toStartWith("$env:") + expect(cmd).toContain("; git log") + expect(cmd).not.toContain("export ") + }) + + test("#given no SHELL and no PSModulePath on win32 #when git command executes #then uses cmd syntax", async () => { + // Platform fallback: win32 without env hints should use cmd + delete process.env.SHELL + delete process.env.PSModulePath + Object.defineProperty(process, "platform", { value: "win32" }) + + const hook = createNonInteractiveEnvHook(mockCtx) + const output: { args: Record; message?: string } = { + args: { command: "git status" }, + } + + await hook["tool.execute.before"]( + { tool: "bash", sessionID: "test", callID: "1" }, + output + ) + + const cmd = output.args.command as string + expect(cmd).toStartWith("set ") + expect(cmd).toContain(" && git status") + expect(cmd).toContain('GIT_EDITOR=":"') + expect(cmd).not.toContain("export ") + expect(cmd).not.toContain("$env:") + }) + + test("#given no SHELL and no PSModulePath on linux #when git command executes #then uses unix syntax", async () => { + // Platform fallback: non-win32 without env hints should use unix + delete process.env.SHELL + delete process.env.PSModulePath + Object.defineProperty(process, "platform", { value: "linux" }) + + const hook = createNonInteractiveEnvHook(mockCtx) + const output: { args: Record; message?: string } = { + args: { command: "git status" }, + } + + await hook["tool.execute.before"]( + { tool: "bash", sessionID: "test", callID: "1" }, + output + ) + + const cmd = output.args.command as string + expect(cmd).toStartWith("export ") + expect(cmd).toContain("; git status") + expect(cmd).not.toContain("$env:") + expect(cmd).not.toContain("set ") }) }) }) 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 d97037ee9..91c54b536 100644 --- a/src/hooks/non-interactive-env/non-interactive-env-hook.ts +++ b/src/hooks/non-interactive-env/non-interactive-env-hook.ts @@ -1,6 +1,7 @@ import type { PluginInput } from "@opencode-ai/plugin" import { HOOK_NAME, NON_INTERACTIVE_ENV, SHELL_COMMAND_PATTERNS } from "./constants" import { log, buildEnvPrefix } from "../../shared" +import { detectShellType } from "../../shared/shell-env" export * from "./constants" export * from "./detector" @@ -52,7 +53,7 @@ export function createNonInteractiveEnvHook(_ctx: PluginInput) { // The env vars (GIT_EDITOR=:, EDITOR=:, etc.) must ALWAYS be injected // for git commands to prevent interactive prompts. - const shellType = process.platform === "win32" ? "powershell" : "unix" + const shellType = detectShellType() const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, shellType) // Check if the command already starts with the prefix to avoid stacking.