Skip to content

Commit 7b414d8

Browse files
sk7n4k3dsteipete
authored andcommitted
shell: fall back to sh when SHELL is /usr/bin/false or nologin
1 parent 7b1871b commit 7b414d8

2 files changed

Lines changed: 47 additions & 4 deletions

File tree

src/agents/shell-utils.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,30 @@ describe("getShellConfig", () => {
7474
const { shell } = getShellConfig();
7575
expect(shell).toBe("sh");
7676
});
77+
78+
it("falls back to sh on PATH when SHELL is /usr/bin/false", () => {
79+
const binDir = createTempCommandDir(tempDirs, [{ name: "sh" }]);
80+
process.env.SHELL = "/usr/bin/false";
81+
process.env.PATH = binDir;
82+
const { shell, args } = getShellConfig();
83+
expect(shell).toBe(path.join(binDir, "sh"));
84+
expect(args).toEqual(["-c"]);
85+
});
86+
87+
it("falls back to sh on PATH when SHELL is /sbin/nologin", () => {
88+
const binDir = createTempCommandDir(tempDirs, [{ name: "sh" }]);
89+
process.env.SHELL = "/sbin/nologin";
90+
process.env.PATH = binDir;
91+
const { shell } = getShellConfig();
92+
expect(shell).toBe(path.join(binDir, "sh"));
93+
});
94+
95+
it("falls back to bare sh when SHELL is a placeholder and no sh is on PATH", () => {
96+
process.env.SHELL = "/usr/bin/false";
97+
process.env.PATH = "";
98+
const { shell } = getShellConfig();
99+
expect(shell).toBe("sh");
100+
});
77101
});
78102

79103
describe("resolveShellFromPath", () => {

src/agents/shell-utils.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ 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"]);
46+
47+
function isNonInteractiveShell(shellPath: string): boolean {
48+
if (!shellPath) {
49+
return false;
50+
}
51+
return NON_INTERACTIVE_SHELLS.has(path.basename(shellPath));
52+
}
53+
4154
export function getShellConfig(): { shell: string; args: string[] } {
4255
if (process.platform === "win32") {
4356
// Use PowerShell instead of cmd.exe on Windows.
@@ -51,7 +64,8 @@ export function getShellConfig(): { shell: string; args: string[] } {
5164
};
5265
}
5366

54-
const envShell = process.env.SHELL?.trim();
67+
const rawEnvShell = process.env.SHELL?.trim();
68+
const envShell = rawEnvShell && !isNonInteractiveShell(rawEnvShell) ? rawEnvShell : undefined;
5569
const shellName = envShell ? path.basename(envShell) : "";
5670
// Fish rejects common bashisms used by tools, so prefer bash when detected.
5771
if (shellName === "fish") {
@@ -64,8 +78,13 @@ export function getShellConfig(): { shell: string; args: string[] } {
6478
return { shell: sh, args: ["-c"] };
6579
}
6680
}
67-
const shell = envShell && envShell.length > 0 ? envShell : "sh";
68-
return { shell, args: ["-c"] };
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"] };
6988
}
7089

7190
export function resolveShellFromPath(name: string): string | undefined {
@@ -114,7 +133,7 @@ export function detectRuntimeShell(): string | undefined {
114133
}
115134

116135
const envShell = process.env.SHELL?.trim();
117-
if (envShell) {
136+
if (envShell && !isNonInteractiveShell(envShell)) {
118137
const name = normalizeShellName(envShell);
119138
if (name) {
120139
return name;

0 commit comments

Comments
 (0)