Deep Review: 20260424-190657-pr-4885

Date2026-04-24 19:06
Repolima-vm/lima
Round1
Author@jandubois
PR#4885 — Windows: support native OpenSSH; no Git for Windows / MSYS2 required
Branchwindows-native-ssh-no-cygpath
Commitse147b971 CI: read symlink targets from git object store
bae5d205 CI: install templates for windows-plain-qemu
1218a5d1 CI: split windows-plain by driver, add reverse-sshfs round-trip
6e599d6e remove LIMAWINDOWSEXTRAPATH
7474e1e1 CI: build the Linux guest agent and fail-loudly in windows-plain
3e09daeb golangci: disable nolintlint
f21ba237 client: silence flaky nolintlint at the grpc.Dial directive (reverted)
d720cd4e ioutilx: drop unnecessary else after returning if branch
07328ee2 add diagnosability for plain-Windows path
9858c839 docs: reflect that plain Windows is now a supported host
1f8a5303 CI: stop forcing Git for Windows on PATH; add plain-Windows smoke test
8e56f7e7 ioutilx, hostagent: support reverse-sshfs on plain Windows
df59f8ff downloader: decompress gzip in pure Go
6924004a copytool: support native Windows OpenSSH
bc99aca8 sshutil: detect Win32-OpenSSH version, export PathForSSH
ce3a0696 sshutil: skip cygpath on Windows when ssh is native OpenSSH
ReviewersClaude Opus 4.7 (effort: xhigh), Codex GPT 5.5 (effort: xhigh), Gemini 3.1 Pro (effort: default)
VerdictMerge with fixes — two real gaps in the new Windows-SSH logic (rsync probe and parseCopyPaths drive-letter detection); otherwise the design and testing are solid for a research PR.
Wall-clock time19 min 30 s


Executive Summary

This PR makes limactl usable on plain Windows 10/11 hosts by teaching sshutil to dispatch between native OpenSSH and Cygwin-based OpenSSH, stripping SSH mux options on Windows, decompressing gzip in-process, and adding two "plain Windows" CI jobs (one per driver). The refactor is scoped well and the research-PR framing is honest about what it does and does not cover.

Two real issues surfaced. The new rsync/scp mux-stripping is applied in rsync.Command and scp.Command but was omitted from the checkRsyncOnGuest probe, so an installed-but-gated rsync can be rejected on native Windows OpenSSH (I1). The drive-letter check in parseCopyPaths uses filepath.VolumeName instead of filepath.IsAbs, which silently breaks limactl copy for users whose instance is named with a single letter like C (I2) — a regression from the pre-PR code that used filepath.IsAbs.

The remaining items are documentation, logging, and defensive-coding suggestions. The author's DO NOT MERGE framing is appropriate for a research PR; the issues above are small enough to address in a follow-up before merging for real.

Structure: 0 Critical / 2 Important / 10 Suggestions.


Critical Issues

None.


Important Issues

I1. checkRsyncOnGuest does not strip SSH mux options on Windows Codex Claude
	}

	return true
}

func checkRsyncOnGuest(ctx context.Context, inst *limatype.Instance) bool {
	sshExe, err := sshutil.NewSSHExe()
	if err != nil {
		logrus.Debugf("failed to create SSH executable: %v", err)
		return false
	}
	sshOpts, err := sshutil.SSHOpts(ctx, sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)
	if err != nil {
		logrus.Debugf("failed to get SSH options for rsync check: %v", err)
		return false
	}

	sshArgs := append([]string{}, sshExe.Args...)
	sshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...)
	checkCmd := exec.CommandContext(ctx, sshExe.Exe, sshArgs...)
	checkCmd.Args = append(checkCmd.Args,
		"-p", fmt.Sprintf("%d", inst.SSHLocalPort),
		*inst.Config.User.Name+"@"+inst.SSHAddress,
		"command -v rsync >/dev/null 2>&1",
	)

	err = checkCmd.Run()
	return err == nil
}

func (t *rsyncTool) Command(ctx context.Context, paths []string, opts *Options) (*exec.Cmd, error) {
	copyPaths, err := parseCopyPaths(ctx, paths)
	if err != nil {
		return nil, err

rsyncTool.Command at pkg/copytool/rsync.go:131-138 and scpTool.Command at pkg/copytool/scp.go:125-134 both call sshutil.SSHOptsRemovingControlPath on Windows. The probe at pkg/copytool/rsync.go:67 builds the same sshOpts but skips that step, so the probe inherits ControlMaster=auto, ControlPath=…, and ControlPersist=yes from SSHOpts.

				}
				sshOpts, err := sshutil.SSHOpts(ctx, sshExe, cp.Instance.Dir, *cp.Instance.Config.User.Name, false, false, false, false)
				if err != nil {
					return nil, err
				}
				if runtime.GOOS == "windows" {
					// See the equivalent comment in scp.go. Strip mux options
					// so rsync's ssh transport does not try to use a
					// ControlMaster socket that is unavailable or unreliable
					// on Windows.
					logrus.Debug("rsync: stripping ControlMaster/ControlPath/ControlPersist (Windows)")
					sshOpts = sshutil.SSHOptsRemovingControlPath(sshOpts)
				}

				sshArgs := []string{sshExe.Exe}
				sshArgs = append(sshArgs, sshExe.Args...)
				sshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...)
				sshArgs = append(sshArgs, "-p", fmt.Sprintf("%d", cp.Instance.SSHLocalPort))
		sshOpts, err = sshutil.CommonOpts(ctx, sshExe, false)
		if err != nil {
			return nil, err
		}
	}
	if runtime.GOOS == "windows" {
		// Strip ControlMaster/ControlPath/ControlPersist so that scp invokes
		// ssh without trying to use SSH connection multiplexing. Native
		// Windows OpenSSH does not support multiplexing (Unix-domain mux
		// socket), and Cygwin-based ssh has known reliability issues with
		// sftp over a mux socket. See also the equivalent handling in
		// hostagent and limactl shell.
		logrus.Debug("scp: stripping ControlMaster/ControlPath/ControlPersist (Windows)")
		sshOpts = sshutil.SSHOptsRemovingControlPath(sshOpts)
	}
	sshArgs := sshutil.SSHArgsFromOpts(sshOpts)

	return exec.CommandContext(ctx, t.toolPath, append(sshArgs, scpArgs...)...), nil
}

Native Windows OpenSSH does not support Unix-domain mux sockets (PR description). Cygwin-based ssh has documented reliability issues over a mux socket (pkg/copytool/scp.go:126-131). The probe therefore hits the same failure mode the Command method works around. When a user installs rsync on a plain-Windows host (e.g., via winget), the probe fails before running command -v rsync on the guest, and BackendAuto falls back to scp while BackendRsync reports "rsync not available on guest(s)". go test ./pkg/copytool/... does not cover this path because CI does not install rsync on the Windows runners.

		if err != nil {
			return nil, err
		}
	}
	if runtime.GOOS == "windows" {
		// Strip ControlMaster/ControlPath/ControlPersist so that scp invokes
		// ssh without trying to use SSH connection multiplexing. Native
		// Windows OpenSSH does not support multiplexing (Unix-domain mux
		// socket), and Cygwin-based ssh has known reliability issues with
		// sftp over a mux socket. See also the equivalent handling in
		// hostagent and limactl shell.
		logrus.Debug("scp: stripping ControlMaster/ControlPath/ControlPersist (Windows)")
		sshOpts = sshutil.SSHOptsRemovingControlPath(sshOpts)
	}
	sshArgs := sshutil.SSHArgsFromOpts(sshOpts)

Fix: apply the same mux-stripping in checkRsyncOnGuest:

 sshOpts, err := sshutil.SSHOpts(ctx, sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)
 if err != nil {
     logrus.Debugf("failed to get SSH options for rsync check: %v", err)
     return false
 }
+if runtime.GOOS == "windows" {
+    sshOpts = sshutil.SSHOptsRemovingControlPath(sshOpts)
+}

 sshArgs := append([]string{}, sshExe.Args...)
 sshArgs = append(sshArgs, sshutil.SSHArgsFromOpts(sshOpts)...)
I2. parseCopyPaths drive-letter check shadows single-letter instance names Gemini
		cp := &Path{}
		// On Windows, detect local drive-letter paths (e.g. C:\...) before
		// splitting on ":" so the drive letter is not mistaken for an instance
		// name. filepath.VolumeName returns "C:" for local drive paths and ""
		// for instance:path forms like "nat:/tmp/file".
		isLocalAbs := runtime.GOOS == "windows" && filepath.VolumeName(path) != ""
		if isLocalAbs {
			sshExe, err := sshutil.NewSSHExe()
			if err != nil {
				return nil, err
			}

On Windows filepath.VolumeName("C:foo.txt") returns "C:" (drive-relative paths still carry a volume). The pre-PR code at the merge-base used filepath.IsAbs(path), which correctly returns false for "C:foo.txt" and routes the string into the colon-split where it is recognised as a remote path on instance C. After this change, limactl copy C:foo.txt . on an instance named C is mis-parsed as a local Windows path and runs through PathForSSH instead of being routed to the guest.

Single-letter instance names are rare but valid, and the failure is silent (cygpath will happily translate the bogus path). The fix restores pre-PR behaviour for the instance-name case while keeping the intended bypass for C:\foo and C:/foo, which filepath.IsAbs accepts on Windows. UNC paths also pass filepath.IsAbs and therefore remain routed through PathForSSH, matching the pre-PR behaviour.

Fix:

-isLocalAbs := runtime.GOOS == "windows" && filepath.VolumeName(path) != ""
+isLocalAbs := runtime.GOOS == "windows" && filepath.IsAbs(path)

A unit test on parseCopyPaths with C:\foo, C:/foo, C:foo, and instance:path would lock the intended branching in place.


Suggestions

S1. IsSSHCygwin uses a process-global sync.Once tied to the first caller's sshExe Claude
	sshExe.Exe = executable

	return sshExe, nil
}

var cygwinDetect struct {
	sync.Once
	isCygwin bool
}

// IsSSHCygwin reports whether sshExe is a Cygwin-based build of OpenSSH
// (e.g. Git for Windows, MSYS2, or Cygwin itself), as opposed to native
// Windows OpenSSH. The detection is based on whether cygpath.exe lives in
// the same directory as ssh.exe, which is the layout used by Git for Windows
// and MSYS2. Always returns false on non-Windows.
//
// The first call's result is cached and logged at Debug level. Subsequent
// calls return the same answer regardless of the sshExe passed; in practice
// limactl uses a single ssh binary throughout a process, so this is fine
// and avoids spamming the log when toolchain-aware code paths run repeatedly.
func IsSSHCygwin(sshExe SSHExe) bool {
	if runtime.GOOS != "windows" || sshExe.Exe == "" {
		return false
	}
	cygwinDetect.Do(func() {
		path := sshExe.Exe
		if !filepath.IsAbs(path) {
			resolved, err := exec.LookPath(path)
			if err != nil {
				logrus.WithError(err).Debugf("IsSSHCygwin: cannot resolve %q via PATH; assuming native", sshExe.Exe)
				return
			}
			path = resolved
		}
		cygpathPath := filepath.Join(filepath.Dir(path), "cygpath.exe")
		if _, err := os.Stat(cygpathPath); err == nil {
			cygwinDetect.isCygwin = true
			logrus.Debugf("ssh at %q detected as Cygwin-based (found %q alongside)", path, cygpathPath)
		} else {
			logrus.Debugf("ssh at %q detected as native Windows OpenSSH (no cygpath.exe alongside)", path)
		}
	})
	return cygwinDetect.isCygwin
}

// PathForSSH converts orig to the path form expected by the ssh family of
// binaries (ssh, ssh-keygen, scp, sftp, rsync-over-ssh) that Lima will invoke.
// On non-Windows the path is returned unchanged. On Windows:
//   - For Cygwin-based ssh (Git for Windows, MSYS2): runs cygpath to produce a

The docstring documents this: "Subsequent calls return the same answer regardless of the sshExe passed". The concern is blast radius, not correctness in the current flow. A unit test that wants to exercise both the Cygwin and native branches of PathForSSH in one binary cannot do so, and any future caller that resets SSH mid-process would silently keep the old detection result.

The cache in openSSHInfos at pkg/sshutil/sshutil.go:479-481 already uses map[sshExecutable]*openSSHInfo guarded by an sync.RWMutex. Mirror that pattern for cygwin detection: key by the resolved absolute ssh path and drop sync.Once. The change is ~10 lines and removes the "first call wins" subtlety.

	ModTime time.Time
}

var (
	// openSSHInfos caches the parsed version and supported options of each ssh executable, if it is needed again.
	openSSHInfos   = map[sshExecutable]*openSSHInfo{}
	openSSHInfosRW sync.RWMutex
)

func detectOpenSSHInfo(ctx context.Context, sshExe SSHExe) openSSHInfo {
	var (
		info   openSSHInfo
		exe    sshExecutable
S2. Preserve the mount-home skip rationale after removal Claude
 case "$NAME" in
 "default")
-    # CI failure:
-    # "[hostagent] failed to confirm whether /c/Users/runneradmin [remote] is successfully mounted"
-    [ "${OS_HOST}" = "Msys" ] && CHECKS["mount-home"]=
     [ "${OS_HOST}" = "Darwin" ] && CHECKS["ssh-over-vsock"]="1"

The PR description frames re-enabling this check as a probe to see whether the path-translation fixes also resolve the pre-existing Msys-side failure. Removing the explanatory comment along with the skip means a future reader who hits the same failure has to reconstruct the history via git blame. Keep the comment as # Was previously skipped on Msys; see PR #4885 so the context survives the next regression.

S3. Resolve symlinks before looking for cygpath.exe Claude
// and avoids spamming the log when toolchain-aware code paths run repeatedly.
func IsSSHCygwin(sshExe SSHExe) bool {
	if runtime.GOOS != "windows" || sshExe.Exe == "" {
		return false
	}
	cygwinDetect.Do(func() {
		path := sshExe.Exe
		if !filepath.IsAbs(path) {
			resolved, err := exec.LookPath(path)
			if err != nil {
				logrus.WithError(err).Debugf("IsSSHCygwin: cannot resolve %q via PATH; assuming native", sshExe.Exe)
				return
			}
			path = resolved
		}
		cygpathPath := filepath.Join(filepath.Dir(path), "cygpath.exe")
		if _, err := os.Stat(cygpathPath); err == nil {
			cygwinDetect.isCygwin = true
			logrus.Debugf("ssh at %q detected as Cygwin-based (found %q alongside)", path, cygpathPath)
		} else {
			logrus.Debugf("ssh at %q detected as native Windows OpenSSH (no cygpath.exe alongside)", path)
		}
	})
	return cygwinDetect.isCygwin
}

// PathForSSH converts orig to the path form expected by the ssh family of
// binaries (ssh, ssh-keygen, scp, sftp, rsync-over-ssh) that Lima will invoke.

exec.LookPath returns the path as it appears in PATH. If that entry is a symlink or a shim (chocolatey, scoop, user shortcut), filepath.Dir points at the shim's directory, not the real binary's neighbourhood, and the Cygwin detection returns the wrong answer. A filepath.EvalSymlinks(path) before filepath.Dir aligns the check with the actual binary location.

S4. Log a Debug line when ParseOpenSSHVersion falls back to 0.0.0 Claude
	return slices.DeleteFunc(copiedOpts, func(s string) bool {
		return strings.HasPrefix(s, "ControlMaster") || strings.HasPrefix(s, "ControlPath") || strings.HasPrefix(s, "ControlPersist")
	})
}

func ParseOpenSSHVersion(version []byte) *semver.Version {
	// Matches "OpenSSH_8.4p1 ..." (upstream) and "OpenSSH_for_Windows_9.5p2 ..."
	// (Win32-OpenSSH). The optional "_for_Windows" prefix is recognized so native
	// Windows OpenSSH is not misdetected as version 0.0.0.
	regex := regexp.MustCompile(`(?m)^OpenSSH(?:_for_Windows)?_(\d+\.\d+)(?:p(\d+))?\b`)
	matches := regex.FindSubmatch(version)
	if len(matches) == 3 {
		if len(matches[2]) == 0 {
			matches[2] = []byte("0")
		}
		return semver.New(fmt.Sprintf("%s.%s", matches[1], matches[2]))
	}
	return &semver.Version{}
}

func parseOpenSSHGSSAPISupported(version string) bool {
	return !strings.Contains(version, `Unsupported option "gssapiauthentication"`)
}

The returned 0.0.0 gates cipher selection and scp URL form. An unrecognised banner (a future fork, a HPN-SSH build, a wrapper that rewrites ssh -V output) silently downgrades behaviour without any signal in --debug logs. One logrus.Debugf line in the fallback branch makes the cause traceable.

     if len(matches) == 3 {
         ...
         return semver.New(fmt.Sprintf("%s.%s", matches[1], matches[2]))
     }
+    logrus.Debugf("ParseOpenSSHVersion: no match in %q; returning 0.0.0", string(version))
     return &semver.Version{}
S5. Warn once if _LIMA_WINDOWS_EXTRA_PATH is still set after removal Claude

The variable is removed from both code and docs. Users who set it in a shell profile get no signal that it stopped doing anything. One release of a transitional warning would flag the mismatch:

	DefaultInstanceName = "default"
	basicCommand        = "basic"
	advancedCommand     = "advanced"
)

func main() {
	if os.Geteuid() == 0 && (len(os.Args) < 2 || os.Args[1] != "generate-doc") {
		fmt.Fprint(os.Stderr, "limactl: must not run as the root user\n")
		os.Exit(1)
	}

	yq.MaybeRunYQ()
	err := newApp().Execute()
	server.StopAllExternalDrivers()
	osutil.HandleExitError(err)
	if err != nil {
		logrus.Fatal(err)
	}
}

func processGlobalFlags(rootCmd *cobra.Command) error {
	// --log-level will override --debug, but will not reset debugutil.Debug
	if debug, _ := rootCmd.Flags().GetBool("debug"); debug {
		logrus.SetLevel(logrus.DebugLevel)
S6. Assert the resolved symlink target stays inside $srcRoot before Copy-Item Claude
          $resolved = $posixPath
          $depth = 0
          while ($linkTargets.ContainsKey($resolved)) {
            if ($depth++ -gt 16) { throw "Symlink chain too deep starting from $posixPath" }
            $target = $linkTargets[$resolved]
            $slash = $resolved.LastIndexOf('/')
            $resolved = if ($slash -ge 0) { "$($resolved.Substring(0, $slash))/$target" } else { $target }
            $segs = New-Object System.Collections.ArrayList
            foreach ($s in ($resolved -split '/')) {
              if ($s -eq '' -or $s -eq '.') { continue }
              if ($s -eq '..') {
                if ($segs.Count -gt 0) { $segs.RemoveAt($segs.Count - 1) | Out-Null }
                continue
              }
              $segs.Add($s) | Out-Null
            }
            $resolved = $segs -join '/'
          }
          $srcFile = $resolved -replace '/', '\'
          $dstFile = Join-Path '_output\share\lima' ($posixPath -replace '/', '\')
          Write-Host "Resolving symlink: $posixPath -> $resolved"
          Copy-Item -Force -Path $srcFile -Destination $dstFile
        }
    - name: Scrub PATH and verify only native Windows toolchain is reachable
      run: |
        $ErrorActionPreference = 'Stop'
        Write-Host "PATH before scrub:"
        $env:PATH -split ';' | ForEach-Object { Write-Host "  $_" }

The loop silently swallows .. underflow. A symlink chain with enough .. segments produces a path relative to the repo root; the Copy-Item on line 552 then copies whichever file is there into _output\share\lima\templates\…. Lima's templates do not use such symlinks today, so this is defensive only, but a single post-loop check (if ($resolved -notlike "$srcRoot/*") { throw ... }) closes the loop on future template edits.

S7. Toolchain swap between limactl create and limactl start changes reverse-sshfs LocalPath Claude
		sshfsOptions += ",follow_symlinks"
	}
	logrus.Infof("Mounting %q on %q", m.Location, *m.MountPoint)

	resolvedLocation := m.Location
	if runtime.GOOS == "windows" {
		// Convert the host path to the form the sftp-server invoked by sshocker
		// expects. PathForSSH tracks the toolchain Lima is using: Cygwin-style
		// (e.g. /c/Users/jan) when ssh comes from Git for Windows / MSYS2 (where
		// sftp-server is also Cygwin-based), and native forward-slash form
		// (e.g. C:/Users/jan) when ssh is native Windows OpenSSH.
		sshExe, err := sshutil.NewSSHExe()
		if err != nil {
			return nil, err
		}
		resolvedLocation, err = sshutil.PathForSSH(ctx, sshExe, m.Location)
		if err != nil {
			return nil, err
		}
		logrus.Debugf("reverse-sshfs: host path %q resolved to %q for sftp-server", m.Location, resolvedLocation)
	}

	sshAddress, sshPort := a.sshAddressPort()
	// Create a copy of sshConfig to avoid
	// modifying HostAgent's sshConfig in case of Windows
	sshConfig := *a.sshConfig

The PR description already flags this: mount.go resolves LocalPath with PathForSSH (toolchain-aware) on every start, while pkg/limayaml/defaults.go computes the default MountPoint once at create time via WindowsSubsystemPath (Cygwin-style, no fallback variation). A user who creates an instance with Git for Windows on PATH and then removes it would see LocalPath change between restarts. Either document "do not swap Windows SSH toolchain between create and start" in the WSL2/QEMU docs, or persist the resolved LocalPath at create time so the restart path does not recompute it.

S8. Harden the native cygpath fallback against relative drive-letter inputs Gemini
	out, err := exec.CommandContext(ctx, "cygpath", filepath.ToSlash(orig)).CombinedOutput()
	if err == nil {
		return strings.TrimSpace(string(out)), nil
	}
	logrus.WithError(err).Debugf("cygpath unavailable for %q, attempting native conversion", orig)
	if vol := filepath.VolumeName(orig); len(vol) == 2 && vol[1] == ':' {
		converted := "/" + strings.ToLower(vol[:1]) + filepath.ToSlash(orig[2:])
		logrus.Debugf("native cygpath fallback: %q -> %q", orig, converted)
		return converted, nil
	}
	return "", fmt.Errorf("cannot convert %q to a Cygwin-style path: cygpath unavailable and input is not a drive-letter path", orig)
}

func WindowsSubsystemPathForLinux(ctx context.Context, orig, distro string) (string, error) {
	out, err := exec.CommandContext(ctx, "wsl", "-d", distro, "--exec", "wslpath", filepath.ToSlash(orig)).CombinedOutput()

orig[2:] is expected to begin with a separator. For orig = "C:foo", the result is "/cfoo" — not a valid Cygwin-style path. All current callers pass absolute paths, but the API contract is fragile for future callers. Insert the slash explicitly:

-converted := "/" + strings.ToLower(vol[:1]) + filepath.ToSlash(orig[2:])
+converted := "/" + strings.ToLower(vol[:1]) + "/" + strings.TrimPrefix(filepath.ToSlash(orig[2:]), "/")
S9. Docs overstate bundled OpenSSH component availability Codex
- When running lima using "wsl2", `${LIMA_HOME}/<INSTANCE>/serial.log` will not contain kernel boot logs
- WSL2 requires a `tar` formatted rootfs archive instead of a VM image

### External tools

Lima uses native Windows OpenSSH (`C:\Windows\System32\OpenSSH\`, included in
Windows 10 build 1803 and later) for `ssh`, `scp`, `ssh-keygen`, and `sftp-server`.
No additional Cygwin-style toolchain is required.

Installing [Git for Windows](https://gitforwindows.org/) (`winget install -e --id Git.MinGit`)
remains supported. Lima detects when ssh is a Cygwin-based build and uses
`cygpath` for path translation in that case, which respects any custom MSYS2
fstab the user has configured. On a vanilla Windows install with neither Git

OpenSSH Client (ssh, scp, ssh-keygen) ships by default on Windows 10 build 1803+, but OpenSSH Server — which provides sftp-server.exe — is an optional Feature on Demand (https://learn.microsoft.com/en-us/windows-server/administration/OpenSSH/openssh-overview). The new CI at .github/workflows/test.yml:357 and :584 treats sftp-server as required, so the reverse-sshfs path assumes the server component is installed. Users without it will see a mismatch between the docs and runtime behaviour. Soften to: Lima works with native Windows OpenSSH when the required components are installed; QEMU with reverse-sshfs additionally requires sftp-server.exe from the OpenSSH Server optional feature.

        #   required-native: must come from C:\Windows or be a built-in like wsl.
        #   forbidden: must NOT resolve at all on a "plain Windows" host.
        #   optional: may or may not be present; logged for context. Lima has
        #             graceful fallbacks (id, rsync) or only needs them for
        #             non-default code paths (qemu-img, decompressors).
        $required = @('ssh','scp','ssh-keygen','sftp-server','wsl','tar')
        $forbidden = @{
          'cygpath' = 'cygpath comes from MSYS2/Git-for-Windows/Cygwin only'
          'pacman'  = 'pacman comes from MSYS2 only'
        }
        # bash.exe is interesting but ambiguous: Windows 10/11 ship a WSL
S10. parseCopyPaths silently accepts UNC paths through PathForSSH Claude
		cp := &Path{}
		// On Windows, detect local drive-letter paths (e.g. C:\...) before
		// splitting on ":" so the drive letter is not mistaken for an instance
		// name. filepath.VolumeName returns "C:" for local drive paths and ""
		// for instance:path forms like "nat:/tmp/file".
		isLocalAbs := runtime.GOOS == "windows" && filepath.VolumeName(path) != ""
		if isLocalAbs {
			sshExe, err := sshutil.NewSSHExe()
			if err != nil {
				return nil, err
			}
			path, err = sshutil.PathForSSH(ctx, sshExe, path)
			if err != nil {
				return nil, err
			}
			cp.Path = path
			cp.IsRemote = false
			copyPaths = append(copyPaths, cp)
			continue
		}
		if runtime.GOOS == "windows" {
			path = filepath.ToSlash(path)
		}

		parts := strings.SplitN(path, ":", 2)

filepath.VolumeName(\\server\share\foo) returns \\server\share on Windows, so UNC paths hit this branch. On native OpenSSH they pass through filepath.ToSlash as //server/share/foo, which scp may or may not accept. On the Cygwin branch ioutilx.WindowsSubsystemPath falls back to the drive-letter handler (len(vol) == 2) and returns the new explicit error. UNC support is neither exercised in tests nor mentioned in the comment. Either document the intent (// UNC paths flow through PathForSSH; native ssh accepts //server/share/foo) or reject UNC early with a clean error. The fix for I2 (switching to filepath.IsAbs) preserves this branching because UNC paths are absolute, so UNC behaviour is unchanged either way — but the contract remains worth documenting.


Design Observations

Concerns

Strengths


Testing Assessment

New test coverage is narrow but targeted:

Gaps, in order of risk:

  1. checkRsyncOnGuest on native Windows OpenSSH with rsync present is not exercised anywhere; I1 would not be caught by any current test.
  2. parseCopyPaths has no unit tests for the new Windows branching; a table test over C:\foo, C:/foo, C:foo, instance:path, and \\server\share\foo would lock I2 in place and clarify UNC behaviour (S10).
  3. IsSSHCygwin cache behaviour across repeated calls with different sshExe arguments is documented but not asserted — a future refactor that expects per-call detection would regress silently.
  4. The native WindowsSubsystemPath fallback has no unit test; S8's relative-drive edge case would surface immediately under a simple table test.

The review agent ran go test ./pkg/sshutil ./pkg/copytool ./pkg/downloader ./pkg/ioutilx ./pkg/limayaml and it passed.


Documentation Assessment


Commit Structure


Acknowledged Limitations


Unresolved Feedback

No PR review comments have been posted. Prior feedback from #4819:


Agent Performance Retro

Claude

Claude produced the broadest review: one strong Important (I1, co-raised with Codex) and seven unique suggestions spanning symlink resolution, logging gaps, CI hardening, and documentation follow-ups. The I2 "gzip Close() swallows errors" finding was a false positive — gzip.Reader.Close just re-returns z.err, and the CRC/length error is surfaced from the last Read() call during io.Copy, so the code correctly returns it. I3 (IsSSHCygwin sync.Once) was downgraded to S1: the docstring documents the tradeoff, so "important" overstates it. Everything else was sound and well-anchored to line numbers.

Codex

Codex produced a tight, focused review: one Important (I1) that overlapped with Claude's, and one Suggestion (S9) about the docs misrepresenting bundled OpenSSH components. Codex ran go test ./pkg/{sshutil,copytool,downloader,ioutilx,limayaml} and reported the results. The narrower scope reflects a pragmatic severity bar — nothing here was padding.

Gemini

Gemini's Important (I2) was the single best find of the review: a subtle regression where switching from filepath.IsAbs to filepath.VolumeName breaks single-letter instance names. Gemini traced the merge-base behaviour, called out the silent failure mode, and proposed a one-line fix. The accompanying S8 (relative-drive edge case in the native cygpath fallback) is the same class of issue in a different file. Gemini did not run git blame — expected, given its quota constraints.

Summary

Claude Opus 4.7Codex GPT 5.5Gemini 3.1 Pro
Duration12m 52s5m 25s7m 46s
Findings1I 8S1I 1S1I 1S
Tool calls50 (Bash 25, Read 21, Grep 3)41 (shell 41)11 (readfile 7, grepsearch 4)
Design observations622
False positives100
Unique insights712
Files reviewed141414
Coverage misses000
Totals1I 8S1I 1S1I 1S
Downgraded1 (I→S)00
Dropped100

Reconciliation:

Claude delivered the most total value through breadth and CI-side findings, but Gemini's I2 is the single most impactful catch of the review — a silent correctness regression that neither of the others identified. Codex was efficient and its I1 converged with Claude's, confirming the rsync-probe gap with independent analysis.


Review Process Notes

Skill improvements

Repo context updates