Deep Review: 20260519-183659-pr-4996
| Date | 2026-05-19 18:36 |
| Repo | lima-vm/lima |
| Round | 1 |
| Author | @mie313 |
| PR | #4996 — PoC: Run Windows guest VM with VirtIO-FS filesystem mount |
| Branch | (PR branch) |
| Commits | c93a1fa2 PoC: Run Windows guest VM with VirtIO-FS filesystem mount |
| Reviewers | Claude Opus 4.7 (effort: xhigh), Codex GPT 5.5 (effort: xhigh), Gemini 3.1 Pro (effort: default), Qwen 3.6 27B (effort: high), Gemma 31B (effort: high) |
| Verdict | Merge with fixes — solid PoC; address the qcow2-clobber, tag mismatch, and virtiofsd lifecycle issues before the next iteration |
| Wall-clock time | 1 h 44 min 2 s |
Executive Summary ¶
This PoC adds poc-windows-guest/ to boot Windows Server 2025 under QEMU with autounattend-driven install, OpenSSH, and VirtIO-FS host-share. End-to-end functional on Ubuntu 24.04, but run.sh deletes windows.qcow2 on every launch, the README's tag=hoge disagrees with run.sh's tag=lima, and backgrounded virtiofsd orphans across runs. Eight Important issues span lifecycle, error handling, drive-letter portability, and supply chain.
Reviewers: 5 (Claude Opus 4.7, Codex GPT 5.5, Gemini 3.1 Pro, Gemma4 31B, Qwen 3.6 27B). Qwen ran twice: MXFP8 produced hallucinated XML; bf16 produced clean findings. Both included below; bf16 is the kept run.
Critical Issues ¶
None.
Important Issues ¶
# Note that this script expects that you've already set Windows OS ISO (windows_server_2025.iso) and virtio-win ISO (virtio-win-0.1.285.iso) in ./images/
set -e
# Cleanup
rm -f images/windows.qcow2
rm -f images/win-cidata.iso
# Build win-cidata.iso
mkdir -p tmp/win-cidata
cp autounattend.xml ./tmp/win-cidata/
cp first_logon.ps1 ./tmp/win-cidata/
The README presents run.sh as the way to launch the VM. The first two lines unconditionally delete the installed qcow2 disk image, so the second invocation destroys the Windows install (and any data created in it) and forces a full reinstall. The cleanup block belongs in a separate rebuild mode, not at the top of the launch path.
Fix: gate the deletion behind a flag (--rebuild or similar), or only create the disk when absent.
-# Cleanup
-rm -f images/windows.qcow2
-rm -f images/win-cidata.iso
+# Build the cidata ISO fresh each run (small, regenerable)
+rm -f images/win-cidata.iso
+
+# Create disk only on first run; preserve installed state otherwise
+if [ ! -f images/windows.qcow2 ]; then
+ qemu-img create -f qcow2 ./images/windows.qcow2 40G
+fi
(and remove the unconditional qemu-img create further down).
virtiofsd is backgrounded with no readiness check, no PID tracking, and no cleanup trap Claude Codex Gemini Gemma Qwen¶
# Create qcow2
qemu-img create -f qcow2 ./images/windows.qcow2 40G
# Run virtiofsd in background
virtiofsd --shared-dir=./ --socket-path=./virtiofsd.sock --sandbox none &
# Run QEMU
qemu-system-x86_64 \
-name guest=win2k25,debug-threads=on \
-machine pc-q35-noble,usb=off,vmport=off,dump-guest-core=off,memory-backend=pc.ram,hpet=off,acpi=on \
Three failure modes compound here. (1) QEMU starts immediately after the &, so if virtiofsd is still binding the socket QEMU's -chardev socket,...,path=./virtiofsd.sock can race and fail to connect. (2) When virtiofsd itself fails immediately (missing binary, bad arg, stale socket already bound), set -e does not catch background-process exits; the script continues into QEMU and prints a confusing "connection refused." (3) When QEMU exits — clean shutdown, Ctrl-C, or crash — virtiofsd is never killed and the socket is never removed; subsequent runs race against the orphan or fail with "address already in use."
Fix: store the PID, install an EXIT trap, remove stale sockets, and verify readiness before launching QEMU.
+rm -f ./virtiofsd.sock
+trap 'kill "${VFS_PID:-0}" 2>/dev/null || true; rm -f ./virtiofsd.sock' EXIT
virtiofsd --shared-dir=./ --socket-path=./virtiofsd.sock --sandbox none &
+VFS_PID=$!
+while [ ! -S ./virtiofsd.sock ]; do kill -0 "$VFS_PID" || exit 1; sleep 0.1; done
The existing Lima QEMU driver under pkg/hostagent follows this pattern for its vhost sockets — reuse that lifecycle when this graduates from PoC.
-blockdev driver=raw,file=cdrom2-storage,node-name=cdrom2,read-only=true \
-device ide-cd,bus=ide.3,drive=cdrom2 \
-netdev user,id=net0,net=192.168.10.0/24,dhcpstart=192.168.10.15,hostfwd=tcp:127.0.0.1:60022-:22 \
-device virtio-net-pci,netdev=net0 \
-chardev socket,id=chr-vu-fs0,path=./virtiofsd.sock \
-device vhost-user-fs-pci,chardev=chr-vu-fs0,tag=hoge \
-device virtio-rng-pci
```
#### Try SSH
```bash
# run.sh:47
-device vhost-user-fs-pci,chardev=chr-vu-fs0,tag=lima \
hoge is a Japanese placeholder name, evidently leftover. The README's own sample output later shows Volume in drive Z is lima (README.md:104), so the documented command does not match the documented output. A reader who runs the manual block instead of run.sh gets a hoge tag, which the WinFSP service path (E:\viofs\2k25\amd64\virtiofs.exe) may or may not pick up — the result is silently inconsistent with what the README shows.
Fix: change README.md:87 to tag=lima.
$logfile = "C:\Users\limauser\lima-setup.log"
# Record logs
Start-Transcript -Path $logfile -Append
# Install OpenSSH server, then enable it
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
# Modify firewall rule
# Note that Windows server may have a firewall rule for SSH by default, but it doesn't work on my env.
# So I remove and recreate the rule.
Remove-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction Ignore
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
# Set a public key. Since a user `lima` is in Administrators group,
# The pubelic key should be located under C:\ProgramData\ssh instead of under C:\Users\lima\.ssh.
$pubkey = Get-Content -Path F:\user.pub
$pubkeyLocation = 'C:\ProgramData\ssh\administrators_authorized_keys'
Add-Content -Force -Path $pubkeyLocation -Value $pubkey
icacls $pubkeyLocation /inheritance:r
icacls $pubkeyLocation /grant "SYSTEM:F"
icacls $pubkeyLocation /grant "Administrators:F"
# Install chocolatey for installing WinFSP.
# WinFSP can be installed through winget as well, but currently winget is unstable in Windows Server Core
# See: https://github.com/microsoft/winget-cli/discussions/5230
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# Install WinFSP for VirtioFS
C:\ProgramData\chocolatey\choco.exe install winfsp -y --pre
# Create VirtioFs service from virtio-win, and enable it
# By default, the host directory is mounted on Z:
New-Service -Name VirtioFsSvc -BinaryPathName 'E:\viofs\2k25\amd64\virtiofs.exe' -DisplayName VirtioFsSvc -StartupType Automatic
Start-Service -Name VirtioFsSvc
# Finish recording logs
Stop-Transcript
PowerShell does not stop on cmdlet failure by default, and native commands (choco.exe, icacls) are not checked through $LASTEXITCODE. If Add-WindowsCapability fails with a transient network error pulling FoD from Microsoft, Start-Service sshd runs anyway and produces a separate, unrelated error; the VM boots into a half-provisioned state where SSH never works. The same applies to chocolatey install failure (no WinFSP → New-Service succeeds but the service binary is missing → Start-Service VirtioFsSvc silently fails). The transcript log captures everything but a user has to scroll through hundreds of lines to notice.
Fix: set strict error handling at the top of the script, wrap the body in try/finally, and check $LASTEXITCODE after each native command.
+$ErrorActionPreference = 'Stop'
+try {
# ... existing body ...
+ if ($LASTEXITCODE -ne 0) { throw "choco install winfsp failed: $LASTEXITCODE" }
+} catch { Write-Error $_; exit 1 }
+finally { Stop-Transcript }
# Create qcow2
qemu-img create -f qcow2 ./images/windows.qcow2 40G
# Run virtiofsd in background
virtiofsd --shared-dir=./ --socket-path=./virtiofsd.sock --sandbox none &
# Run QEMU
qemu-system-x86_64 \
-name guest=win2k25,debug-threads=on \
-machine pc-q35-noble,usb=off,vmport=off,dump-guest-core=off,memory-backend=pc.ram,hpet=off,acpi=on \
The shared directory is the PoC root, which contains images/ (the Windows ISO, the virtio-win ISO, and the live windows.qcow2 that QEMU is using as the guest disk), autounattend.xml (with the plaintext password), first_logon.ps1, and the virtiofsd.sock itself. The README's dir Z:\ screenshot at line 109-119 confirms — the guest can list and modify its own backing disk. Combined with --sandbox none (no chroot/user-namespace isolation), this gives the guest a direct path to corrupt its own VM image and tamper with the installation media. For a PoC this is tolerable, but the default share root should be a dedicated subdirectory.
Fix:
+mkdir -p ./shared
-virtiofsd --shared-dir=./ --socket-path=./virtiofsd.sock --sandbox none &
+virtiofsd --shared-dir=./shared --socket-path=./virtiofsd.sock --sandbox none &
and a note in the README that --sandbox none plus a parent-directory share means the guest can tamper with its own VM image.
icacls $pubkeyLocation /grant "Administrators:F"
# Install chocolatey for installing WinFSP.
# WinFSP can be installed through winget as well, but currently winget is unstable in Windows Server Core
# See: https://github.com/microsoft/winget-cli/discussions/5230
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# Install WinFSP for VirtioFS
C:\ProgramData\chocolatey\choco.exe install winfsp -y --pre
# Create VirtioFs service from virtio-win, and enable it
This is the canonical iex (DownloadString …) supply-chain anti-pattern: every VM provisioning run executes whatever code currently lives at community.chocolatey.org/install.ps1, as Administrator, with no checksum and no pinned commit. A compromise of the chocolatey CDN (or a man-in-the-middle between the guest and the CDN — the guest does its first network reach through QEMU's user-mode NAT) is full code execution on every Lima Windows guest provisioned from this PoC. WinFSP is the only package needed; downloading the MSI directly from github.com/winfsp/winfsp releases and verifying a pinned SHA-256 removes both the chocolatey dependency and the unpinned-fetch risk.
Fix:
$winfspUrl = 'https://github.com/winfsp/winfsp/releases/download/v2.0/winfsp-2.0.23075.msi'
$winfspSha256 = '<sha256-of-the-msi>'
$msi = "$env:TEMP\winfsp.msi"
Invoke-WebRequest -Uri $winfspUrl -OutFile $msi -UseBasicParsing
$got = (Get-FileHash -Algorithm SHA256 $msi).Hash
if ($got -ne $winfspSha256) { throw "winfsp checksum mismatch: $got" }
Start-Process msiexec.exe -Wait -ArgumentList "/i `"$msi`" /qn"
<component name="Microsoft-Windows-PnpCustomizationsWinPE" publicKeyToken="31bf3856ad364e35"
language="neutral" versionScope="nonSxS" processorArchitecture="amd64"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<DriverPaths>
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
<Path>E:\viostor\2k25\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="3">
<Path>E:\NetKVM\2k25\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
<Path>E:\viofs\2k25\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="5">
<Path>E:\Balloon\2k25\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="6">
<Path>E:\viorng\2k25\amd64</Path>
</PathAndCredentials>
</DriverPaths>
</component>
<component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
<InputLocale>en-US</InputLocale>
Two failure surfaces share one cause. At install time, the answer file driver paths (autounattend.xml:9-22), the cidata first_logon.ps1 invocation (autounattend.xml:134), and the cidata user.pub read (first_logon.ps1:19) all assume E: = virtio-win ISO and F: = cidata ISO. That mapping comes from the cdrom0/ide.1 → D:, cdrom1/ide.2 → E:, cdrom2/ide.3 → F: Windows convention — not a contract. Reorder the -device ide-cd,bus=ide.N lines in run.sh, or add another removable device, and every reference silently breaks.
At runtime, first_logon.ps1:36 registers VirtioFsSvc with its BinaryPathName on the virtio-win CD-ROM. The service persists across reboots, but its binary lives on removable media — detaching the ISO or any drive-letter reshuffle leaves the service registered with a missing binary.
# Install WinFSP for VirtioFS
C:\ProgramData\chocolatey\choco.exe install winfsp -y --pre
# Create VirtioFs service from virtio-win, and enable it
# By default, the host directory is mounted on Z:
New-Service -Name VirtioFsSvc -BinaryPathName 'E:\viofs\2k25\amd64\virtiofs.exe' -DisplayName VirtioFsSvc -StartupType Automatic
Start-Service -Name VirtioFsSvc
# Finish recording logs
Stop-Transcript
Fix: copy the service binary to C:\ before registering, and locate the cidata ISO by volume label (Get-Volume -FileSystemLabel autounattend) rather than by drive letter.
-New-Service -Name VirtioFsSvc -BinaryPathName 'E:\viofs\2k25\amd64\virtiofs.exe' -DisplayName VirtioFsSvc -StartupType Automatic
+Copy-Item -Path 'E:\viofs\2k25\amd64\virtiofs.exe' -Destination 'C:\Windows\System32\virtiofs.exe'
+New-Service -Name VirtioFsSvc -BinaryPathName 'C:\Windows\System32\virtiofs.exe' -DisplayName VirtioFsSvc -StartupType Automatic
virtiofsd --shared-dir=./ --socket-path=./virtiofsd.sock --sandbox none &
# Run QEMU
qemu-system-x86_64 \
-name guest=win2k25,debug-threads=on \
-machine pc-q35-noble,usb=off,vmport=off,dump-guest-core=off,memory-backend=pc.ram,hpet=off,acpi=on \
-accel kvm \
-cpu host,migratable=on,hv-time=on,hv-relaxed=on,hv-vapic=on,hv-spinlocks=0x1fff \
-m size=2048 \
-object memory-backend-memfd,id=pc.ram,share=true,size=2048M \
-overcommit mem-lock=off \
The pc-q35-noble alias ships with Canonical's Ubuntu 24.04 ("noble") QEMU package. On Fedora, Arch, Debian-stable, RHEL, or any non-Ubuntu host, QEMU rejects this with unsupported machine type before Windows setup even starts. The README's "Tested on Ubuntu 24.04" note understates the constraint — this is a hard prerequisite, not a "tested on" annotation. The portable name q35 selects the newest variant available on every distro.
Fix:
- -machine pc-q35-noble,usb=off,vmport=off,dump-guest-core=off,memory-backend=pc.ram,hpet=off,acpi=on \
+ -machine q35,usb=off,vmport=off,dump-guest-core=off,memory-backend=pc.ram,hpet=off,acpi=on \
Apply the same change in README.md:67.
Severity note: Gemini ranked this as Important on portability grounds; Claude and Codex ranked it as a Suggestion since the README explicitly says Ubuntu 24.04 was used. Settling on Important here because the script fails immediately on non-Ubuntu hosts, which is the first thing many readers will try.
Suggestions ¶
### Host environment
I tested the code on:
CPU arch: x86_64
OS: Ubuntu 24.04
### Prerequisites
You should have:
- QEMU binary
- mkisofs (or altanatives) for creating ISO file
- Through apt, you can install it via `apt install genisoimage`
- virtiofsd binary
- You can install via `cargo install virtiofsd`
### Prepare ISO files
#### Download Windows Server 2025 ISO
You can download the ISO file from https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2025.
Note that you need to register your name, email, company, and so on.
After downloading it, you should place the ISO file as `poc-windows-guest/images/windows_server_2025.iso`.
~/.lima/_config/user.pub is created on the first limactl invocation that needs SSH keys. A fresh Lima install (or a CI environment) has no such file; the user gets a generic cp: cannot stat '/root/.lima/_config/user.pub' and has no obvious next step. The Prerequisites section should explicitly say "Run limactl create --name=default template://default once to generate ~/.lima/_config/user{,.pub}."
#!/bin/bash
# Note that this script expects that you've already set Windows OS ISO (windows_server_2025.iso) and virtio-win ISO (virtio-win-0.1.285.iso) in ./images/
set -e
# Cleanup
rm -f images/windows.qcow2
rm -f images/win-cidata.iso
# Build win-cidata.iso
mkdir -p tmp/win-cidata
cp autounattend.xml ./tmp/win-cidata/
cp first_logon.ps1 ./tmp/win-cidata/
Every path is relative to CWD. Running bash poc-windows-guest/run.sh from the repo root silently uses (and creates) ./images/, ./tmp/, and ./virtiofsd.sock in the wrong place. The README's "Please move to ./poc-windows-guest before executing those commands" is a documentation workaround for a one-line script fix.
Fix:
#!/bin/bash
+cd "$(dirname "$0")"
set -e
# Note that Windows server may have a firewall rule for SSH by default, but it doesn't work on my env.
# So I remove and recreate the rule.
Remove-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction Ignore
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
# Set a public key. Since a user `lima` is in Administrators group,
# The pubelic key should be located under C:\ProgramData\ssh instead of under C:\Users\lima\.ssh.
$pubkey = Get-Content -Path F:\user.pub
$pubkeyLocation = 'C:\ProgramData\ssh\administrators_authorized_keys'
Add-Content -Force -Path $pubkeyLocation -Value $pubkey
icacls $pubkeyLocation /inheritance:r
icacls $pubkeyLocation /grant "SYSTEM:F"
The account is named limauser throughout autounattend.xml (line 117) and the rest of the script, not lima. Both references in this comment block — and the user-profile path C:\Users\lima\.ssh — are wrong. Also pubelic → public. The comment is the place where the subtle Windows OpenSSH "Administrator accounts authorize via C:\ProgramData\ssh\administrators_authorized_keys" rule gets explained for the next maintainer; the explanation should not also be wrong about the username.
Fix:
-# Set a public key. Since a user `lima` is in Administrators group,
-# The pubelic key should be located under C:\ProgramData\ssh instead of under C:\Users\lima\.ssh.
+# Set a public key. Since limauser is in the Administrators group,
+# the public key must live under C:\ProgramData\ssh\administrators_authorized_keys
+# rather than C:\Users\limauser\.ssh\authorized_keys (OpenSSH on Windows
+# enforces this for admin accounts).
OS: Ubuntu 24.04
### Prerequisites
You should have:
- QEMU binary
- mkisofs (or altanatives) for creating ISO file
- Through apt, you can install it via `apt install genisoimage`
- virtiofsd binary
- You can install via `cargo install virtiofsd`
### Prepare ISO files
altanatives → alternatives. The ISO volume label autoanattend is missing a u (should be autounattend). Windows Setup keys off the filename autounattend.xml, not the volume label, so the label typo is cosmetic — but it propagates from run.sh:16 into the README and matters for grep-ability and search-engine indexing. Fix both occurrences together so the label stays consistent.
# Build win-cidata.iso
mkdir -p tmp/win-cidata
cp autounattend.xml ./tmp/win-cidata/
cp first_logon.ps1 ./tmp/win-cidata/
cp ~/.lima/_config/user.pub ./tmp/win-cidata/
mkisofs -o ./images/win-cidata.iso -J -r -V "autoanattend" ./tmp/win-cidata/
# Create qcow2
qemu-img create -f qcow2 ./images/windows.qcow2 40G
# Run virtiofsd in background
Fix:
- - mkisofs (or altanatives) for creating ISO file
+ - mkisofs (or alternatives) for creating ISO file
...
-mkisofs -o ./images/win-cidata.iso -J -r -V "autoanattend" ./tmp/win-cidata/
+mkisofs -o ./images/win-cidata.iso -J -r -V "autounattend" ./tmp/win-cidata/
and the same change in run.sh:16.
images/virtio-win-0.1.285.iso
images/win-cidata.iso
images/windows.qcow2
images/windows_server_2025.iso
tmp/*
*virtiofsd.sock*
If a future contributor downloads a different virtio-win release (e.g., virtio-win-0.1.290.iso), it won't be ignored, and git status will offer to commit it. Use a glob.
Fix:
-images/virtio-win-0.1.285.iso
+images/virtio-win-*.iso
rm -f images/windows.qcow2
rm -f images/win-cidata.iso
# Build win-cidata.iso
mkdir -p tmp/win-cidata
cp autounattend.xml ./tmp/win-cidata/
cp first_logon.ps1 ./tmp/win-cidata/
cp ~/.lima/_config/user.pub ./tmp/win-cidata/
mkisofs -o ./images/win-cidata.iso -J -r -V "autoanattend" ./tmp/win-cidata/
# Create qcow2
qemu-img create -f qcow2 ./images/windows.qcow2 40G
# Run virtiofsd in background
~ expands fine outside quotes here, but if a user clones the repo under a path with spaces (a common case on macOS and Windows), the unquoted relative paths break. Combined with S2's cd "$(dirname "$0")", every relative path becomes a fixed string that does not need quoting, so this is largely subsumed once S2 lands.
Repo style (CLAUDE.md) also calls for long-form CLI options in checked-in scripts; the mkisofs flags -J -r -V are recognizable enough that this is a stylistic preference rather than a correctness issue.
# Note that this script expects that you've already set Windows OS ISO (windows_server_2025.iso) and virtio-win ISO (virtio-win-0.1.285.iso) in ./images/
set -e
# Cleanup
rm -f images/windows.qcow2
rm -f images/win-cidata.iso
# Build win-cidata.iso
mkdir -p tmp/win-cidata
cp autounattend.xml ./tmp/win-cidata/
cp first_logon.ps1 ./tmp/win-cidata/
cp ~/.lima/_config/user.pub ./tmp/win-cidata/
mkisofs -o ./images/win-cidata.iso -J -r -V "autoanattend" ./tmp/win-cidata/
mkdir -p does not clear the directory. A previous run that left a renamed first_logon.ps1.old (or any other artifact from manual editing) gets packaged into the next ISO, because mkisofs ... ./tmp/win-cidata/ writes the directory's current contents verbatim. Either rm -rf tmp/win-cidata before the mkdir -p, or use mktemp -d for a fresh staging area each run.
Fix:
+# Build win-cidata.iso (fresh staging dir each time)
+rm -rf tmp/win-cidata
mkdir -p tmp/win-cidata
Design Observations ¶
Concerns
- Plaintext credentials in
autounattend.xml(future)Claude —poc-windows-guest/autounattend.xml:111,142. The local-account password and the autologon password both sit in plaintext, intended as a one-shot bootstrap before the public key is installed. Once the SSH key is in place the password becomes the weak link, and the same file ships into the cidata ISO that gets attached to the running VM. For the eventual non-PoC integration, generate a strong random password per provision, or use<UserAccounts>\AdministratorPasswordwith the base64 obfuscation Microsoft documents (still trivially decoded, but less visible than<Value>limauser</Value>next to the usernamelimauser). autounattend.xmlandfirst_logon.ps1are pinned to Windows Server 2025(future)Claude —poc-windows-guest/autounattend.xml:9-22,poc-windows-guest/first_logon.ps1:36. Driver paths reference\2k25\literally. The eventual Lima integration needs a templating layer that substitutes both the Windows version (2k22,2k25,11, etc.) and the virtio-win release. The current PoC layout — one fixed2k25subdir — should not get codified as the long-term answer-file format.- The PoC reimplements QEMU/virtiofsd lifecycle handling instead of reusing Lima's
(future)Codex. The in-tree QEMU driver underpkg/hostagentalready handles socket readiness, vhost-process shutdown, and structured logging. If this graduates into Lima proper, the Windows guest path should reuse that machinery; the currentrun.shfailures aroundvirtiofsd(I2) are exactly the failure modes the driver already solves.
Strengths
- The PoC keeps all new code under one directory and touches no existing Lima Go or template code, making the experiment trivial to delete or evolve.
- VirtIO-FS (rather than 9p or SMB) on the host side matches the QEMU recommendation in #4852 and the existing Lima VirtIO-FS path on macOS, so the host-side plumbing is reusable.
<SynchronousCommand>for first-logon (autounattend.xml:131) is the correct primitive — Windows blocks boot until the script completes, so installation failures are observable rather than racing against early SSH access.- SSH key placement at
C:\ProgramData\ssh\administrators_authorized_keyswith theicacls /inheritance:r+SYSTEM:F+Administrators:FACL set Gemma correctly handles the specific OpenSSH-for-Windows rule that admin-group accounts authorize via a system-wide file, not%USERPROFILE%\.ssh.
Testing Assessment ¶
No tests added. Acceptable for a PoC, but the eventual feature needs:
- First-logon failure-mode coverage: a smoke check inside
first_logon.ps1that asserts the WinFSP MSI install succeeded andvirtiofs.exeexists beforeNew-Serviceruns. Addresses I4, I6, I7. - Drive-letter-independence test: either a runtime probe (find ISOs by volume label) or an integration test that boots the guest and asserts
Z:\is mounted after first reboot. Addresses I7. - Idempotent re-launch test: running
run.sha second time against an already-installed VM should preserve the qcow2. Addresses I1. - virtiofsd lifecycle test: verify the socket is removed and no orphan
virtiofsdremains after a clean QEMU exit or Ctrl-C. Addresses I2. - Cross-distro QEMU smoke: at minimum, document which distros are known to work; better, replace
pc-q35-noblewithq35so the script runs on Fedora/Debian/Arch without modification. Addresses I8. - Offline / proxy-blocked install path: the chocolatey bootstrap (I6) and
Add-WindowsCapability(which pulls from Microsoft FoD) both require unrestricted internet — CI behind a corporate proxy will fail. Document the network requirements or pre-bundle the dependencies.
Documentation Assessment ¶
README.md is the only documentation, written as a runbook rather than a design note. That fits a PoC, but two improvements would significantly raise the floor for the next reader:
- Distinguish "create/rebuild" from "launch existing": once I1 lands, the README should make the two flows explicit. Today's text reads as "launch the VM," but the script actually means "wipe and reinstall."
- State hard prerequisites alongside package names: the existing "Tested on Ubuntu 24.04" note understates the
pc-q35-nobleand--accel kvmconstraints. List them as hard requirements (Ubuntu 24.04 QEMU, KVM-enabled host, user in thekvmgroup) so a reader knows up-front whether their setup will work. - Note the host-data-exposure caveat: the README should flag that the guest can read and write any file under the shared directory, including the qcow2 backing its own disk (I5).
- Fix the find-and-replace artifact noise: see I3 (tag mismatch). Once that and S4 are fixed, the README and
run.shagree, and the volume label is no longer misspelled.
The Lima top-level documentation does not yet reference this directory (confirmed via grep) — which is correct for a PoC. Do not link it from the main README until the design issues above are settled.
Acknowledged Limitations ¶
- Code comment: "Windows server may have a firewall rule for SSH by default, but it doesn't work on my env." —
poc-windows-guest/first_logon.ps1:12. The remove-and-recreate firewall rule is intentional. Worth investigating later whether this is environment-specific or universal, but not raised as a finding for this PoC. - Code comment: "winget is unstable in Windows Server Core" —
poc-windows-guest/first_logon.ps1:27. The chocolatey choice is intentional given winget's current state on Server Core. I6 is limited to the missing integrity check around that choice, not the choice itself.
Agent Performance Retro ¶
Claude ¶
Most thorough reviewer. Caught the supply-chain risk in the chocolatey bootstrap (I6) and the --shared-dir=./ host-data exposure (I5) — both raised only by Claude, both high-impact. Grouped the three drive-letter symptoms (autounattend driver paths, first_logon.ps1:19 cidata path, and the persistent VirtioFsSvc binary path) into one coherent I5/S3 chain instead of three disconnected findings. Severity calibration matched the consolidated outcome on every entry; no downgrades, no rejections.
Codex ¶
Tightest signal-to-noise. The unique catch — rm -f images/windows.qcow2 at the top of run.sh destroys the installed VM on every launch — is arguably the most user-visible bug in the PR, and only Codex framed it as a data-loss issue rather than a cleanup note. Useful Acknowledged Limitations section flagged the chocolatey choice and the firewall workaround as intentional, which kept the consolidated report from re-flagging those as findings.
Gemini ¶
Mixed quality. Caught one real angle the others missed — the persistent VirtioFsSvc binary path pointing at the CD-ROM drive (now part of I7). One outright fabrication: I3 in Gemini's output claimed the README had find-and-replace artifacts like limauser @localhost and colima.svg, but the actual lines are correct — this was rejected as a false positive. Promoted pc-q35-noble to Critical-adjacent Important; Claude and Codex ranked it as a Suggestion. Settled on Important in the consolidated report, but Gemini overstated by classifying the entire issue as "regression" with a critical-severity verdict block. As usual, Gemini skipped git blame (known quota behavior).
Gemma ¶
Best local-model performance to date. 4 findings, all consolidated cleanly with the cloud trio, no false positives, no malformed output. Caught nothing unique, but the smaller surface (4 findings vs. Claude's 14) is appropriate for a 31B local model on a 362-line diff. Correctly identified chocolatey as an in-scope design concern rather than just a finding, which matches Claude's I6 framing.
Qwen ¶
The first pass with the MXFP8 quantization (qwen3.6:27b-coding-mxfp8) produced 33 lines of hallucinated XML state markers (<state>world_write\n<files>\n<file>...) rather than the requested Critical/Important/Suggestions structure. The schema does not appear in the prompt or the source files; the model invented it on its final turn after 3 model turns and ~54 K tokens of accumulated context. A second pass with the bf16 quantization of the same base model (qwen3.6:27b-coding-bf16, 50 GB vs MXFP8's 29 GB) against the same prompt produced clean structured findings: 2 Important and 3 Suggestions, all consolidating with the cloud trio. Notably, bf16 caught the qcow2-destruction issue (consolidated I1) that was otherwise Codex's unique catch among the cloud agents — no other local model surfaced it. After post-consolidation promotions (S2 virtiofsd → I2, S3 error handling → I4), Qwen's bf16 pass contributes to four Important findings and one Suggestion. The MXFP8/bf16 comparison isolates quantization as the cause of the earlier failure: same model architecture (standard Qwen 3.5 dense decoder per Ollama's arch=Qwen3_5ForConditionalGeneration log line), same prompt, same multi-turn conversation shape, same diff under review — only the quantization changed, and the result went from malformed to high-quality. Initial framing during the first pass that called this a "coding-specialized" model was incorrect; the failure was MXFP8 degrading instruction-following under multi-turn load, not training-time bias. The retro table below reflects only the bf16 pass (the kept run); the MXFP8 pass is preserved at /tmp/deep-review-HlOEwA/*.mxfp8 for the record.
Summary ¶
| Claude Opus 4.7 | Codex GPT 5.5 | Gemini 3.1 Pro | Qwen 3.6 27B | Gemma 31B | |
|---|---|---|---|---|---|
| Duration | 3m 50s | 4m 35s | — | 9m 37s | 10m 07s |
| Findings | 7I 7S | 6I 1S | 4I 1S | 4I 1S | 3I |
| Tool calls | 8 (Read 5, Bash 3) | 33 (shell 31, stdin 2) | — | 9 (read 6, bash 2, glob 1) | 5 (read 5) |
| Design observations | 2 | 1 | 1 | 1 | 1 |
| False positives | 0 | 0 | 1 | 0 | 0 |
| Unique insights | 6 | 1 | 1 | 0 | 0 |
| Files reviewed | 6 | 6 | 6 | 6 | 6 |
| Coverage misses | 0 | 0 | 0 | 0 | 0 |
| Totals | 7I 7S | 6I 1S | 4I 1S | 4I 1S | 3I |
| Downgraded | 0 | 0 | 1 (I→S) | 0 | 0 |
| Dropped | 0 | 0 | 1 | 0 | 0 |
Reconciliation:
- Gemini C1 → I2 (downgraded critical → important). Gemini classified "virtiofsd socket conflicts and process leaks" as Critical. The same issue surfaced from Claude (I2), Codex (I3), and Gemma (I1) at Important. Settled on Important: the bug breaks re-runs and leaves orphan processes, but the first run completes successfully and no data is corrupted. Critical is reserved for issues blocking a single-run success or risking persistent damage.
- Gemini S1, Codex S1, Gemma S1 → I3 (promoted suggestion → important). The README
tag=hogevsrun.sh tag=limamismatch is the central documented entry point for the PoC; the README is what most users will copy. Claude raised it as Important; the others as Suggestion. Promoted to match Claude's read. - Claude S5, Codex S2 → I8 (promoted suggestion → important). The
pc-q35-noblemachine type fails immediately on non-Ubuntu hosts. Gemini ranked Important; Claude and Codex ranked Suggestion citing the README's Ubuntu-24.04 note. Promoted to Important because the failure is immediate on every other distribution and the README's annotation does not flag it as a hard prerequisite. - Gemini I3 — rejected. Claimed the README contained accidental find-and-replace artifacts (
limauser @localhostand@website/static/images/users/colima.svg). Verified against the README in the lead worktree: the lines readlimauser@localhostandlimauser@LIMArespectively. The cited fragments do not exist in the file. False positive. - Gemma S2 → merged into I7. Gemma raised drive-letter fragility as a Suggestion; Claude raised the same issue plus the persistent service path as Important (I5 in Claude's numbering). Merged into the consolidated I7 at Important.
- Gemini I2 → merged into I7. Same root cause as above; Gemini's framing focused on the persistent service path specifically, which became one of the two failure surfaces under consolidated I7.
- Qwen S2 → I2 (promoted suggestion → important). Qwen's bf16 pass raised the
virtiofsdcleanup issue as a Suggestion; consolidated as Important on the same reasoning as Gemma S2 above. - Qwen S3 → I4 (promoted suggestion → important). Qwen's bf16 pass raised
first_logon.ps1error handling as a Suggestion; consolidated as Important on the same reasoning as the cloud agents.
The cloud trio produced highly complementary findings: Claude's depth, Codex's data-loss catch (also caught by Qwen-bf16), and Gemini's persistent-service angle each surfaced something the others missed. Gemma usefully confirmed consensus without inflating the noise floor. Qwen-bf16 — once the quantization was switched from MXFP8 to bf16 — joined the consensus and added independent confirmation of the highest-impact issue (qcow2 destruction). For future PRs the recommended combination is Claude + Codex + Gemini + Gemma + Qwen (bf16); the MXFP8 variant of Qwen should not be used on prompts in this length class until further evidence shows it can hold the format under multi-turn load.
Review Process Notes ¶
Skill improvements ¶
- Per-agent wall-clock is undercounted for local (opencode-backed) agents and may be undercounted for cloud agents too.
collect-metrics/session_opencode.gocomputesduration_sas the span between the first and lasttimestampfield inraw-{kind}-pass-{N}.ndjson. opencode's firststep_startevent fires only when the model begins streaming its first response tokens — that excludes model load AND all server-side prompt processing on every preceding HTTP request. On this review, the qwen pass showed an ndjson event window of 5 min 25 s while Ollama's own server log showed actual model-side wall-clock of 13 min 48 s (~60 % undercount; ~8 min of model load and multi-turn prompt processing were invisible to the ndjson stream). The same event-bounds approach applies to Claude/Codex/Gemini; cloud-agent CLIs emit their first session event much earlier, so the undercount is smaller, but it is still nonzero. Capture the actual process start and end times inlaunch.go(immediately before and aftercmd.Run()) and write them to e.g.times-{kind}-pass-{N}.json;collect-metricsshould prefer those timestamps over event bounds for all agent kinds. - Local-model format adherence is nondeterministic across conversation length and context size, not a fixed property of the model. This review saw a dense 27 B local model (Qwen 3.5 dense decoder per Ollama's
arch=…log) produce hallucinated XML-shaped markup on a multi-turn 16 KB-diff review while the same model produced clean structured findings on a 7 KB single-turn review three hours earlier. Do not treat a single bad run as evidence of a persistent model failure: the skill's existing failure handling already drops empty/malformed local-model passes from consolidation, and one such miss is the expected cost of including a local-model slot. Before swapping a model line inlaunch.goon the strength of one run, check the sidecar JSON'sconsolidated_findingsfor the same agent across 3+ prior reviews of varying diff sizes — if the agent regularly joins consensus findings on small/single-turn reviews but fails on larger/multi-turn ones, the right response is to scope the agent's use, not to swap the model.
Repo context updates ¶
None this review. The PoC sits in a standalone poc-windows-guest/ directory and does not exercise existing Lima architecture, conventions, or stable APIs. The current deep-review-context.md entries on mount-path symlink resolution, EnsureDisk responsibilities, Windows SSH toolchains, and the other Lima-specific patterns did not apply to any finding in this review.