Agent runs on your local machine - NOT on Proxmox
Forbidden:
Must Do:
Generates Proxmox VM creation commands with cloud-init configuration, SSH key management, and optional data disks. All hardware defaults are config-driven via pve-env.md.
IMPORTANT: Commands are output as text for you to copy/paste into Proxmox shell. The agent does NOT execute any Proxmox commands.
pve-env.md in the skill directory.gitignore excludes pve-env.mdSectionKeysPurposeProxyProxy Required, HTTP Proxy, HTTPS Proxy, Proxy CA CertificateNetwork proxy for apt inside VMsSSHDefault User, Key Path, Key TypeDefault SSH user, key storage location, key typeNetworkDefault Bridge, Default VLAN, DNS Server, Use DHCP Default, Network InterfaceDefault network settings and interface typeStorageDefault Storage, Template Path, Default OS Disk Size, Auto-Format Data Disks, Data Disk Interface, Default Cloud ImageStorage defaults and cloud image pathNodeDefault Node, BIOS Type, Machine Type, CPU Type, OS Type, SCSI Controller, OnbootHardware defaults for VM creationWorkload PresetsPreset table (RAM/CPU/Disk)Recommended specs per workload typePackage DefaultsPackage Update, Base PackagesAlways-installed package list
The workflow uses section-based numbered prompts with continuous numbering across sections:
text
=== VM Specs ===
1. CPU cores 2. CPU sockets 3. RAM in GB 4. OS disk size
=== Network ===
5. Bridge 6. VLAN 7. DHCP?
[if static:] 8. IP 9. Gateway 10. DNS
=== User & Disks ===
11. SSH user 12. Add data disks? 13. Format? 14. Count 15.x: Disk sizes
15. Proxy? 16. Extra packages 17. SSH key directory
Steps:
pve-env.md (error if missing)Template Path + Default Cloud Image)openssl rand -base64 12 | tr -d '/+=' | head -c16. Use this in chpasswd. If the user explicitly provides a password, use theirs instead. Always show the password in the final output summary.pvesh get /cluster/nextid on the Proxmox node and paste back the result. Use that VMID in all generated commands. Never hardcode a VMID — always get the next ID from the cluster. Add a # Replace VMID=... if already taken comment in the output.qm start) and Post-boot cleanup (delete the snippets YAML — only run after the VM is verified up).Before generating commands, the agent validates that the target storage, bridge, and cloud image exist on the Proxmox node.
~/.pve-builder/validation.jsonIf no valid cache exists, the agent shows these commands for the user to run on the Proxmox node:
bash
echo "=== Storage ==="; pvesm status
echo "=== Bridge ==="; ip -br link show
echo "=== Image ==="; ls -la <image-path>
echo "=== END ==="
Results are parsed:
pvesm statusOn failure: Agent aborts and reports which check(s) failed. On success: Results are cached with node/storage/bridge/timestamp.
/var/lib/vz/snippets/<VMNAME>-user-data.yaml and attached via --cicustom "user=local:snippets/<VMNAME>-user-data.yaml". The local:snippets/ storage path maps to /var/lib/vz/snippets/ on the Proxmox node. Never use /var/lib/vz/template/cloud-init/ — that directory is not a recognized Proxmox snippets storage target.--cicustom reads the snippets file at every boot. The snippets YAML must exist on the node when the VM starts — cloud-init won't apply without it. Do NOT delete the snippets file before the first boot.mkdir -p /var/lib/vz/snippets at the top of the generated commands.--ide2 and --citype, add --cicustom to wire the user-data: qm set $VMID --cicustom "user=local:snippets/${VMNAME}-user-data.yaml".--cicustom, regenerate the cloud-init drive before starting: qm cloudinit update $VMID.true when a password is configured (so the password actually works over SSH). Set false only for SSH-key-only VMs.~/.ssh/pve-builder/)set -e or be in the main command block. It should be in a separate "Post-boot" section that runs only after the VM is verified running and cloud-init applied. If the user deletes the YAML before the first boot, cloud-init fails silently.echo "VMID: <id>" in the verify/final section so the user has the VMID referencedNever present commands to the user without running this validation first. Generate → validate → fix → present.
Build the complete command set internally (do not show yet).
Generate a small bash validation script, set the variables to match the VM specs, and run it locally with exec. If it fails, fix the draft and re-run until it passes. Only then show the commands.
bash
#!/bin/bash
# Pre-flight: verify parameters match intended spec
# SET THESE to match the VM being built:
VMID="1024"; VMNAME="MB-TBD"; RAM_MB="12288"; CORES="2"; SOCKETS="1"
OS_DISK_SIZE="25G"; BRIDGE="vmbr0"; VLAN_TAG="160"; STORAGE="Data"
IMAGE="/mnt/pve/ISO/template/iso/ubuntu-24.04-server-cloudimg-amd64.img"
CITYPE="nocloud"; VM_GB="12"
PASS=true
# RAM must be multiple of 256
(( RAM_MB % 256 != 0 )) && { echo "FAIL: RAM $RAM_MB not mult. of 256"; PASS=false; }
# Cores/sockets positive
(( CORES < 1 )) && { echo "FAIL: CORES < 1"; PASS=false; }
(( SOCKETS < 1 )) && { echo "FAIL: SOCKETS < 1"; PASS=false; }
# VLAN in valid range
(( VLAN_TAG < 1 || VLAN_TAG > 4094 )) && { echo "FAIL: VLAN out of range"; PASS=false; }
# Disk size format
[[ ! "$OS_DISK_SIZE" =~ ^[0-9]+[G]$ ]] && { echo "FAIL: OS_DISK_SIZE format"; PASS=false; }
# Valid CITYPE
case "$CITYPE" in nocloud|configdrive2|opennebula) ;; *) echo "FAIL: CITYPE=$CITYPE invalid"; PASS=false ;; esac
# Image path not empty
[ -z "$IMAGE" ] && { echo "FAIL: IMAGE path empty"; PASS=false; }
# RAM_MB must match VM_GB * 1024
EXPECTED_MB=$((VM_GB * 1024))
[ "$RAM_MB" -ne "$EXPECTED_MB" ] && { echo "FAIL: RAM_MB=$RAM_MB != VM_GB=$VM_GB (expected $EXPECTED_MB)"; PASS=false; }
$PASS && echo "PREFLIGHT OK" || { echo "PREFLIGHT FAILED"; exit 1; }
The agent:
exec bash /tmp/preflight.shPREFLIGHT OKmkdir -p /var/lib/vz/snippets/var/lib/vz/snippets/$VMNAME-user-data.yamlssh_pwauth: true if password configured, false if SSH-only--citype nocloud (NOT cloud-config)qm create with no --nodeqm importdisk references correct image--scsi0 attaches imported disk--ide2 <storage>:cloudinit attached--cicustom "user=local:snippets/${VMNAME}-user-data.yaml" addedqm cloudinit update $VMID after cicustom set--ipconfig0 ip=dhcp (DHCP) or --ipconfig0 ip=<CIDR>,gw=<GW> (static) — this actually configures the network inside the guest--net0 virtio,bridge=<bridge>,tag=<vlan>--boot order=scsi0qm resize $VMID scsi0 <size> after importecho "VMID: $VMID" in outputqm config $VMID for reviewqm not vm, $VMID not $VMSimple network config: qm set --ipconfig0 ip=dhcp
Do NOT use --nameserver with DHCP — let the DHCP lease provide DNS. Only set --nameserver when using static IP.
When DHCP is declined, the agent prompts for:
10.0.12.50/24)10.0.12.1)Generated commands:
qm set --ipconfig0 ip=10.0.12.50/24,gw=10.0.12.1qm set --nameserver 8.8.8.8All VMs get base packages from pve-env.md (deduplicated with any extra packages).
If proxy is configured, apt proxy is automatically enabled in cloud-init.
openssl rand -base64 12 | tr -d '/+=' | head -c16. Use in both chpasswd in cloud-init YAML and embed in command output. If the user provides an explicit password, use theirs instead — but always show it back in output so they have it. Set ssh_pwauth: true unless the user explicitly says SSH-only.Key Path in pve-env.md (default ~/.ssh/pve-builder); permissions 700 on base dir--cicustom warning: snippets file must survive first boot. Split output into Setup and Post-boot sections. VMID auto-detection via cluster/nextid. Fix workflow numbering.openssl rand -base64 12), user override option, always display password in output/var/lib/vz/snippets/ + --cicustom, qm cloudinit update, ssh_pwauth conditionalmkdir -p for required dirs in generated commands, command self-review checklist, --citype nocloud enforced, automatic disk resize after importdiskThis file is yours to evolve. As you learn who you are, update it.