Skip to content

Port: respect $SHELL on Windows when spawning child processes #46

Closed

Description

@JohnnyVicious

Port of openai/codex-plugin-cc#178 (merged)

Problem

On Windows, Node's child_process.spawn with shell: true uses COMSPEC (cmd.exe) by default. When the user runs under Git Bash or another POSIX-ish shell, commands that work in their interactive shell fail inside the plugin because the plugin spawned via cmd.exe instead.

plugins/opencode/scripts/lib/process.mjs::runCommand and resolveOpencodeBinary both use spawn("opencode"|"which", ...) without a shell: option at all today. This is different from the upstream issue shape — upstream has shell: process.platform === "win32" which picks cmd. Opencode has no shell, which means on Windows it can't find opencode.cmd, git.cmd, gh.cmd, etc. at all.

Both sides of the issue apply to us:

  1. On Windows we need shell: true so .cmd shims resolve.
  2. When we do pass shell: true, we should honor $SHELL for Git Bash users.

Fix (adapted from upstream)

lib/process.mjs::runCommand:

export function runCommand(cmd, args, opts = {}) {
  return new Promise((resolve) => {
    const shellOpt =
      process.platform === "win32"
        ? (process.env.SHELL || true)  // respect Git Bash if present, else cmd
        : false;

    const proc = spawn(cmd, args, {
      stdio: ["ignore", "pipe", "pipe"],
      cwd: opts.cwd,
      env: { ...process.env, ...opts.env },
      shell: shellOpt,
      windowsHide: true,
    });
    // ... rest unchanged ...
  });
}

lib/process.mjs::resolveOpencodeBinary:

which opencode doesn't work on Windows (no which binary). Use where on win32 and handle the .cmd shim:

export async function resolveOpencodeBinary() {
  return new Promise((resolve) => {
    const isWin = process.platform === "win32";
    const locator = isWin ? "where" : "which";
    const proc = spawn(locator, ["opencode"], {
      stdio: ["ignore", "pipe", "ignore"],
      shell: isWin ? (process.env.SHELL || true) : false,
      windowsHide: true,
    });
    let out = "";
    proc.stdout.on("data", (d) => (out += d));
    proc.on("close", (code) => {
      if (code !== 0) return resolve(null);
      // `where` returns all matches; take the first.
      resolve(out.trim().split(/\r?\n/)[0] ?? null);
    });
  });
}

lib/opencode-server.mjs::ensureServer already uses stdio: "ignore" + detached: true for opencode serve, but it still uses spawn("opencode", ...) without a shell option. Apply the same shellOpt fix there so .cmd shims resolve on Windows.

lib/process.mjs::spawnDetached — used by the background-task worker path — needs the same treatment:

export function spawnDetached(cmd, args, opts = {}) {
  const shellOpt =
    process.platform === "win32" ? (process.env.SHELL || true) : false;
  const child = spawn(cmd, args, {
    stdio: "ignore",
    detached: true,
    cwd: opts.cwd,
    env: { ...process.env, ...opts.env },
    shell: shellOpt,
    windowsHide: true,
  });
  child.unref();
  return child;
}

Test plan

  1. On Windows with Git Bash installed, set SHELL=C:\Program Files\Git\bin\bash.exe, run /opencode:setup. Assert it locates opencode.cmd and reports the version.
  2. On Windows without Git Bash, unset SHELL, run /opencode:setup. Assert cmd-shell fallback still works.
  3. On POSIX (Linux/macOS), assert behavior is unchanged — the shell: false branch kicks in.
  4. Confirm git and gh calls from lib/git.mjs (via runCommand) also find their .cmd shims on Windows.

Upstream reference

openai/codex-plugin-cc#178 (merged 2026-04-08). Related upstream context: #55 / #13 / #70 (Windows ENOENT cluster).

Activity

  1. JohnnyVicious commented on Apr 12, 2026

    @JohnnyVicious
    OwnerAuthor

    Consolidated into #28. Windows SHELL/spawn behavior belongs with the same portable process spawning policy.

  2. added 2 commits that reference this issue on Apr 12, 2026
    f8d3fcc
    e6a18bc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions