Agent CLI on Windows: No way to configure shell (hardcoded to PowerShell, no --shell flag or config option)

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

  1. Install Agent CLI on Windows: irm 'https://cursor.com/install?win32=true' | iex
  2. Run agent about – note it reports Shell: cmd and Terminal: unknown
  3. Ask the agent to run any command – observe it uses PowerShell
  4. Set $env:SHELL = "C:\Users\...\nu.exe" and retry – still uses PowerShell
  5. There is no --shell flag and cli-config.json has no shell field

Expected Behavior

The Agent CLI should support configuring which shell is used for command execution, either via:

  • A --shell CLI flag: agent --shell "C:\path\to\nu.exe"
  • A shell.path field in ~/.cursor/cli-config.json
  • Respecting the $SHELL environment 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 running dump_bash_state / dump_zsh_state after each command. Its execute() method always spawns Bash/Zsh specifically. Constructed with (cwd, state, userTerminalHint, useFileStateTransport).
  • NaiveTerminalExecutor: A generic, shell-agnostic executor. Its execute() method spawns options.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:

  1. CLI has no config path for shell: userTerminalHint is never set – cli-config.json has no shell field, there’s no --shell flag, and process.env.SHELL is unset on Windows. So detectShellType("") is called, which always falls to PowerShell.
  2. Detection function ignores non-standard shells: Even if userTerminalHint were set to a nushell path, there’s no includes("nu") check. Worse, on Windows the PowerShell ternary arm combines both string matching and a system-level commandExists fallback in one || chain. Since PowerShell is always installed, this condition evaluates to true regardless of the hint string – any shell check placed after this point is dead code on Windows.
  3. No Naive case in executor factory: Even if detection returned ShellType.Naive, the factory switch has no case for it.

Proposed fix:

  1. Add shell.path to cli-config.json schema and/or a --shell CLI flag, wired to userTerminalHint
  2. In detectShellType(), add non-standard shell recognition before the PowerShell condition (critical – placing it after is unreachable on Windows due to the combined includes || commandExists condition):
  : 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 */
  1. Add the missing ShellType.Naive case to the executor factory using NaiveTerminalExecutor (NOT BashStateExecutor – 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.

1 Like

Hey, thanks for the detailed report.

This is definitely a bug. The CLI configuration schema (docs) really doesn’t include a shell.path field, and the detection logic ends up picking PowerShell on Windows, just like you described.

I’ve shared this with the team along with your IDE report. Your code analysis will be really helpful for fixing it. I don’t have an ETA yet, but your detailed breakdown helps us prioritize.

I’ll update here if there’s any news.

When will you have the ETA?

Same root cause on the IDE side. My default terminal is Git Bash and it works for me when I use the integrated terminal — I use it every day. The IDE agent does not use the same shell; it uses PowerShell or tries WSL instead of respecting terminal.integrated.defaultProfile.windows. I’ve added my details to the IDE thread (IDE Agent ignores terminal.integrated.defaultProfile.windows). Fixing the shared shell detection to respect the user’s profile / hint would fix both CLI and IDE for users who want Git Bash (or other shells) on Windows.