diff --git a/src/agents/shell-utils.test.ts b/src/agents/shell-utils.test.ts index 9716fb73c8d..99dca3fba19 100644 --- a/src/agents/shell-utils.test.ts +++ b/src/agents/shell-utils.test.ts @@ -51,28 +51,39 @@ describe("getShellConfig", () => { it("prefers bash when fish is default and bash is on PATH", () => { const binDir = createTempCommandDir(tempDirs, [{ name: "bash" }]); process.env.PATH = binDir; - const { shell } = getShellConfig(); + const { shell, args } = getShellConfig(); expect(shell).toBe(path.join(binDir, "bash")); + expect(args).toEqual(["--noprofile", "--norc", "-c"]); }); it("falls back to sh when fish is default and bash is missing", () => { const binDir = createTempCommandDir(tempDirs, [{ name: "sh" }]); process.env.PATH = binDir; - const { shell } = getShellConfig(); + const { shell, args } = getShellConfig(); expect(shell).toBe(path.join(binDir, "sh")); + expect(args).toEqual(["-c"]); }); it("falls back to env shell when fish is default and no sh is available", () => { process.env.PATH = ""; - const { shell } = getShellConfig(); + const { shell, args } = getShellConfig(); expect(shell).toBe("/usr/bin/fish"); + expect(args).toEqual(["--no-config", "-c"]); + }); + + it("uses zsh no-rc mode to avoid startup-file env overrides", () => { + process.env.SHELL = "/bin/zsh"; + const { shell, args } = getShellConfig(); + expect(shell).toBe("/bin/zsh"); + expect(args).toEqual(["-f", "-c"]); }); it("uses sh when SHELL is unset", () => { delete process.env.SHELL; process.env.PATH = ""; - const { shell } = getShellConfig(); + const { shell, args } = getShellConfig(); expect(shell).toBe("sh"); + expect(args).toEqual(["-c"]); }); }); diff --git a/src/agents/shell-utils.ts b/src/agents/shell-utils.ts index a4a5dbc115a..e23a2bf600d 100644 --- a/src/agents/shell-utils.ts +++ b/src/agents/shell-utils.ts @@ -39,6 +39,23 @@ export function resolvePowerShellPath(): string { return "powershell.exe"; } +function resolvePosixShellArgs(shellPath: string): string[] { + const shellName = normalizeShellName(shellPath); + + // Keep exec commands deterministic: avoid user startup files overriding inherited + // daemon environment variables (for example launchd-provided secrets on macOS). + if (shellName === "zsh") { + return ["-f", "-c"]; + } + if (shellName === "bash") { + return ["--noprofile", "--norc", "-c"]; + } + if (shellName === "fish") { + return ["--no-config", "-c"]; + } + return ["-c"]; +} + export function getShellConfig(): { shell: string; args: string[] } { if (process.platform === "win32") { // Use PowerShell instead of cmd.exe on Windows. @@ -58,15 +75,15 @@ export function getShellConfig(): { shell: string; args: string[] } { if (shellName === "fish") { const bash = resolveShellFromPath("bash"); if (bash) { - return { shell: bash, args: ["-c"] }; + return { shell: bash, args: resolvePosixShellArgs(bash) }; } const sh = resolveShellFromPath("sh"); if (sh) { - return { shell: sh, args: ["-c"] }; + return { shell: sh, args: resolvePosixShellArgs(sh) }; } } const shell = envShell && envShell.length > 0 ? envShell : "sh"; - return { shell, args: ["-c"] }; + return { shell, args: resolvePosixShellArgs(shell) }; } export function resolveShellFromPath(name: string): string | undefined {