diff --git a/collab-electron/src/main/config.ts b/collab-electron/src/main/config.ts
index 79b1d7c9..2e2ccc2d 100644
--- a/collab-electron/src/main/config.ts
+++ b/collab-electron/src/main/config.ts
@@ -127,3 +127,11 @@ export function getTerminalTarget(): TerminalTarget {
const target = getPref(config, "terminalTarget");
return isTerminalTarget(target) ? target : "auto";
}
+
+export function getTerminalCommand(): string | null {
+ const config = loadConfig();
+ const value = getPref(config, "terminalCommand");
+ if (typeof value !== "string") return null;
+ const trimmed = value.trim();
+ return trimmed === "" ? null : trimmed;
+}
diff --git a/collab-electron/src/main/index.ts b/collab-electron/src/main/index.ts
index 23bb006c..15f6e9dd 100644
--- a/collab-electron/src/main/index.ts
+++ b/collab-electron/src/main/index.ts
@@ -48,7 +48,7 @@ import {
} from "./analytics";
import { stopImageWorker } from "./image-service";
import { installCli } from "./cli-installer";
-import { listTerminalTargets } from "./terminal-target";
+import { commandExists, listTerminalTargets } from "./terminal-target";
import { readSessionMeta } from "./tmux";
import { registerBrowserIpc } from "./ipc-browser";
import { registerAgentIpc } from "./acp-agent";
@@ -568,6 +568,15 @@ ipcMain.handle(
() => listTerminalTargets(),
);
+ipcMain.handle(
+ "terminal:check-shell-command",
+ (_event, command: string) => {
+ const trimmed = typeof command === "string" ? command.trim() : "";
+ if (trimmed === "") return true;
+ return commandExists(trimmed);
+ },
+);
+
ipcMain.handle(
"theme:set",
(_event, mode: string) => {
diff --git a/collab-electron/src/main/pty.ts b/collab-electron/src/main/pty.ts
index aed3ab3f..436a57f3 100644
--- a/collab-electron/src/main/pty.ts
+++ b/collab-electron/src/main/pty.ts
@@ -32,7 +32,7 @@ import {
SIDECAR_PID_PATH,
} from "./sidecar/protocol";
import { COLLAB_DIR } from "./paths";
-import { resolveTerminalTarget } from "./terminal-target";
+import { resolveShellPath, resolveTerminalTarget } from "./terminal-target";
interface PtySession {
pty: pty.IPty;
@@ -486,7 +486,7 @@ export async function createSession(
cwdGuestPath?: string;
}> {
const resolvedCwd = cwd || os.homedir();
- const shell = process.env.SHELL || "/bin/zsh";
+ const shell = resolveShellPath();
const c = cols || 80;
const r = rows || 24;
@@ -520,6 +520,7 @@ export async function createSession(
"-c", resolvedCwd,
"-x", String(c),
"-y", String(r),
+ shell,
);
if (zshIntegrated) {
diff --git a/collab-electron/src/main/terminal-target.ts b/collab-electron/src/main/terminal-target.ts
index 3a2e0dca..03a34d6a 100644
--- a/collab-electron/src/main/terminal-target.ts
+++ b/collab-electron/src/main/terminal-target.ts
@@ -1,7 +1,7 @@
import { execFileSync } from "node:child_process";
import * as os from "node:os";
import { displayBasename, hostPathToGuestPath, parseWslUncPath } from "@collab/shared/path-utils";
-import { type TerminalTarget } from "./config";
+import { getTerminalCommand, type TerminalTarget } from "./config";
export interface TerminalTargetOption {
id: TerminalTarget;
@@ -33,7 +33,7 @@ interface WslDistro {
isDefault: boolean;
}
-function commandExists(command: string): boolean {
+export function commandExists(command: string): boolean {
try {
execFileSync(
process.platform === "win32" ? "where.exe" : "which",
@@ -121,7 +121,9 @@ function resolveWindowsAutoTarget(
return defaultDistro ? `wsl:${defaultDistro}` : "powershell";
}
-function resolveShellPath(): string {
+export function resolveShellPath(): string {
+ const override = getTerminalCommand();
+ if (override && commandExists(override)) return override;
if (process.platform === "darwin") {
return process.env.SHELL || "/bin/zsh";
}
diff --git a/collab-electron/src/preload/universal.ts b/collab-electron/src/preload/universal.ts
index 030bb989..37b22241 100644
--- a/collab-electron/src/preload/universal.ts
+++ b/collab-electron/src/preload/universal.ts
@@ -156,6 +156,8 @@ contextBridge.exposeInMainWorld("api", {
ipcRenderer.invoke("pref:set", key, value),
listTerminalTargets: () =>
ipcRenderer.invoke("terminal:list-targets"),
+ checkShellCommand: (command: string) =>
+ ipcRenderer.invoke("terminal:check-shell-command", command),
getWorkspacePref: (key: string, workspacePath: string) =>
ipcRenderer.invoke("workspace-pref:get", { key, workspacePath }),
setWorkspacePref: (key: string, value: unknown, workspacePath: string) =>
diff --git a/collab-electron/src/windows/settings/src/App.tsx b/collab-electron/src/windows/settings/src/App.tsx
index b993666d..aff080d6 100644
--- a/collab-electron/src/windows/settings/src/App.tsx
+++ b/collab-electron/src/windows/settings/src/App.tsx
@@ -20,6 +20,7 @@ interface SettingsApi {
label: string;
isDefault?: boolean;
}>>;
+ checkShellCommand: (command: string) => Promise Shell command
+ Not found on PATH. New terminals will fall back to your login shell.
+
+ Name or path of a shell binary (e.g.
+ {" "}
+ /bin/bash
+ ). Leave empty to use your login shell.
+
Terminal backend