CRITICAL: Agent Access Limitations
YOU DO NOT HAVE ACCESS TO PROXMox
Agent runs on your local machine - NOT on Proxmox
Forbidden:
- Try to run qm commands
- Try to run pvesh commands
- Check storage availability
- Verify VM creation
- Access Proxmox API
Must Do:
- Output commands as text for user to copy/paste
- Tell user which node to SSH to
- Store keys locally in a configurable directory (default: ~/.ssh/pve-builder/)
- Never claim to create VMs
- Use web search for specs validation
PVE Builder Skill
Overview
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.
Environment Setup
- Config file:
pve-env.mdin the skill directory - Ignored from git:
.gitignoreexcludespve-env.md
Critical Configuration Keys (pve-env.md)
| Section | Keys | Purpose |
|---|---|---|
| Proxy | Proxy Required, HTTP Proxy, HTTPS Proxy, Proxy CA Certificate | Network proxy for apt inside VMs |
| SSH | Default User, Key Path, Key Type | Default SSH user, key storage location, key type |
| Network | Default Bridge, Default VLAN, DNS Server, Use DHCP Default, Network Interface | Default network settings and interface type |
| Storage | Default Storage, Template Path, Default OS Disk Size, Auto-Format Data Disks, Data Disk Interface, Default Cloud Image | Storage defaults and cloud image path |
| Node | Default Node, BIOS Type, Machine Type, CPU Type, OS Type, SCSI Controller, Onboot | Hardware defaults for VM creation |
| Workload Presets | Preset table (RAM/CPU/Disk) | Recommended specs per workload type |
| Package Defaults | Package Update, Base Packages | Always-installed package list |
Agent Workflow
The workflow uses section-based numbered prompts with continuous numbering across sections:
=== 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:
- Load
pve-env.md(error if missing) - Ask cloud image path (default from config:
Template Path+Default Cloud Image) - Ask Proxmox node (default from config)
- Validate storage/bridge/image (see Validation section below)
- Ask VM name
- Software lookup (name or URL) → web search for RAM/CPU recommendations (or manual)
- Prompt specs (numbered prompts: cores, sockets, RAM, OS disk)
- Prompt network (bridge, VLAN, DHCP vs static)
- Static IP details (only if no DHCP)
- Prompt SSH username
- Prompt data disks (count, sizes, formatting option)
- Proxy configuration (yes/no/change)
- Extra apt packages
- SSH key directory (default from config)
- Password setup — always generate a random password via
openssl rand -base64 12 | tr -d '/+=' | head -c16. Use this inchpasswd. If the user explicitly provides a password, use theirs instead. Always show the password in the final output summary. - Generate SSH key (unique ed25519 per VM)
- Show summary & confirm
- VMID auto-detection — Ask the user to run
pvesh get /cluster/nextidon 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 takencomment in the output. - Build cloud-init user-data YAML (packages, proxy, data disk formatting)
- Pre-flight validation (see Command Pre-flight Validation below) — generate commands internally, validate, fix errors, then present
- Generate and display the final verified commands in two sections: Setup commands (create VM through
qm start) and Post-boot cleanup (delete the snippets YAML — only run after the VM is verified up). - Optional: save commands to file
- Show SSH key path and chmod reminder
Validation
Before generating commands, the agent validates that the target storage, bridge, and cloud image exist on the Proxmox node.
Cache System
- Cache file:
~/.pve-builder/validation.json - Valid for: 24 hours
- Cache invalidated if: node, storage, or bridge values change
- On cache hit: validation is skipped if all checks passed
Validation Process
If no valid cache exists, the agent shows these commands for the user to run on the Proxmox node:
echo "=== Storage ==="; pvesm status
echo "=== Bridge ==="; ip -br link show
echo "=== Image ==="; ls -la <image-path>
echo "=== END ==="
Results are parsed:
- Storage: Checks if configured storage name exists in
pvesm status - Bridge: Verifies bridge interface is present and UP
- Image: Confirms cloud image file exists at path
On failure: Agent aborts and reports which check(s) failed. On success: Results are cached with node/storage/bridge/timestamp.
Notes
- Custom cloud-init user-data is written to
/var/lib/vz/snippets/<VMNAME>-user-data.yamland attached via--cicustom "user=local:snippets/<VMNAME>-user-data.yaml". Thelocal: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. --cicustomreads 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.- Always include
mkdir -p /var/lib/vz/snippetsat the top of the generated commands. - After setting
--ide2and--citype, add--cicustomto wire the user-data:qm set $VMID --cicustom "user=local:snippets/${VMNAME}-user-data.yaml". - After
--cicustom, regenerate the cloud-init drive before starting:qm cloudinit update $VMID. - ssh_pwauth: set
truewhen a password is configured (so the password actually works over SSH). Setfalseonly for SSH-key-only VMs. - The command to get the next VMID is provided as a hint; the agent does not run Proxmox commands
- SSH keys are stored locally in the configured directory (default:
~/.ssh/pve-builder/) - Generated commands include cleanup steps at the end: lists cloud-init YAML files for review, then removes the current VM's file. IMPORTANT: The cleanup step (rm snippets YAML) must NOT use
set -eor 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. - Always include
echo "VMID: <id>"in the verify/final section so the user has the VMID referenced
Command Pre-flight Validation
Never present commands to the user without running this validation first. Generate → validate → fix → present.
Phase 1: Internal Draft
Build the complete command set internally (do not show yet).
Phase 2: Parameter Verification Script
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.
#!/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:
- Writes this script to a temp file with actual VM values
- Runs it with
exec bash /tmp/preflight.sh - If it fails, fixes values, re-runs
- Only presents commands when
PREFLIGHT OK
Checklist (run after pre-flight script passes)
- Shebang/header with VM name, VMID, node
-
mkdir -p /var/lib/vz/snippets - User-data written to
/var/lib/vz/snippets/$VMNAME-user-data.yaml -
ssh_pwauth: trueif password configured,falseif SSH-only -
--citype nocloud(NOTcloud-config) -
qm createwith no--node -
qm importdiskreferences correct image -
--scsi0attaches imported disk -
--ide2 <storage>:cloudinitattached -
--cicustom "user=local:snippets/${VMNAME}-user-data.yaml"added -
qm cloudinit update $VMIDafter 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=scsi0 -
qm resize $VMID scsi0 <size>after import -
echo "VMID: $VMID"in output -
qm config $VMIDfor review - Cleanup note at end (remove snippets YAML)
- No typos:
qmnotvm,$VMIDnot$VM - Variable names consistent throughout
Networking
DHCP (default)
Simple 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.
Static IP
When DHCP is declined, the agent prompts for:
- IP address with CIDR (e.g.,
10.0.12.50/24) - Gateway (e.g.,
10.0.12.1) - DNS servers (comma-separated, default from config)
Generated commands:
qm set --ipconfig0 ip=10.0.12.50/24,gw=10.0.12.1qm set --nameserver 8.8.8.8
Package Installation
All 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.
Security
- Passwords: ALL VMs get a random password by default. Generate with:
openssl rand -base64 12 | tr -d '/+=' | head -c16. Use in bothchpasswdin 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. Setssh_pwauth: trueunless the user explicitly says SSH-only. - SSH keys: Unique per VM, ed25519, no passphrase
- pve-env.md: chmod 600, excluded from git
- Private keys: chmod 600, never in commands
- Public keys: Safe to embed in commands
- SSH key directory: configurable via
Key Pathin pve-env.md (default~/.ssh/pve-builder); permissions 700 on base dir
Version History
- 1.0.10: Add
--cicustomwarning: snippets file must survive first boot. Split output into Setup and Post-boot sections. VMID auto-detection viacluster/nextid. Fix workflow numbering. - 1.0.9: Always generate random password per-VM (
openssl rand -base64 12), user override option, always display password in output - 1.0.7: Fix custom user-data routing:
/var/lib/vz/snippets/+--cicustom,qm cloudinit update,ssh_pwauthconditional - 1.0.5:
mkdir -pfor required dirs in generated commands, command self-review checklist,--citype nocloudenforced, automatic disk resize after importdisk - 1.0.3: Full config decoupling (all hardware defaults from pve-env.md), storage/bridge validation with 24h cache, static IP support, continuous numbered prompts with section headers, SSH key path not echoed in summary, duplicate package removal, direct VMID input, cloud-init cleanup commands
- 1.0.2: Added URL analysis, web search validation, simplified proxy flow
- 1.0.1: Added explicit access limitation warnings
- 1.0.0: Initial release
This file is yours to evolve. As you learn who you are, update it.