Conversation
WrapWithUserShell correctly single-quotes a backslash-style Windows path (e.g. C:\ProgramData\foo\bar.cmd) when the resolved login shell is bash-like, but Git Bash / MSYS still cannot execute it: MSYS's own exec layer only resolves POSIX-style paths. A backslash path falls through to bash's PATH lookup, which fails with "command not found" and mangles the path in its own error rendering (backslashes silently dropped). This breaks every stdio server configured with an absolute Windows-path command (wrapper .cmd/.bat scripts are the common case) on any Windows host where the resolved shell is bash-like, e.g. Git Bash / VS Code integrated terminal / MSYS2 set as $SHELL. Fix: convert backslashes to forward slashes in the command and args before shell-escaping, but only on the Windows + bash-like-shell branch. Windows' CreateProcess and MSYS's exec layer both accept forward-slash paths, so this is safe in both directions. Verified against a real config with 14 wrapper-.cmd-based stdio servers that were all failing with this exact error: connected server count went from 13 to 27, tool count from 249 to 403, after rebuilding with this fix. Fixes smart-mcp-proxy#1318
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Dumbris
left a comment
There was a problem hiding this comment.
Thanks for the detailed writeup and the manual verification with 14 wrapper-.cmd servers — that's a great regression to have caught and a clear repro. The fix direction (stop keying the quoting dialect on GOOS and instead ask "what shell will actually parse this") is the right one.
I did find a few things worth another pass before merging, mostly around how broadly the new backslash→forward-slash conversion applies:
-
toBashPathrewrites backslashes in every arg, not just paths (internal/shellwrap/shellwrap.goaround the new closure). It's applied unconditionally tocommandand every element ofargswhenever GOOS=windows and the shell is bash-like, with no check that the string is actually a Windows path. That'll also mangle non-path values that happen to contain a literal\— e.g. a SQL Server instance name like.\SQLEXPRESS, aDOMAIN\usercredential, a regex (\d+\.log), or a Docker-e KEY=VALUEvalue injected viainjectEnvVarsIntoDockerArgs— silently, with no error. Might be worth scoping the rewrite to things that look like Windows paths (e.g.^[A-Za-z]:\\or UNC\\) rather than any string containing\. -
The new regression test may not exercise the fix it's meant to guard.
TestWrapWithUserShell_GitBashCanExecWindowsPathnever sets$SHELLbefore callingWrapWithUserShell, andresolveLoginShell()reads$SHELLfirst — so on a Windows box/runner where$SHELLisn't set, it silently falls through to thecmd.exebranch instead of the Git Bash one, and the test would pass without ever touching the code path it's testing. On top of that,.github/workflows/unit-tests.ymlonly runswindows-lateston push, not onpull_request— so as written this test can't gate a PR either way. Could you addt.Setenv("SHELL", bashPath)and see if the Windows job can run on PRs (or is skipped intentionally for cost reasons)? -
Smaller / lower-confidence, worth a look:
isBashLikeShell'sstrings.Contains(lower, "sh")also matchespwsh.exe,fish,csh/tcsh. It previously only picked argv flags (-l -cvs/c); now it also gates the quoting dialect and the backslash rewrite, so a false-positive there (e.g.$SHELL=pwsh.exe) would now silently mis-quote/mis-convert rather than just fail loudly.insertCidfileIntoShellDockerCommand's literal"docker run"substring match (internal/upstream/core/connection_docker.go) breaks once the docker path is quoted — which happens for the very common Docker Desktop default install pathC:\Program Files\Docker\.... Pre-existing, but directly hit by this PR's target scenario, so flagging it here.- The public
Shellescape()still keys its dialect on bareruntime.GOOS, unaware ofisBash— its doc comment ("mirrors... so both code paths can converge") is now a bit stale, and a future caller of it could reintroduce this exact bug.
None of this is a knock on the diagnosis or the manual verification — the root cause and the general direction look right to me. Requesting changes mainly for #1 (the unscoped rewrite) and #2 (test coverage), since those affect correctness/confidence in the fix itself. Happy to take another look once addressed!
Summary
WrapWithUserShellcorrectly single-quoted an absolute Windows path but Git Bash still couldn't execute it — MSYS's exec layer only resolves POSIX-style paths, so a backslash path falls through to bash'sPATHlookup and fails withcommand not found(mangling the path in its own error output)..cmd/.batscripts are the common case) whenever$SHELLresolves to a bash-like shell on Windows — a common setup for anyone using Git Bash / MSYS2 / VS Code's integrated terminal as their default shell.CreateProcessand MSYS's exec layer accept forward-slash paths.TestSetupDockerIsolationShellWrapsWhenDaemonEnvMissingNonDarwin, which exercises the same shell-wrap code path for the Docker isolation fallback and was hitting the identical latent bug — its assertion now accepts either separator style.TestWrapWithUserShell_GitBashCanExecWindowsPath, a Windows-only regression test that actually execs a real binary (whoami.exe) through the wrapped Git Bash command, so a regression here fails loudly instead of only failing a string-quoting assertion.Test plan
go test ./internal/shellwrap/...— pass, including the new regression testgo test ./internal/upstream/...— pass (all subpackages)go build ./...— cleangofmt -lon changed files — cleango vet ./internal/shellwrap/... ./internal/upstream/core/...— cleanmcpproxywith this change and reconnected a real config with 14 wrapper-.cmd-based stdio servers that were all failing with this exact error before the fix. All connected afterward — connected server count went from 13 to 27, tool count from 249 to 403.