fix(terminal): read host IDE shell from CLINE_TERMINAL_SHELL_PATH instead of COMSPEC/SHELL

Host integrations (the JetBrains plugin) forwarded the user's IDE terminal
shell by overriding COMSPEC on Windows. COMSPEC is an OS-level contract that
must keep pointing at cmd.exe: cross-spawn, Node's shell:true, and npm .cmd
shims all pass cmd-style /d /s /c arguments to whatever it references.
With IntelliJ's default terminal shell (PowerShell) forwarded as COMSPEC,
every .cmd-shimmed child process spawned by cline-core broke - e.g. the
Claude Code provider failed with:
  /d : The term '/d' is not recognized ...

Introduce a dedicated CLINE_TERMINAL_SHELL_PATH env var that hosts can set
safely. It takes precedence in getShell(), in the standalone terminal's
default-shell pick, and in the system-prompt shell report for background
exec mode, while COMSPEC/SHELL keep their OS semantics.

Fixes #11290
This commit is contained in:
Mikołaj Kondratek
2026-07-09 13:53:58 +02:00
parent 797565f203
commit 602097477a
4 changed files with 81 additions and 3 deletions
@@ -1,5 +1,5 @@
import osModule from "node:os"
import { getShell } from "@utils/shell"
import { getHostProvidedShell, getShell } from "@utils/shell"
import osName from "os-name"
import { getWorkspacePaths } from "@/hosts/vscode/hostbridge/workspace/getWorkspacePaths"
import { SystemPromptSection } from "../templates/placeholders"
@@ -21,7 +21,13 @@ Home Directory: {{homeDir}}
*/
function getEffectiveShell(context: SystemPromptContext): string {
if (context.terminalExecutionMode === "backgroundExec") {
// Background exec uses the system default shell, not VS Code config
// Background exec uses the host-forwarded shell if present (mirrors
// StandaloneTerminalProcess.getDefaultShell), otherwise the system
// default shell, not the VS Code config.
const hostShell = getHostProvidedShell()
if (hostShell) {
return hostShell
}
if (process.platform === "win32") {
return process.env.COMSPEC || "cmd.exe"
} else {
@@ -9,6 +9,7 @@
*/
import { telemetryService } from "@services/telemetry"
import { getHostProvidedShell } from "@utils/shell"
import { ChildProcess, spawn } from "child_process"
import { EventEmitter } from "events"
import { terminateProcessTree } from "@/utils/process-termination"
@@ -308,6 +309,12 @@ export class StandaloneTerminalProcess extends EventEmitter<TerminalProcessEvent
* @returns The default shell path
*/
private getDefaultShell(): string {
// A shell explicitly forwarded by the host IDE (e.g. the JetBrains
// terminal profile) takes precedence over the OS defaults.
const hostShell = getHostProvidedShell()
if (hostShell) {
return hostShell
}
if (process.platform === "win32") {
return process.env.COMSPEC || "cmd.exe"
}
+38 -1
View File
@@ -1,4 +1,4 @@
import { getShell } from "@utils/shell"
import { getShell, HOST_SHELL_ENV_VAR } from "@utils/shell"
import { expect } from "chai"
import { afterEach, beforeEach, describe, it } from "mocha"
import { userInfo } from "os"
@@ -36,6 +36,7 @@ describe("Shell Detection Tests", () => {
// Clear environment variables for a clean test
delete process.env.SHELL
delete process.env.COMSPEC
delete process.env[HOST_SHELL_ENV_VAR]
// Default userInfo() mock
;(userInfo as any) = () => ({ shell: null })
@@ -49,6 +50,42 @@ describe("Shell Detection Tests", () => {
;(userInfo as any) = originalUserInfo
})
// --------------------------------------------------------------------------
// Host-Provided Shell Override (CLINE_TERMINAL_SHELL_PATH)
// --------------------------------------------------------------------------
describe("Host-Provided Shell Override", () => {
it("wins over VS Code config, userInfo, and COMSPEC on Windows", () => {
Object.defineProperty(process, "platform", { value: "win32" })
mockVsCodeConfig("windows", "PowerShell", {
PowerShell: { path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe" },
})
;(userInfo as any) = () => ({ shell: "C:\\Custom\\PowerShell.exe" })
process.env.COMSPEC = "C:\\Windows\\System32\\cmd.exe"
process.env[HOST_SHELL_ENV_VAR] = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
expect(getShell()).to.equal("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe")
})
it("wins over userInfo and SHELL on POSIX", () => {
Object.defineProperty(process, "platform", { value: "darwin" })
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
;(userInfo as any) = () => ({ shell: "/bin/zsh" })
process.env.SHELL = "/bin/bash"
process.env[HOST_SHELL_ENV_VAR] = "/usr/local/bin/fish"
expect(getShell()).to.equal("/usr/local/bin/fish")
})
it("is ignored when blank", () => {
Object.defineProperty(process, "platform", { value: "darwin" })
vscode.workspace.getConfiguration = () => ({ get: () => undefined }) as any
;(userInfo as any) = () => ({ shell: "/bin/zsh" })
process.env[HOST_SHELL_ENV_VAR] = " "
expect(getShell()).to.equal("/bin/zsh")
})
})
// --------------------------------------------------------------------------
// Windows Shell Detection
// --------------------------------------------------------------------------
+28
View File
@@ -43,6 +43,28 @@ interface LinuxTerminalProfile {
type LinuxTerminalProfiles = Record<string, LinuxTerminalProfile>
// -----------------------------------------------------
// 0) Host-Provided Shell Override
// -----------------------------------------------------
/**
* Environment variable through which host integrations (e.g. the JetBrains
* plugin) forward the user's IDE terminal profile shell to cline-core.
*
* A dedicated variable is used instead of overriding COMSPEC/SHELL because
* COMSPEC is an OS-level contract that must keep pointing at cmd.exe:
* cross-spawn, Node's `shell: true`, and npm `.cmd` shims all pass cmd-style
* `/d /s /c` arguments to whatever COMSPEC references. Overriding it with
* PowerShell breaks every such child process (see cline/cline#11290).
*/
export const HOST_SHELL_ENV_VAR = "CLINE_TERMINAL_SHELL_PATH"
/** Returns the shell explicitly forwarded by the host IDE, or null. */
export function getHostProvidedShell(): string | null {
const shell = process.env[HOST_SHELL_ENV_VAR]?.trim()
return shell || null
}
// -----------------------------------------------------
// 1) VS Code Terminal Configuration Helpers
// -----------------------------------------------------
@@ -299,6 +321,12 @@ export function getShellForProfile(profileId: string): string {
// -----------------------------------------------------
export function getShell(): string {
// 0. A shell explicitly forwarded by the host IDE wins over detection.
const hostShell = getHostProvidedShell()
if (hostShell) {
return hostShell
}
// 1. Check VS Code config first.
if (process.platform === "win32") {
// Special logic for Windows