From fef1d4530b3be6151de8fd3fdea11616d494edd9 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 13 May 2026 20:07:46 +0900 Subject: [PATCH 1/2] fix(non-interactive-env): respect Windows command shell Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/non-interactive-env/index.test.ts | 50 ++++++++++++++++++- .../non-interactive-env-hook.ts | 27 +++++++++- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/hooks/non-interactive-env/index.test.ts b/src/hooks/non-interactive-env/index.test.ts index ecf5e06a9..7f34651c5 100644 --- a/src/hooks/non-interactive-env/index.test.ts +++ b/src/hooks/non-interactive-env/index.test.ts @@ -251,7 +251,7 @@ describe("non-interactive-env hook", () => { expect(cmd).toContain("; git commit") }) - test("#given Windows with PowerShell env #when bash tool git command executes #then uses powershell syntax", async () => { + test("#given Windows cmd environment with PSModulePath #when bash tool git command executes #then uses cmd syntax", async () => { delete process.env.SHELL delete process.env.MSYSTEM process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules" @@ -267,10 +267,56 @@ describe("non-interactive-env hook", () => { 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("$env:") + expect(cmd).not.toContain("export ") + }) + + test("#given Windows SHELL=cmd.exe #when bash tool git command executes #then uses cmd syntax", async () => { + process.env.SHELL = "C:\\Windows\\System32\\cmd.exe" + delete process.env.MSYSTEM + process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules" + 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).not.toContain("$env:") + expect(cmd).not.toContain("export ") + }) + + test("#given Windows SHELL=pwsh.exe #when bash tool git command executes #then uses powershell syntax", async () => { + process.env.SHELL = "C:\\Program Files\\PowerShell\\7\\pwsh.exe" + delete process.env.MSYSTEM + process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules" + 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("$env:") expect(cmd).toContain("; git status") - expect(cmd).toContain("$env:GIT_EDITOR=':'") expect(cmd).not.toContain("set ") expect(cmd).not.toContain("export ") }) 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 91c54b536..7fc034110 100644 --- a/src/hooks/non-interactive-env/non-interactive-env-hook.ts +++ b/src/hooks/non-interactive-env/non-interactive-env-hook.ts @@ -1,7 +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" +import { detectShellType, type ShellType } from "../../shared/shell-env" export * from "./constants" export * from "./detector" @@ -20,6 +20,29 @@ function detectBannedCommand(command: string): string | undefined { return undefined } +function detectCommandShellType(): ShellType { + if (process.platform === "win32" && process.env.SHELL) { + const shellName = process.env.SHELL.replace(/\\/g, "/").split("/").pop()?.toLowerCase() + if (shellName === "cmd" || shellName === "cmd.exe") { + return "cmd" + } + if ( + shellName === "powershell" || + shellName === "powershell.exe" || + shellName === "pwsh" || + shellName === "pwsh.exe" + ) { + return "powershell" + } + } + + if (process.platform === "win32" && !process.env.SHELL && !process.env.MSYSTEM) { + return "cmd" + } + + return detectShellType() +} + export function createNonInteractiveEnvHook(_ctx: PluginInput) { return { "tool.execute.before": async ( @@ -53,7 +76,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 = detectShellType() + const shellType = detectCommandShellType() const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, shellType) // Check if the command already starts with the prefix to avoid stacking. From 47d60a74d30f9c562a3e5c4e01b665f0399791bd Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 13 May 2026 22:47:37 +0900 Subject: [PATCH 2/2] fix(non-interactive-env): honor Windows ComSpec shell Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/non-interactive-env/index.test.ts | 25 +++++++++++++ .../non-interactive-env-hook.ts | 36 ++++++++++++------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/hooks/non-interactive-env/index.test.ts b/src/hooks/non-interactive-env/index.test.ts index 7f34651c5..0582a3853 100644 --- a/src/hooks/non-interactive-env/index.test.ts +++ b/src/hooks/non-interactive-env/index.test.ts @@ -13,6 +13,7 @@ describe("non-interactive-env hook", () => { SHELL: process.env.SHELL, PSModulePath: process.env.PSModulePath, MSYSTEM: process.env.MSYSTEM, + ComSpec: process.env.ComSpec, CI: process.env.CI, OPENCODE_NON_INTERACTIVE: process.env.OPENCODE_NON_INTERACTIVE, } @@ -298,6 +299,30 @@ describe("non-interactive-env hook", () => { expect(cmd).not.toContain("export ") }) + test("#given Windows ComSpec=pwsh.exe without SHELL #when bash tool git command executes #then uses powershell syntax", async () => { + delete process.env.SHELL + delete process.env.MSYSTEM + process.env.ComSpec = "C:\\Program Files\\PowerShell\\7\\pwsh.exe" + process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules" + 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("$env:") + expect(cmd).toContain("; git status") + expect(cmd).not.toContain("set ") + expect(cmd).not.toContain("export ") + }) + test("#given Windows SHELL=pwsh.exe #when bash tool git command executes #then uses powershell syntax", async () => { process.env.SHELL = "C:\\Program Files\\PowerShell\\7\\pwsh.exe" delete process.env.MSYSTEM 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 7fc034110..6fc42aea9 100644 --- a/src/hooks/non-interactive-env/non-interactive-env-hook.ts +++ b/src/hooks/non-interactive-env/non-interactive-env-hook.ts @@ -20,24 +20,36 @@ function detectBannedCommand(command: string): string | undefined { return undefined } +function detectWindowsShellType(shellPath: string | undefined): ShellType | undefined { + if (!shellPath) { + return undefined + } + + const shellName = shellPath.replace(/\\/g, "/").split("/").pop()?.toLowerCase() + if (shellName === "cmd" || shellName === "cmd.exe") { + return "cmd" + } + if ( + shellName === "powershell" || + shellName === "powershell.exe" || + shellName === "pwsh" || + shellName === "pwsh.exe" + ) { + return "powershell" + } + return undefined +} + function detectCommandShellType(): ShellType { if (process.platform === "win32" && process.env.SHELL) { - const shellName = process.env.SHELL.replace(/\\/g, "/").split("/").pop()?.toLowerCase() - if (shellName === "cmd" || shellName === "cmd.exe") { - return "cmd" - } - if ( - shellName === "powershell" || - shellName === "powershell.exe" || - shellName === "pwsh" || - shellName === "pwsh.exe" - ) { - return "powershell" + const shellType = detectWindowsShellType(process.env.SHELL) + if (shellType) { + return shellType } } if (process.platform === "win32" && !process.env.SHELL && !process.env.MSYSTEM) { - return "cmd" + return detectWindowsShellType(process.env.ComSpec) ?? "cmd" } return detectShellType()