Superpowers-lab windows-vm
Create, manage, or connect to a headless Windows 11 VM running in Docker with SSH access. Use when the user wants to spin up, stop, restart, or SSH into a Windows VM.
git clone https://github.com/obra/superpowers-lab
T=$(mktemp -d) && git clone --depth=1 https://github.com/obra/superpowers-lab "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/windows-vm" ~/.claude/skills/obra-superpowers-lab-windows-vm && rm -rf "$T"
skills/windows-vm/SKILL.mdHeadless Windows 11 VM
Manage a headless Windows 11 VM running via dockur/windows in Docker with KVM acceleration. The VM is accessible via SSH only — no RDP or GUI required.
Host prerequisites
- Docker
- KVM support (
must exist — check with/dev/kvm
)ls /dev/kvm
(sshpass
)sudo apt install sshpass
(optional, for screenshot debugging:imagemagick
)sudo apt install imagemagick
Configuration
- Container name:
windows11 - VM directory:
$HOME/windows-vm/
— VM disk image (managed by dockur, wiped on recreate)storage/
— cached Windows ISO (7.3GB, persists across recreates)iso/win11x64.iso
— post-install script (installs OpenSSH Server)oem/install.bat
- Credentials: user / password
- SSH:
(bound to 127.0.0.1 only)localhost:2222 - RDP:
(bound to 127.0.0.1 only, fallback)localhost:3389 - Web console:
(VNC in browser, for debugging)localhost:8006 - Resources: 8GB RAM, 4 CPU cores, 64GB disk
Actions
create — First-time setup or full recreate
-
Ensure directories exist:
mkdir -p "$HOME/windows-vm/oem" "$HOME/windows-vm/storage" "$HOME/windows-vm/iso" -
Ensure
exists with OpenSSH setup:$HOME/windows-vm/oem/install.bat@echo off echo Installing OpenSSH Server... powershell -Command "Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0" 2>nul powershell -Command "Get-WindowsCapability -Online -Name OpenSSH.Server* | Add-WindowsCapability -Online" 2>nul dism /Online /Add-Capability /CapabilityName:OpenSSH.Server~~~~0.0.1.0 2>nul powershell -Command "Start-Service sshd" 2>nul powershell -Command "Set-Service -Name sshd -StartupType Automatic" powershell -Command "New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -PropertyType String -Force" powershell -Command "New-NetFirewallRule -Name 'OpenSSH-Server' -DisplayName 'OpenSSH Server' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22" powershell -Command "Get-Service sshd" 2>nul echo Done. -
If recreating, remove the old container and disk:
docker stop windows11 && docker rm windows11 rm -f "$HOME/windows-vm/storage/data.img" -
Launch the container. There are two cases:
If cached ISO exists (
):$HOME/windows-vm/iso/win11x64.isodocker run -d \ --name windows11 \ -p 127.0.0.1:3389:3389 \ -p 127.0.0.1:2222:22 \ -p 127.0.0.1:8006:8006 \ -e RAM_SIZE="8G" \ -e CPU_CORES="4" \ -e DISK_SIZE="64G" \ -e USERNAME="user" \ -e PASSWORD="password" \ --cap-add NET_ADMIN \ --device /dev/kvm \ -v "$HOME/windows-vm/storage:/storage" \ -v "$HOME/windows-vm/oem:/oem" \ -v "$HOME/windows-vm/iso/win11x64.iso:/boot.iso" \ dockurr/windowsFirst time (no cached ISO) — omit the
mount and add/boot.iso
:VERSIONdocker run -d \ --name windows11 \ -p 127.0.0.1:3389:3389 \ -p 127.0.0.1:2222:22 \ -p 127.0.0.1:8006:8006 \ -e RAM_SIZE="8G" \ -e CPU_CORES="4" \ -e DISK_SIZE="64G" \ -e VERSION="win11" \ -e USERNAME="user" \ -e PASSWORD="password" \ --cap-add NET_ADMIN \ --device /dev/kvm \ -v "$HOME/windows-vm/storage:/storage" \ -v "$HOME/windows-vm/oem:/oem" \ dockurr/windowsAfter the ISO downloads and Windows boots, immediately copy the ISO out before the container is ever stopped (dockur wipes
on recreate):/storagecp "$HOME/windows-vm/storage/win11x64.iso" "$HOME/windows-vm/iso/win11x64.iso" -
Wait for Windows install + OpenSSH setup to complete. This takes 20-30 minutes for a fresh install (the OEM install.bat runs at the end of Windows OOBE and downloads OpenSSH from Microsoft, which is slow). Monitor with:
docker logs -f windows11You can also watch the VM screen via the web console at
.http://localhost:8006To check if SSH is up:
sshpass -p 'password' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -p 2222 user@localhost "whoami" -
Once SSH is responding, install Node.js and Claude Code by piping a setup script via stdin (avoids PowerShell escaping hell over SSH):
cat << 'PS' | sshpass -p 'password' ssh -o StrictHostKeyChecking=no -p 2222 user@localhost "powershell -ExecutionPolicy Bypass -Command -" # Download and install Node.js silently Invoke-WebRequest -Uri 'https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi' -OutFile 'C:\Users\user\node-install.msi' Start-Process msiexec.exe -ArgumentList '/i C:\Users\user\node-install.msi /qn /norestart' -Wait -Verb RunAs Write-Host "Node.js installed" # Install Claude Code globally & 'C:\Program Files\nodejs\npm.cmd' install -g @anthropic-ai/claude-code Write-Host "Claude Code installed" # Add npm global bin to SYSTEM PATH (user PATH is not read by sshd) $systemPath = [Environment]::GetEnvironmentVariable('Path', 'Machine') $additions = @() if ($systemPath -notlike '*AppData*npm*') { $additions += 'C:\Users\user\AppData\Roaming\npm' } if ($systemPath -notlike '*Git\cmd*') { $additions += 'C:\Program Files\Git\cmd' } if ($additions.Count -gt 0) { [Environment]::SetEnvironmentVariable('Path', $systemPath + ';' + ($additions -join ';'), 'Machine') Write-Host "Added to system PATH: $($additions -join ', ')" } # Set execution policy machine-wide (required for claude.ps1) Set-ExecutionPolicy RemoteSigned -Scope LocalMachine -Force -ErrorAction SilentlyContinue # Create system-wide PowerShell profile that rebuilds PATH from registry on login. # Without this, interactive SSH sessions don't pick up the full system PATH. $profileDir = Split-Path $PROFILE.AllUsersAllHosts if (-not (Test-Path $profileDir)) { New-Item -ItemType Directory -Path $profileDir -Force } @' $machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine') $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') $env:Path = "$machinePath;$userPath" '@ | Set-Content -Path $PROFILE.AllUsersAllHosts -Force Write-Host "PowerShell profile created" # Restart sshd so it picks up the new PATH Restart-Service sshd -Force PSNote: the connection will drop when sshd restarts — that's expected.
-
Clear the stale host key (new VM = new host key) and verify:
ssh-keygen -f ~/.ssh/known_hosts -R '[localhost]:2222' sshpass -p 'password' ssh -o StrictHostKeyChecking=no -p 2222 user@localhost "claude --version"
start — Start a stopped VM
docker start windows11
stop — Stop the VM
docker stop windows11
restart — Restart the VM
docker restart windows11
status — Check VM status
docker ps -f name=windows11 --format "table {{.Status}}\t{{.Ports}}" docker logs windows11 2>&1 | tail -5
ssh — Connect to the VM
ssh -p 2222 user@localhost
screenshot — See what's on the VM screen (for debugging)
docker exec windows11 bash -c "echo 'screendump /tmp/screen.ppm' | nc -w 2 localhost 7100" > /dev/null 2>&1 sleep 1 docker cp windows11:/tmp/screen.ppm /tmp/screen.ppm convert /tmp/screen.ppm /tmp/screen.png
Important Notes
- ISO caching: The
volume is managed by dockur and gets wiped on recreate. Store the ISO separately in/storage
and mount it as$HOME/windows-vm/iso/
to skip the 7.3GB download./boot.iso
is required for port forwarding to work. Without it, QEMU falls back to user-mode networking and port forwarding silently fails.--cap-add NET_ADMIN
is required for hardware acceleration.--device /dev/kvm- Boot time: Fresh install takes 20-30 min (Windows install + OpenSSH download from Microsoft). Subsequent boots from existing
are fast (~2 min).data.img - Ports are bound to
only — not exposed to the network.127.0.0.1 - Do NOT use
when mounting-e VERSION="win11"
— the version is auto-detected from the ISO./boot.iso
Post-install gotchas
- Node.js is not pre-installed — the Claude Code install script (
) will report success butirm https://claude.ai/install.ps1 | iex
won't work without Node. Install Node.js via MSI first.claude - npm global bin not in PATH — Node's MSI adds
to PATH but notC:\Program Files\nodejs
(whereC:\Users\user\AppData\Roaming\npm
puts binaries). Must add it to the system PATH (not user PATH) because OpenSSH's sshd only reads system PATH. After changing system PATH, restart sshd.npm install -g - PowerShell execution policy — Default policy is
, which blocksRestricted
. Must set toclaude.ps1
at LocalMachine scope (not CurrentUser) for it to take effect in SSH sessions.RemoteSigned - Escaping hell — Running PowerShell commands over SSH with nested quotes is unreliable. Pipe scripts via stdin using
instead.powershell -ExecutionPolicy Bypass -Command - - Interactive SSH sessions don't get full PATH — Windows OpenSSH sshd doesn't properly propagate the system PATH to interactive PowerShell sessions. Fix: create a system-wide PowerShell profile (
) that rebuilds$PROFILE.AllUsersAllHosts
from the registry on every login.$env:Path - winget may not work — The Microsoft Store certificate can fail in a VM. Use direct MSI/installer downloads instead.
- Host key changes — Each recreated VM gets new SSH host keys. Run
to clear the old one.ssh-keygen -R '[localhost]:2222'