internals/skills/ovmf/SKILL.md
UEFI firmware (OVMF_CODE + OVMF_VARS) path resolution for VMs. Covers the per-distro path table (Fedora vs Arch vs Debian/Ubuntu), per-VM NVRAM copies pattern, secure-boot variants, and the "empty strings = skip loader/nvram" contract for firmware: bios. Source: charly/ovmf_paths.go. MUST be invoked before editing UEFI firmware resolution.
npx skillsauth add overthinkos/overthink-plugins ovmfInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Per-distro UEFI firmware path resolution for VMs. ResolveOvmfPaths picks the correct OVMF_CODE + OVMF_VARS pair for the host distro + secure-boot setting; EnsurePerVmNvram copies the shared OVMF_VARS template to a per-VM writable NVRAM file on first use. ResolveOvmfForSpec is the convenience wrapper that reads spec.Firmware and dispatches.
Key contract: when spec.Firmware == "bios" or empty, ResolveOvmfForSpec returns ("", "", nil) — no OVMF package dependency, no per-VM NVRAM file, no <loader>/<nvram> emission. The libvirt renderer uses these empty strings as a sentinel to skip UEFI entirely.
charly/ovmf_paths.go — ~185 LOC; standalone, no other dependencies inside charly/.
Fedora / CentOS / RHEL / Rocky / Alma:
insecure: /usr/share/OVMF/OVMF_CODE.fd + /usr/share/OVMF/OVMF_VARS.fd
secure: /usr/share/OVMF/OVMF_CODE.secboot.fd + /usr/share/OVMF/OVMF_VARS.secboot.fd
(fallback): /usr/share/edk2/ovmf/... — pre-F40
Arch / Manjaro / EndeavourOS:
insecure: /usr/share/edk2/x64/OVMF_CODE.4m.fd + /usr/share/edk2/x64/OVMF_VARS.4m.fd
secure: /usr/share/edk2/x64/OVMF_CODE.secboot.4m.fd + (same VARS template)
(fallback): /usr/share/edk2-ovmf/x64/... — pre-edk2-ovmf-202411 repackage
Debian / Ubuntu:
insecure: /usr/share/OVMF/OVMF_CODE_4M.fd + /usr/share/OVMF/OVMF_VARS_4M.fd
secure: /usr/share/OVMF/OVMF_CODE_4M.ms.fd + /usr/share/OVMF/OVMF_VARS_4M.ms.fd
(fallback): /usr/share/OVMF/OVMF_CODE.fd — older non-4M paths
Multiple candidates per distro handle historical path drift. ResolveOvmfPaths returns the first candidate pair where both files exist. Clean error when no candidate matches, with a distro-appropriate install hint:
| Distro family | Install hint |
|---|---|
| Fedora / CentOS / RHEL | sudo dnf install edk2-ovmf |
| Arch | sudo pacman -S edk2-ovmf |
| Debian / Ubuntu | sudo apt-get install ovmf |
EnsurePerVmNvram(templatePath, perVmDir) copies the OVMF_VARS template to <perVmDir>/nvram.fd on first use. Idempotent: if the per-VM file already exists, it's preserved (the guest has accumulated UEFI variables into it — boot entries, secure-boot state, etc., which must survive reboot).
The shared template stays read-only. Every VM that boots with UEFI gets its own NVRAM copy; no two VMs share NVRAM state.
Per-VM NVRAM path convention: ~/.local/share/charly/vm/charly-<vm-name>/nvram.fd.
firmware: uefi-secure hard-wires the VM to Microsoft UEFI CA keys (enrolled in the secure-boot OVMF_VARS template). Unsigned EFI binaries fail to boot; dual-booting unsigned kernels or self-built EFI loaders requires enrolling custom keys through the OVMF console. Not reversible without a fresh per-VM NVRAM.
firmware: uefi-insecure uses the same OVMF binary but boots any EFI payload. Most cloud images ship signed BOOTX64.EFI binaries that work under either mode; test before committing.
From /charly-vm:arch Finding B: UEFI boot with a stale BOOTX64.EFI (embedded grub.cfg older than the image's on-disk /boot/grub/grub.cfg) is a common failure mode for distribution cloud images. BIOS boot sidesteps it entirely — GRUB reads /boot/grub/grub.cfg from the root filesystem, which is always current.
When spec.Firmware == "bios":
func ResolveOvmfForSpec(spec *VmSpec, vmStateDir string) (codePath, nvramPath string, err error) {
if spec.Firmware == "" || spec.Firmware == "bios" {
return "", "", nil // ← the sentinel
}
// ... UEFI path below
}
The libvirt renderer (buildDomainOS in libvirt_yaml_bridge.go) emits <loader> + <nvram> only when VmRuntimeParams.OVMFCodePath / .NVRAMPath are non-empty; for BIOS, ResolveOvmfForSpec returns empty paths (resolved upstream in vm_create_spec.go) so both are skipped. QEMU then boots via SeaBIOS (shipped with QEMU itself, no separate package). No OVMF on the host package-manager list. No per-VM NVRAM file. No Secure Boot lock-in.
When to pick BIOS:
When to pick UEFI:
/charly-internals:vm-spec — spec.Firmware field/charly-internals:libvirt-renderer — RenderDomainXML consumer; <loader>/<nvram> emission conditions/charly-vm:vm — command-family; BIOS vs UEFI decision matrix/charly-vm:arch — live-test RCA showing why firmware: bios is the right default for Arch cloud imagetools
Use when authoring or modifying a charly PLUGIN — a candy with a `plugin:` block that contributes Providers (verbs/kinds/deploy-targets/steps/builders/commands), its own CUE schema, builtin (compiled-in) or external (out-of-tree git repo). Covers the unified Provider model, the per-plugin CUE-schema contract (single source → Go params for dev + schema-over-Describe RPC for runtime), the SDK, and the loader.
tools
The CUE data-validation / configuration CLI (cue), pinned to v0.16.1. Use when working with the cue candy, installing the cue binary into a box or onto a target:local dev host, or running the offline schema-vendoring pipeline that feeds charly's egress validation.
tools
CUE EGRESS validation — validating (and, where it adds value, generating) the config files charly WRITES to a system BEFORE the bytes hit disk. MUST be invoked before working on charly/egress.go, the vendored schemas under candy/plugin-egress/egress-schemas/vendor/, the ValidateEgress / registerVendoredEgressKind path, the offline `task cue:vendor` pipeline, or adding an egress schema for any written artifact (cloud-init, k8s manifests, traefik routes, runtime config, install ledger, systemd/quadlet units, ssh_config, libvirt XML).
tools
Kubernetes cluster-probe declarative check verb — the `kube:` check verb (nodes, pods, ingress, storage class, addon health, apply/delete, and arbitrary resource GETs) served out-of-process by the candy/plugin-kube plugin (vendored client-go; no external kubectl required).