Where does the bug appear (feature/product)?
Cursor CLI
Describe the Bug
The Agent CLI on Windows always uses PowerShell for command execution. There is no --shell CLI flag, no shell field in ~/.cursor/cli-config.json, and $SHELL / $COMSPEC environment variables are ignored by the shell detection logic.
This causes:
- Slow startup: PowerShell has multi-second cold-start times (5-30s observed) that delay every agent command
- Post-execution hangs: Commands stall for several seconds after completing
- No user choice: Users cannot switch to faster shells like nushell, bash, or fish
Steps to Reproduce
- Install Agent CLI on Windows:
irm 'https://cursor.com/install?win32=true' | iex - Run
agent about– note it reportsShell: cmdandTerminal: unknown - Ask the agent to run any command – observe it uses PowerShell
- Set
$env:SHELL = "C:\Users\...\nu.exe"and retry – still uses PowerShell - There is no
--shellflag andcli-config.jsonhas no shell field
Expected Behavior
The Agent CLI should support configuring which shell is used for command execution, either via:
- A
--shellCLI flag:agent --shell "C:\path\to\nu.exe" - A
shell.pathfield in~/.cursor/cli-config.json - Respecting the
$SHELLenvironment variable on Windows (like it does on Unix)
Operating System
Windows 10/11
Version Information
CLI: 2026.02.13-41ac335
Additional Information
Root cause analysis (from examining the shared shell execution library in CLI v2026.02.13-41ac335):
The CLI and IDE agents share a shell execution library. There are two key components:
1. Shell type detection (detectShellType):
function detectShellType(hint) {
const shellStr = hint || process.env.SHELL || "";
const isWindows = process.platform === "win32";
const isGitBash = /git.*bash\.exe$/i.test(shellStr)
|| /program.*git.*bin.*bash\.exe$/i.test(shellStr);
return shellStr.includes("zsh") ? ShellType.Zsh
: shellStr.includes("bash") && isGitBash ? ShellType.Bash
// On Windows, the PowerShell condition includes a system-level fallback
// in the SAME ternary arm -- if PowerShell is installed (always true on
// Windows), this fires regardless of the hint string:
: shellStr.includes("pwsh") || shellStr.includes("powershell")
|| isWindows && (commandExists("pwsh") || commandExists("powershell"))
? ShellType.PowerShell
// No checks for "nu", "fish", or any other shell
// Everything below is UNREACHABLE on Windows when PowerShell is installed
: commandExists("zsh") ? ShellType.Zsh
: commandExists("bash") && isGitBash ? ShellType.Bash
: commandExists("pwsh") || commandExists("powershell") ? ShellType.PowerShell
: ShellType.Naive;
}
2. Executor factory (createExecutor):
function createExecutor(options) {
let opts = options;
if (!options?.userTerminalHint) {
const gitBash = detectGitBash();
gitBash && (opts = { ...options, userTerminalHint: gitBash });
}
switch (detectShellType(opts?.userTerminalHint ?? "")) {
case ShellType.Zsh: return new LazyExecutor(ZshStateExecutor.init(opts));
case ShellType.Bash: return new LazyExecutor(BashStateExecutor.init(opts));
case ShellType.PowerShell: return new LazyExecutor(PowerShellExecutor.init());
// ShellType.Naive has NO case -- falls through with no executor
}
}
Critical detail about the executor class hierarchy:
The codebase has two fundamentally different executor types:
BashStateExecutor(also used by Zsh): Tracks shell state (cwd,env,aliases) by runningdump_bash_state/dump_zsh_stateafter each command. Itsexecute()method always spawns Bash/Zsh specifically. Constructed with(cwd, state, userTerminalHint, useFileStateTransport).NaiveTerminalExecutor: A generic, shell-agnostic executor. Itsexecute()method spawnsoptions.shell -c "command", working with any shell that supports-c(including nushell, fish, bash, etc.). Constructed with(cwd, {shell: "/path/to/shell", ...options}).
The NaiveTerminalExecutor already exists and works correctly. Nushell supports -c for command execution. The only problem is that the code never routes to it.
Three problems:
- CLI has no config path for shell:
userTerminalHintis never set –cli-config.jsonhas no shell field, there’s no--shellflag, andprocess.env.SHELLis unset on Windows. SodetectShellType("")is called, which always falls to PowerShell. - Detection function ignores non-standard shells: Even if
userTerminalHintwere set to a nushell path, there’s noincludes("nu")check. Worse, on Windows the PowerShell ternary arm combines both string matching and a system-levelcommandExistsfallback in one||chain. Since PowerShell is always installed, this condition evaluates totrueregardless of the hint string – any shell check placed after this point is dead code on Windows. - No
Naivecase in executor factory: Even if detection returnedShellType.Naive, the factory switch has no case for it.
Proposed fix:
- Add
shell.pathtocli-config.jsonschema and/or a--shellCLI flag, wired touserTerminalHint - In
detectShellType(), add non-standard shell recognition before the PowerShell condition (critical – placing it after is unreachable on Windows due to the combinedincludes || commandExistscondition):
: shellStr.includes("bash") && isGitBash ? ShellType.Bash
// These must come BEFORE the PowerShell check:
: shellStr.includes("nu") ? ShellType.Naive
: shellStr.includes("fish") ? ShellType.Naive
: shellStr.length > 0 ? ShellType.Naive // trust any explicit user config
// Then the existing PowerShell check:
: shellStr.includes("pwsh") || shellStr.includes("powershell")
|| isWindows && (commandExists("pwsh") || commandExists("powershell"))
? ShellType.PowerShell
: /* existing fallback chain */
- Add the missing
ShellType.Naivecase to the executor factory usingNaiveTerminalExecutor(NOTBashStateExecutor– that class always requires Bash and will throw “Can’t find Bash” on systems without Git Bash):
case ShellType.Naive:
return new LazyExecutor(Promise.resolve(
new NaiveTerminalExecutor(process.cwd(), {
shell: opts.userTerminalHint || process.env.SHELL || "/bin/sh",
...opts
})
));
Note: The shell name mapping already recognizes nushell (nu -> "nushell"), confirming it was anticipated but never wired up for execution.
Note: The IDE agent has the same shell library bug but a different input path – filed separately.