From fef1d4530b3be6151de8fd3fdea11616d494edd9 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 13 May 2026 20:07:46 +0900 Subject: [PATCH] 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.