Skip to content

session terminal ignores the "shell" config option (always PowerShell on Windows) #51051

Open

Description

@Tsuki-hash

Description

packages/web/src/content/docs/config.mdx documents the shell option as configuring "the shell used for the interactive terminal". The agent's shell tool honors it; session terminals do not — they resolve through a path that reads only process.env.SHELL, which is normally unset on Windows, so on Windows a session terminal starts PowerShell whatever you put in shell.

Visible symptom: with "shell": "C:\\Program Files\\Git\\bin\\bash.exe" in opencode.json, an agent command runs in Git Bash, while opening a terminal in the same session gives PowerShell. Two shells, one setting. On macOS/Linux the same divergence appears whenever $SHELL differs from the configured value.

Code path on v2:

  • packages/core/src/persistent-pty/index.ts:160 — program: input.command ?? ShellSelect.environment(global.bin)
  • packages/core/src/shell/select.ts:172-174 — environment() passes undefined in the options slot that the other entry points fill, and starts from process.env.SHELL; select.ts:121 then returns win(options, bin)[0], which puts pwsh/powershell first.
  • Compare packages/core/src/pty.ts:166 and packages/core/src/shell.ts:263, both of which do yield* shell.resolve({ priority: "config" }).
  • The client sends no command of its own: packages/tui/src/context/session-terminals.tsx:78-86.

PersistentPty is a process-global node while ShellSelect.Service is Location-scoped, which is likely why the free function was used here. Resolving in the persistentPty.create handler and passing the result down as command keeps that layering; packages/server/src/handlers/config.ts:21 already yields ShellSelect.Service from a handler, and routes.ts:130 is where the configured options enter the graph.

Plugins

None.

OpenCode version

Source read at v2 tip 318a8c1; published @opencode/cli 2.0.15 carries the same code.

Steps to reproduce

  1. Windows, set "shell" in opencode.json to a Git Bash path.
  2. Open a session terminal from the TUI and run $PSVersionTable — it answers, so the terminal is PowerShell.
  3. Ask the agent to run $PSVersionTable through the shell tool — it does not, because that one runs in the configured shell.

Stated plainly so nobody wastes review time: the above comes from reading the call chain, I have not recorded a session, so steps 2 and 3 describe what should happen rather than captured output.

Operating System

Windows 11 build 26200, x64, native (not WSL).

Terminal

Windows Terminal.

Activity

  1. ggbdpq commented on Oct 1, 2026

    @ggbdpq

    I'd like to work on this.

    Confirmed the diagnosis is still accurate on the current v2 tip (dd786c62af): PersistentPty.Service.create (packages/core/src/persistent-pty/index.ts) still falls back to the free function ShellSelect.environment(global.bin), which reads only process.env.SHELL and drops the options slot, so the configured shell never reaches session terminals. Every other entry point (pty.ts, shell.ts, command execution) already resolves through ShellSelect.Service.resolve({ priority: "config" }).

    Plan: in PersistentPtyHandler's persistentPty.create handler, resolve the configured shell via ShellSelect.Service (same layering handlers/config.ts already uses) and pass it down as command when the payload omits one, keeping PersistentPty itself process-global as suggested. Will include a regression test that fails before the change.

  2. opencodejr commented on Oct 28, 2026

    @opencodejr

    Thanks for the careful write-up. I reproduced this on Linux with 2.0.15, using the same path you traced. The script, screenshot and recording are attached below.

    Setup: "shell": "/bin/dash" in the project config, with SHELL=/bin/bash.

    • The agent's shell tool ran echo "SHELL_EXE=$(readlink /proc/$$/exe)" in dash, which is the configured shell.
    • A session terminal opened in the same session (ctrl+x t) printed bash, so it fell back to $SHELL and ignored the config.

    It still happens on 2.2.0, the latest release. I couldn't run current dev from source, but the code there (fa1260d) is unchanged: persistent-pty/index.ts still falls back to ShellSelect.environment(), which only reads $SHELL. On Windows, where SHELL is usually unset, that gives you PowerShell, as you described.

    Related: #51994 (terminal shell settings shared between Local and WSL servers) and #57310 (gitbash() falling back to pwsh).

    recording.mp4

    Screenshot

    Reproduction script (drive.ts)
    // Repro for anomalyco/opencode#51051: session terminal ignores the `shell` config option.
    // Configured shell: /bin/dash. Ambient $SHELL: /bin/bash.
    // The agent's shell tool runs in dash; the session terminal opens bash.
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    process.env.SHELL = "/bin/bash"
    const probe = 'echo "SHELL_EXE=$(readlink /proc/$$/exe)"'
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        config: { shell: "/bin/dash" },
        project: { git: true },
        tui: { recording: true },
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          // 1. Agent shell tool (real implementation) -> configured shell
          yield* llm.queue(
            Llm.toolCall({ index: 0, id: "call_probe", name: "shell", input: { command: probe, description: "probe shell" } }),
            Llm.finish("tool-calls"),
          )
          yield* llm.queue(Llm.text("Done probing."))
          yield* ui.submit("Which shell runs your commands?")
          yield* ui.waitFor("Done probing.", { timeout: 30000 })
          yield* Effect.sleep(1000)
          yield* ui.screenshot("agent-shell")
    
          // 2. Session terminal -> should also be the configured shell
          yield* ui.press("x", { ctrl: true })
          yield* ui.press("t")
          yield* Effect.sleep(3000)
          yield* ui.type(probe)
          yield* ui.enter()
          yield* Effect.sleep(3000)
          yield* ui.screenshot("session-terminal")
          const frame = yield* ui.capture()
          yield* Effect.log(JSON.stringify(frame).match(/SHELL_EXE=[^"\\ ]*/g))
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  3. Tsuki-hash commented on Oct 28, 2026

    @Tsuki-hash
    Author

    Thanks for the detailed reproduction — this confirms the issue is still live on 2.2.0.

    For reference, #51052 (fix(server): resolve the configured shell for session terminals) implements exactly this fix: session terminals resolve the configured shell instead of falling back to $SHELL. It was closed by the automated cleanup (age + reaction count), not on its merits, and it still merges cleanly against the current dev tip.

    Would a maintainer be able to reopen #51052, or would you prefer I open a fresh PR? Happy to do either.

  4. WuJunkai2004 commented on Nov 3, 2026

    @WuJunkai2004

    why this issues is created in Sep, But in whole Oct, the shell in web works very well? At least in 2.2.2, the terminal I opened in webui, it is followed my setting(git-bash in win).

    But just Now I upgrade opencode to 2.2.5, and find it change back to pwsh :(

    from ai:
    The regression window is 2.2.3 → 2.2.4: PR #60346 (commit ef712d3) moved the desktop terminal to persistent PTYs, and that path resolves the shell via ShellSelect.environment() which ignores the shell config (#51051's root cause).

  5. added a commit that references this issue on Nov 3, 2026
    ba936f3
  6. Tsuki-hash commented on Nov 3, 2026

    @Tsuki-hash
    Author

    @WuJunkai2004 Thanks for the report — this matches the root cause of this issue. #60346 moved the desktop terminal to persistent PTYs, and that path resolves the shell through ShellSelect.environment(), which only reads $SHELL and ignores the configured shell. So the 2.2.5 regression you are seeing is the same bug surfacing on the new path, not a different one.

    For reference, #51052 (fix(server): resolve the configured shell for session terminals) implemented exactly this fix: session terminals resolve the configured shell instead of falling back to $SHELL, and the same approach applies to the persistent PTY path. That PR was closed by the automated cleanup (age + reaction count), not on its merits, and its branch is no longer available — so if maintainers would like this, I am happy to resubmit it as a fresh PR against current dev. Just say the word.

  7. WuJunkai2004 commented on Nov 3, 2026

    @WuJunkai2004

    @Tsuki-hash thank you. I try to fix this and take a PR for the issue. It can fix in the new version

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions