Deep Review: 20260519-183659-pr-4996

Date2026-05-19 18:36
Repolima-vm/lima
Round1
Author@mie313
PR#4996 — PoC: Run Windows guest VM with VirtIO-FS filesystem mount
Branch(PR branch)
Commitsc93a1fa2 PoC: Run Windows guest VM with VirtIO-FS filesystem mount
ReviewersClaude 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)
VerdictMerge with fixes — solid PoC; address the qcow2-clobber, tag mismatch, and virtiofsd lifecycle issues before the next iteration
Wall-clock time1 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

I1. run.sh deletes the installed windows.qcow2 on every launch Codex Qwen

# 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).

I2. 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.

I3. VirtIO-FS tag differs between README.md and run.sh Claude Codex Gemini Gemma Qwen
    -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.

I4. first_logon.ps1 has no error handling — silent partial-provisioning failures Claude Codex Qwen
$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 }
I5. virtiofsd --shared-dir=./ exposes the host ISOs and the live qcow2 to the guest Claude Codex

# 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.

I6. Chocolatey bootstrap is iex-piped from the internet with no integrity check Claude
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"
I7. Drive letters E:\ and F:\ are hardcoded throughout — install-time and at runtime Claude Gemini Gemma
    <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
I8. pc-q35-noble machine type is Ubuntu-noble-specific Claude Codex Gemini
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

S1. README does not list "run Lima at least once" as a prerequisite Claude
### 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}."

S2. run.sh relies on the user's CWD being poc-windows-guest/ Claude
#!/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
S3. Stale and misspelled identifier references in first_logon.ps1 Claude Codex
# 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 pubelicpublic. 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).
S4. Typos in README and run.shaltanatives, autoanattend Claude Gemini Qwen
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

altanativesalternatives. 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.

S5. .gitignore pins a single virtio-win ISO version Claude
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
S6. Path quoting in run.sh breaks on paths containing spaces Claude
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.

S7. tmp/win-cidata/ is not cleaned before each mkisofs run Claude

# 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

Strengths


Testing Assessment

No tests added. Acceptable for a PoC, but the eventual feature needs:

  1. First-logon failure-mode coverage: a smoke check inside first_logon.ps1 that asserts the WinFSP MSI install succeeded and virtiofs.exe exists before New-Service runs. Addresses I4, I6, I7.
  2. 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.
  3. Idempotent re-launch test: running run.sh a second time against an already-installed VM should preserve the qcow2. Addresses I1.
  4. virtiofsd lifecycle test: verify the socket is removed and no orphan virtiofsd remains after a clean QEMU exit or Ctrl-C. Addresses I2.
  5. Cross-distro QEMU smoke: at minimum, document which distros are known to work; better, replace pc-q35-noble with q35 so the script runs on Fedora/Debian/Arch without modification. Addresses I8.
  6. 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:

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


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.7Codex GPT 5.5Gemini 3.1 ProQwen 3.6 27BGemma 31B
Duration3m 50s4m 35s9m 37s10m 07s
Findings7I 7S6I 1S4I 1S4I 1S3I
Tool calls8 (Read 5, Bash 3)33 (shell 31, stdin 2)9 (read 6, bash 2, glob 1)5 (read 5)
Design observations21111
False positives00100
Unique insights61100
Files reviewed66666
Coverage misses00000
Totals7I 7S6I 1S4I 1S4I 1S3I
Downgraded001 (I→S)00
Dropped00100

Reconciliation:

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

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.