fix(hooks): use unix shell syntax for bash tool on all platforms (#1015)

The bash tool always runs in a Unix-like shell (bash/sh), even on Windows (via Git Bash, WSL, etc.), so we should always use unix export syntax instead of detecting the shell type dynamically.

Fixes #983

Fixes #889

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-01-23 16:24:58 +09:00
committed by GitHub
parent 262c7118da
commit 15c4637e0a
2 changed files with 34 additions and 33 deletions

View File

@@ -178,7 +178,11 @@ describe("non-interactive-env hook", () => {
})
})
describe("cross-platform shell support", () => {
describe("bash tool always uses unix shell syntax", () => {
// The bash tool always runs in a Unix-like shell (bash/sh), even on Windows
// (via Git Bash, WSL, etc.), so we should always use unix export syntax.
// This fixes GitHub issues #983 and #889.
test("#given macOS platform #when git command executes #then uses unix export syntax", async () => {
delete process.env.PSModulePath
process.env.SHELL = "/bin/zsh"
@@ -221,7 +225,9 @@ describe("non-interactive-env hook", () => {
expect(cmd).toContain("; git commit")
})
test("#given Windows with PowerShell #when git command executes #then uses powershell $env syntax", async () => {
test("#given Windows with PowerShell env #when bash tool git command executes #then still uses unix export syntax", async () => {
// Even when PSModulePath is set (indicating PowerShell environment),
// the bash tool runs in a Unix-like shell, so we use export syntax
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
Object.defineProperty(process, "platform", { value: "win32" })
@@ -236,13 +242,16 @@ describe("non-interactive-env hook", () => {
)
const cmd = output.args.command as string
expect(cmd).toContain("$env:")
// Should use unix export syntax, NOT PowerShell $env: syntax
expect(cmd).toStartWith("export ")
expect(cmd).toContain("; git status")
expect(cmd).not.toStartWith("export ")
expect(cmd).not.toContain("$env:")
expect(cmd).not.toContain("set ")
})
test("#given Windows without PowerShell #when git command executes #then uses cmd set syntax", async () => {
test("#given Windows without SHELL env #when bash tool git command executes #then still uses unix export syntax", async () => {
// Even when detectShellType() would return "cmd" (no SHELL, no PSModulePath, win32),
// the bash tool runs in a Unix-like shell, so we use export syntax
delete process.env.PSModulePath
delete process.env.SHELL
Object.defineProperty(process, "platform", { value: "win32" })
@@ -258,14 +267,18 @@ describe("non-interactive-env hook", () => {
)
const cmd = output.args.command as string
expect(cmd).toContain("set ")
expect(cmd).toContain("&&")
expect(cmd).not.toStartWith("export ")
// Should use unix export syntax, NOT cmd.exe set syntax
expect(cmd).toStartWith("export ")
expect(cmd).toContain("; git log")
expect(cmd).not.toContain("set ")
expect(cmd).not.toContain("&&")
expect(cmd).not.toContain("$env:")
})
test("#given PowerShell #when values contain quotes #then escapes correctly", async () => {
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
test("#given Windows Git Bash environment #when git command executes #then uses unix export syntax", async () => {
// Simulating Git Bash on Windows: SHELL might be set to /usr/bin/bash
delete process.env.PSModulePath
process.env.SHELL = "/usr/bin/bash"
Object.defineProperty(process, "platform", { value: "win32" })
const hook = createNonInteractiveEnvHook(mockCtx)
@@ -279,32 +292,16 @@ describe("non-interactive-env hook", () => {
)
const cmd = output.args.command as string
expect(cmd).toMatch(/\$env:\w+='[^']*'/)
expect(cmd).toStartWith("export ")
expect(cmd).toContain("; git status")
})
test("#given cmd.exe #when values contain spaces #then escapes correctly", async () => {
test("#given any platform #when chained git commands via bash tool #then uses unix export syntax", async () => {
// Even on Windows, chained commands should use unix syntax
delete process.env.PSModulePath
delete process.env.SHELL
Object.defineProperty(process, "platform", { value: "win32" })
const hook = createNonInteractiveEnvHook(mockCtx)
const output: { args: Record<string, unknown>; 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).toMatch(/set \w+="[^"]*"/)
})
test("#given PowerShell #when chained git commands #then env vars apply to all commands", async () => {
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
Object.defineProperty(process, "platform", { value: "win32" })
const hook = createNonInteractiveEnvHook(mockCtx)
const output: { args: Record<string, unknown>; message?: string } = {
args: { command: "git add file && git commit -m 'test'" },
@@ -316,7 +313,7 @@ describe("non-interactive-env hook", () => {
)
const cmd = output.args.command as string
expect(cmd).toContain("$env:")
expect(cmd).toStartWith("export ")
expect(cmd).toContain("; git add file && git commit")
})
})

View File

@@ -1,7 +1,8 @@
import type { PluginInput } from "@opencode-ai/plugin"
import type { ShellType } from "../../shared"
import { HOOK_NAME, NON_INTERACTIVE_ENV, SHELL_COMMAND_PATTERNS } from "./constants"
import { isNonInteractive } from "./detector"
import { log, detectShellType, buildEnvPrefix } from "../../shared"
import { log, buildEnvPrefix } from "../../shared"
export * from "./constants"
export * from "./detector"
@@ -50,7 +51,10 @@ export function createNonInteractiveEnvHook(_ctx: PluginInput) {
return
}
const shellType = detectShellType()
// The bash tool always runs in a Unix-like shell (bash/sh), even on Windows
// (via Git Bash, WSL, etc.), so we always use unix export syntax.
// This fixes GitHub issues #983 and #889.
const shellType: ShellType = "unix"
const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, shellType)
output.args.command = `${envPrefix} ${command}`