MVM had a command called cleanup --nuclear.
It did not delete everything.
It deleted every directory somebody had remembered to add to a list.
As the project grew, the list did not. Images, machines, checkpoints, snapshots, instances, pool state, shares, runtime binaries, observers, and other state survived a flag whose name promised the opposite.
The bug was not a missing path.
The bug was the definition of “everything.”
Closed lists fail in open systems
Selective cleanup needs a list.
If --cache means “remove rebuildable caches,” the command should name the cache directories it owns. Unknown state should be left alone because the operation is intentionally narrow.
A nuclear cleanup is different.
Its contract is open-ended: remove every entry under the MVM root, including entries added by future subsystems.
That cannot be implemented with a historical allow-list.
The correct set is:
where:
- is the set of entries that exist now
- is the explicitly preserved set
- is what gets deleted
Enumerate the root. Delete what is there. Define exceptions by subtraction.
flowchart TB
R[Enumerate current MVM root] --> Q{keep identity?}
Q -->|no| A[Delete every entry]
Q -->|yes| S[Subtract identity set]
S --> D[Delete everything else]
A --> V[Leave root directory in place]
D --> V
A new subsystem is covered on the day it lands. Nobody has to remember to update the cleanup command.
The root directory stays
The implementation leaves the MVM root itself in place.
That is a small operational detail with a useful consequence: the directory’s ownership and 0700 mode do not have to be recreated later by a different code path.
Delete the contents, not the boundary that protects them.
It also gives the command a stable place to operate from while entries disappear.
Identity is not one key file
Once nuclear deletion really meant everything, I wanted one practical escape hatch:
--keep-identity
That sounds like “keep the signing key.”
It is wider than that.
A cryptographic identity is only useful with the state that gives it meaning:
- host signing keys
- chain-signed audit history
- attestation state
- encrypted secret store
- secret-binding metadata
- egress certificate authority
- secret-store encryption key
- snapshot key
- hand-written configuration
Preserving the audit chain without the signer makes future verification awkward or impossible.
Preserving encrypted secrets without their key preserves an unreadable blob.
Preserving a certificate authority while deleting every binding that explains how it was used loses the operational context.
The keep set is a connected state set:
flowchart LR
K[Signing keys] --> A[Audit chain]
K --> T[Attestations]
S[Secret-store key] --> E[Encrypted secrets]
E --> B[Secret bindings]
C[Egress CA] --> B
P[Snapshot key] --> H[Checkpoint history]
CFG[config.toml] --> K
CFG --> C
These files travel together because partial preservation can be worse than explicit deletion. It creates the appearance of recoverability without the ability to verify or decrypt.
Rebuildable and irreplaceable state
The cleanup design became clearer after dividing state into two classes.
Rebuildable or refetchable
- images
- kernels
- runtime overlays
- templates
- downloaded artifacts
- generated machine state
- caches
Irreplaceable or identity-bearing
- private keys
- audit history
- encrypted user secrets
- certificate authority material
- hand-written policy configuration
This does not mean identity-bearing state should never be deleted. A true nuclear wipe should delete it.
It means the operator should understand that destroying it changes more than disk usage. Past records may become unverifiable. Stored secrets may become undecryptable. Existing trust relationships may no longer make sense.
The command now says that plainly and requires an interactive DELETE-EVERYTHING confirmation.
--yes is not enough.
The running-VM check was also wrong
Making deletion complete raised the stakes of another bug.
The safety guard checked whether a VM was running by looking for libkrun.pid.
That worked for libkrun.
It did not work for HVF, Firecracker, or QEMU.
On macOS, HVF is the preferred backend. A running HVF guest could therefore appear stopped to the cleanup command.
Before the nuclear fix, that bug was partly masked because many live state directories were not deleted anyway. Once the command really removed the whole tree, the stale liveness check became dangerous.
The fix was not to add four more PID filenames to the cleanup module.
MVM already had a shared backend-aware liveness probe. Cleanup needed to use it.
That sounds obvious after the fact. It is a recurring systems problem: operational edge commands often reimplement a simplified version of the main runtime’s model.
The simplified version survives until the system adds another backend.
Operational commands belong on the same abstractions
The main launch path already knew:
- which backend was running
- which PID marker it owned
- how to distinguish a live process from stale state
- how to treat zombies
- which state directory belonged to the instance
Cleanup had a local shortcut.
That shortcut was cheaper when there was one backend and wrong when there were several.
The rule I am taking from this is simple:
Destructive tools must use the same ownership and liveness abstractions as the runtime they can destroy.
Not a similar copy. The same code.
This applies to more than VMs:
- database compaction
- cache eviction
- container cleanup
- certificate rotation
- garbage collection
- orphan reaping
- uninstallers
The edge command is where stale assumptions become data loss.
“Keep identity” still deletes a lot
The safe-sounding flag does not make nuclear cleanup gentle.
With --keep-identity, MVM still removes templates, machines, images, checkpoints, snapshots, pools, shares, binaries, and other generated state.
That is why the interactive confirmation remains mandatory.
Preserving proof of identity is not preserving the environment.
It means the operator can rebuild while keeping continuity:
- old audit records remain verifiable
- stored secrets remain decryptable
- the host retains its signing identity
- policy configuration survives
- new artifacts can be fetched under the same trust root
That is the intended use case: reclaim the machine without pretending it is a new machine.
Deletion needs a model, not a loop
The first implementation of cleanup looked like filesystem code.
The corrected implementation is closer to a state model.
It answers:
- What is the operation’s scope?
- Is the scope selective or complete?
- Which state is generated?
- Which state carries identity?
- Which entries must remain coupled?
- Which live owners can still be using the state?
- What evidence becomes unusable after deletion?
- What must the operator type before the command proceeds?
The remove_dir_all calls are the least interesting part.
Names are contracts too
A flag called --nuclear creates an expectation.
The implementation either has to meet it or use a less dramatic name.
I prefer fixing the contract.
“Everything” now means every current entry under the root. Future subsystems are included automatically. The exception is explicit and narrow: preserve the cryptographic identity set if the operator asks.
That definition will age much better than another longer list.
What survived the rewrite
The cleanup command now has a distinction I wish more systems made:
- selective operations are defined by inclusion
- complete operations are defined by exclusion
And identity is treated as a graph of coupled state rather than a single secret file.
Those are small ideas.
They showed up in a cleanup bug, but they apply across the rest of the system.
When you delete a machine’s state, you are also deciding whether it remains the same machine afterward.
Sometimes the right answer is no.
Sometimes you want to wipe almost everything and keep the ability to prove who you are.
Beneath the Agent: Start with The Safest NIC Is No NIC