Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions core/tools/implementations/runTerminalCommand.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import iconv from "iconv-lite";
import childProcess from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import { ContinueError, ContinueErrorReason } from "../../util/errors";
// Automatically decode the buffer according to the platform to avoid garbled Chinese
Expand All @@ -26,9 +27,25 @@ function getShellCommand(command: string): { shell: string; args: string[] } {
args: ["-NoLogo", "-ExecutionPolicy", "Bypass", "-Command", command],
};
} else {
// Unix/macOS: Use login shell to source .bashrc/.zshrc etc.
const userShell = process.env.SHELL || "/bin/bash";
return { shell: userShell, args: ["-l", "-c", command] };
// Unix/macOS: prefer configured shell, but gracefully fall back when unavailable
// (e.g. minimal dev containers without /bin/bash).
const configuredShell = process.env.SHELL;
const unixFallbacks = ["/bin/bash", "/bin/sh", "/bin/ash"];
const shellCandidates = configuredShell
? [configuredShell, ...unixFallbacks]
: unixFallbacks;

const shell =
shellCandidates.find((candidate) => {
if (!candidate) return false;
if (!candidate.startsWith("/")) {
// Non-absolute shells are resolved by PATH at spawn time.
return true;
}
return fs.existsSync(candidate);
}) ?? "/bin/sh";

return { shell, args: ["-l", "-c", command] };
}
}

Expand Down
21 changes: 21 additions & 0 deletions core/tools/implementations/runTerminalCommand.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,27 @@ describe("runTerminalCommandImpl", () => {
expect(result[0].status).toBe("Command completed");
});

it("should fall back to an available shell when SHELL is invalid", async () => {
if (process.platform === "win32") {
return;
}

const originalShell = process.env.SHELL;
process.env.SHELL = "/definitely-not-a-real-shell";

try {
const result = await runTerminalCommandImpl(
{ command: `node -e "console.log('fallback shell works')"` },
createMockExtras(),
);

expect(result[0].status).toBe("Command completed");
expect(result[0].content).toContain("fallback shell works");
} finally {
process.env.SHELL = originalShell;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Environment variable cleanup may leak SHELL state - assigning undefined to process.env creates string 'undefined'

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At core/tools/implementations/runTerminalCommand.vitest.ts, line 162:

<comment>Environment variable cleanup may leak SHELL state - assigning undefined to process.env creates string 'undefined'</comment>

<file context>
@@ -142,6 +142,27 @@ describe("runTerminalCommandImpl", () => {
+      expect(result[0].status).toBe("Command completed");
+      expect(result[0].content).toContain("fallback shell works");
+    } finally {
+      process.env.SHELL = originalShell;
+    }
+  });
</file context>
Fix with Cubic

}
});

it("should stream output when onPartialOutput is provided", async () => {
// This test uses Node to create a command that outputs data incrementally
const command = `node -e "
Expand Down
Loading