Back to blog
FILE 0xE4·DRIVING A WINDOWS VM INSTALL WITH QM SENDKEY

Driving a Windows VM install with qm sendkey

May 13, 2026 · proxmox, windows, automation

I needed to slipstream a Windows 11 install across a handful of VMs on Proxmox without sitting in front of a noVNC window pressing Enter for an hour. The unattended XML handles most of it, but Windows Setup still has a few prompts where the answer file silently falls through and a human is expected to hit a key.

What was happening

The install would get to OOBE FirstLogonCommands, sit for a couple of minutes while the OpenSSH server feature pulled itself from Windows Update, then continue. But before that, there's a stretch around the partitioning step where the unattended XML and the actual disk layout I'd allocated disagreed in a way that prompted an "are you sure" dialog. Without an Enter, the install hung indefinitely.

I'd been using a monitor wrapper script that polled the qemu monitor for output and sent keystrokes back through the same channel. It was flaky — the monitor socket would drop, the wrapper would lose track of state, and I'd come back to a stalled install.

What I found

qm sendkey is a built-in Proxmox CLI command that pushes a single keystroke directly into the VM's input device, bypassing the monitor wrapper entirely. It's stateless, it doesn't care about prior socket state, and it works whether or not you have a noVNC session attached.

qm sendkey <vmid> ret           # press Enter
qm sendkey <vmid> tab           # press Tab
qm sendkey <vmid> spc           # press Space

The other piece is that you need a USB keyboard attached in the VM's config for input to actually route somewhere. The Proxmox default for q35 machine types is a PS/2 keyboard, which works for boot-time stuff but isn't always recognized by Windows Setup's later phases.

The fix

A small loop that fires Enter every three seconds for the duration of the install, with USB keyboard pinned in the VM args:

# add USB keyboard to VM args (one-time, before install starts)
qm set <vmid> -args '-device usb-kbd,bus=ehci.0'

# during install, in a loop
while ! vm_install_complete <vmid>; do
  qm sendkey <vmid> ret
  sleep 3
done

The "complete" check is the other half — I watch for the same screen for too long, then either send Enter and keep going, or post a stuck-warning to push notification. Stable screen count gets reported every 15 seconds; stuck warnings (no change for >5 minutes) get an alert.

last_hash=""; same_count=0
while true; do
  hash=$(qm screendump <vmid> /tmp/s.png && sha256sum /tmp/s.png | cut -c1-12)
  if [ "$hash" = "$last_hash" ]; then
    same_count=$((same_count + 1))
    [ $same_count -gt 100 ] && notify "VM <vmid> stuck"
  else
    same_count=0
    last_hash=$hash
  fi
  sleep 3
done

qm screendump is the trick — it grabs the framebuffer to a PNG, and hashing the bytes is a cheap "did the screen change" signal.

What I'd do differently

I should have skipped the monitor wrapper from the start. The hierarchy of trust on Proxmox is qm commands > monitor socket > anything you script on top. qm sendkey and qm screendump together cover 90% of what a "drive an install via automation" workflow needs, without the moving parts of a long-lived socket connection. The remaining 10% — e.g., needing to type a full string instead of single keys — can be handled by chaining qm sendkey calls with the right delays in between, or by leaning harder on the unattended XML so you never need to type anything in the first place.