Skip to content

Windows: Git Bash cannot exec backslash-style Windows paths, breaks every wrapper-script stdio server #1318

Description

@LocoLoboZ

Summary

On Windows, when the login shell resolves to Git Bash / MSYS ($SHELL set, or otherwise detected as bash-like), shellwrap.WrapWithUserShell correctly single-quotes a backslash-style Windows path (e.g. C:\ProgramData\foo\bar.cmd), but Git Bash still cannot execute it. MSYS's exec layer only resolves POSIX-style paths (C:/ProgramData/foo/bar.cmd); a backslash-style path falls through to bash's PATH lookup, which fails with command not found and — in its own error rendering — drops the backslashes entirely, producing a confusing message like:

/usr/bin/bash: line 1: C:ProgramDataQalatCybermcpproxy-wrappersnexusmods-mcp.cmd: command not found

This affects every stdio server whose command is an absolute Windows path (wrapper .cmd/.bat scripts are the common case) on any host where the resolved login shell is bash-like — i.e. any Windows box with Git Bash and $SHELL set, which is common for anyone using Git Bash / VS Code's integrated terminal / MSYS2 as their default shell.

Repro

  1. On Windows, with $SHELL set to a Git Bash path (e.g. C:\Program Files\Git\bin\bash.exe), configure a stdio server with an absolute Windows-path command, e.g.:
    { "command": "C:\ProgramData\SomeVendor\wrappers\foo.cmd" }
  2. Start mcpproxy and watch it try to connect. It fails every retry with:
    failed to connect: stdio transport (command="C:\Program Files\Git\bin\bash.exe", ...):
    server process exited before completing the MCP initialize handshake; recent stderr:
    /usr/bin/bash: line 1: C:ProgramDataSomeVendorwrappersfoo.cmd: command not found
    
  3. Confirmed with a minimal Go reproduction calling shellwrap.WrapWithUserShell directly and then exec.Command(shell, args...) on the result — the quoting is correct (backslashes are preserved inside the single-quoted string), but the resulting bash -l -c '...' invocation still fails to exec, proving the bug is in MSYS's own path resolution, not in the Go-side quoting.

Root cause

internal/shellwrap/shellwrap.go, WrapWithUserShell: when the resolved shell is bash-like (isBashLikeShell(shell) is true) and runtime.GOOS == "windows", the command and args are still shell-escaped and joined verbatim, backslashes and all. MSYS/Git-Bash's exec() implementation does not translate a backslash-style absolute Windows path to POSIX form before attempting to run it, so the exec attempt silently fails and falls through to a PATH-name lookup, which is what produces the garbled command not found message.

Fix

Convert backslashes to forward slashes in the command and each argument before shell-escaping, but only on the Windows + bash-like-shell branch (Windows itself accepts forward-slash paths interchangeably with backslash ones via CreateProcess, and MSYS accepts them natively, so this is safe in both directions).

// Git Bash / MSYS on Windows cannot exec a backslash-style Windows path
// (C:\ProgramData\...) even when it is correctly single-quoted: MSYS's
// own exec layer only resolves POSIX-style paths, so it falls through to
// bash's PATH lookup, which reports "command not found" using its own
// mangled rendering of the argv word (backslashes silently dropped). A
// forward-slash path (C:/ProgramData/...) is accepted by both Windows'
// CreateProcess and MSYS's exec layer, so convert before quoting when
// we're about to run a bash-like shell on Windows.
toBashPath := func(s string) string {
    if runtime.GOOS == osWindows && isBash {
        return strings.ReplaceAll(s, `\`, "/")
    }
    return s
}

Verified locally:

  • Unit test reproducing the original bug (single-quoted backslash path still fails to exec through real Git Bash — exit status 127, command not found, backslashes dropped from bash's own error message).
  • Unit test proving the fix (bash -l -c '<forward-slash form>' successfully execs a real Windows binary, e.g. whoami.exe, through the wrapped path).
  • Full internal/shellwrap and internal/upstream/... test suites pass after the change; one existing Docker-isolation test (TestSetupDockerIsolationShellWrapsWhenDaemonEnvMissingNonDarwin) needed its assertion updated to accept either separator style, since the Docker shell-wrap fallback path goes through the same code and was hitting the identical latent bug.
  • End-to-end: rebuilt mcpproxy with the fix and reconnected a real config with ~14 wrapper-.cmd-based stdio servers that were all failing with this exact error — all connected successfully afterward (connected server count went from 13 to 27, tool count from 249 to 403).

Happy to open a PR with this change (plus the updated test) if useful — flagging as an issue first in case there's a preferred approach or an existing fix already in flight.

Environment

  • OS: Windows 11 Pro
  • mcpproxy-go: v0.67.0
  • Shell: Git Bash ($SHELL set to Git's bash.exe)

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions