Skip to content

Commit ff642cf

Browse files
committed
fix(exec): avoid shell startup-file env overrides
1 parent abf940d commit ff642cf

2 files changed

Lines changed: 29 additions & 22 deletions

File tree

src/agents/shell-utils.test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,28 +56,39 @@ describe("getShellConfig", () => {
5656
it("prefers bash when fish is default and bash is on PATH", () => {
5757
const binDir = createTempCommandDir(tempDirs, [{ name: "bash" }]);
5858
process.env.PATH = binDir;
59-
const { shell } = getShellConfig();
59+
const { shell, args } = getShellConfig();
6060
expect(shell).toBe(path.join(binDir, "bash"));
61+
expect(args).toEqual(["--noprofile", "--norc", "-c"]);
6162
});
6263

6364
it("falls back to sh when fish is default and bash is missing", () => {
6465
const binDir = createTempCommandDir(tempDirs, [{ name: "sh" }]);
6566
process.env.PATH = binDir;
66-
const { shell } = getShellConfig();
67+
const { shell, args } = getShellConfig();
6768
expect(shell).toBe(path.join(binDir, "sh"));
69+
expect(args).toEqual(["-c"]);
6870
});
6971

7072
it("falls back to env shell when fish is default and no sh is available", () => {
7173
process.env.PATH = "";
72-
const { shell } = getShellConfig();
74+
const { shell, args } = getShellConfig();
7375
expect(shell).toBe("/usr/bin/fish");
76+
expect(args).toEqual(["-c"]);
77+
});
78+
79+
it("uses zsh no-rc mode to avoid startup-file env overrides", () => {
80+
process.env.SHELL = "/bin/zsh";
81+
const { shell, args } = getShellConfig();
82+
expect(shell).toBe("/bin/zsh");
83+
expect(args).toEqual(["-f", "-c"]);
7484
});
7585

7686
it("uses sh when SHELL is unset", () => {
7787
delete process.env.SHELL;
7888
process.env.PATH = "";
79-
const { shell } = getShellConfig();
89+
const { shell, args } = getShellConfig();
8090
expect(shell).toBe("sh");
91+
expect(args).toEqual(["-c"]);
8192
});
8293

8394
it("falls back to sh on PATH when SHELL is /usr/bin/false", () => {

src/agents/shell-utils.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,18 @@ export function resolvePowerShellPath(): string {
3838
return "powershell.exe";
3939
}
4040

41-
// Non-interactive placeholder shells that reject "-c"-style invocations.
42-
// macOS LaunchDaemon service users commonly use /usr/bin/false so login sessions
43-
// cannot be opened; honoring SHELL in that case causes every exec to exit 1.
44-
// See https://github.com/openclaw/openclaw/issues/69077.
45-
const NON_INTERACTIVE_SHELLS = new Set(["false", "nologin"]);
41+
function resolvePosixShellArgs(shellPath: string): string[] {
42+
const shellName = normalizeShellName(shellPath);
4643

47-
function isNonInteractiveShell(shellPath: string): boolean {
48-
if (!shellPath) {
49-
return false;
44+
// Keep exec commands deterministic: avoid user startup files overriding inherited
45+
// daemon environment variables (for example launchd-provided secrets on macOS).
46+
if (shellName === "zsh") {
47+
return ["-f", "-c"];
5048
}
51-
return NON_INTERACTIVE_SHELLS.has(path.basename(shellPath));
49+
if (shellName === "bash") {
50+
return ["--noprofile", "--norc", "-c"];
51+
}
52+
return ["-c"];
5253
}
5354

5455
export function getShellConfig(): { shell: string; args: string[] } {
@@ -71,20 +72,15 @@ export function getShellConfig(): { shell: string; args: string[] } {
7172
if (shellName === "fish") {
7273
const bash = resolveShellFromPath("bash");
7374
if (bash) {
74-
return { shell: bash, args: ["-c"] };
75+
return { shell: bash, args: resolvePosixShellArgs(bash) };
7576
}
7677
const sh = resolveShellFromPath("sh");
7778
if (sh) {
78-
return { shell: sh, args: ["-c"] };
79+
return { shell: sh, args: resolvePosixShellArgs(sh) };
7980
}
8081
}
81-
if (envShell) {
82-
return { shell: envShell, args: ["-c"] };
83-
}
84-
// Placeholder SHELL (or unset): prefer a resolved sh/bash on PATH so we do not
85-
// re-invoke the placeholder and get a spurious exitCode=1.
86-
const sh = resolveShellFromPath("sh") ?? resolveShellFromPath("bash");
87-
return { shell: sh ?? "sh", args: ["-c"] };
82+
const shell = envShell && envShell.length > 0 ? envShell : "sh";
83+
return { shell, args: resolvePosixShellArgs(shell) };
8884
}
8985

9086
export function resolveShellFromPath(name: string): string | undefined {

0 commit comments

Comments
 (0)