The command had a dramatic name and a surprisingly timid implementation. I expected --nuclear to leave an empty machine; it deleted a list of directories somebody had remembered to write down.
MVM exposed:
cleanup --nuclear
As MVM grew, that list did not. Images, machines, checkpoints, snapshots, instances, pool state, shares, runtime binaries, observers, and newer subsystems could survive a cleanup whose name suggested nothing would.
The bug looked like a few missing paths. It was really a bad definition of everything.
Fixing that definition led to a harder question: if the operator wants to wipe the machine but keep its cryptographic identity, what exactly has to survive together?
The answer was much larger than one private key.
A closed list cannot implement an open promise
Selective cleanup should be conservative.
If --cache means “remove rebuildable caches,” the command should enumerate the cache directories it owns and leave unknown state alone. The operation is intentionally narrow, so a closed list makes sense.
A nuclear cleanup has the opposite contract. It means every current entry under the MVM root, including entries introduced by subsystems that did not exist when the cleanup code was written.
An allow-list of historical directory names can never keep that promise for long.
The correct set is:
where is the set of entries that exist now, is an explicitly preserved set, and is what gets deleted.
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
The implementation now enumerates the root and removes what is actually there. A new subsystem is covered on the day it lands; nobody has to remember to teach the old cleanup list its name.
That is the difference between defining a complete operation by inclusion and defining it by exclusion.
Keep the boundary, remove the contents
The MVM root directory itself stays in place.
At first this felt like a small implementation detail. It preserves something useful: the directory’s ownership and 0700 mode do not have to be reconstructed later by a different code path.
The command empties the protected boundary without deleting the boundary itself. It also gives the cleanup routine a stable location from which to remove entries while the tree changes underneath it.
Destructive filesystem work becomes easier to reason about when the operation has one root that never disappears during the walk.
“Keep identity” is not “keep the key”
Once nuclear cleanup really meant everything, I wanted one escape hatch:
--keep-identity
My first mental model was a signing key. Preserve the private key, delete everything else, and let the host rebuild under the same identity.
That model fell apart as soon as I followed the dependencies.
A signing key without the audit history it signed preserves a principal but loses continuity. An audit chain without the key material and attestation context may become difficult or impossible to verify later. An encrypted secret store without its encryption key is a durable unreadable blob. A certificate authority without the bindings that explain where it was used loses its operational meaning.
The identity set is connected state:
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
In MVM, the preserve set includes host signing keys, chain-signed audit history, attestation state, the encrypted secret store, secret-binding metadata, the egress certificate authority, secret-store and snapshot keys, and hand-written configuration.
These files travel together because partial preservation can be more misleading than deletion. It creates the appearance that recovery is possible while quietly removing the material required to decrypt or verify the state that remains.
Rebuildable state and identity-bearing state feel different when deleted
The cleanup model became clearer after dividing the root into two broad classes.
Images, kernels, runtime overlays, templates, downloaded artifacts, generated machine state, and caches can be rebuilt or fetched again. Losing them costs time and bandwidth.
Private keys, audit history, encrypted user secrets, certificate authority material, and hand-written policy configuration are different. Losing them may make past evidence unverifiable, stored secrets undecryptable, and existing trust relationships meaningless.
That does not mean a nuclear wipe should refuse to delete identity. Sometimes the operator really does want a new machine in every meaningful sense.
It means the confirmation should describe the consequence honestly.
MVM now requires the operator to type:
DELETE-EVERYTHING
--yes is not enough.
The friction is deliberate because the command is not merely reclaiming disk space. It may be severing the machine’s continuity with its own history.
The safety check was using yesterday’s backend
Making deletion complete increased the cost of another shortcut.
The cleanup guard decided whether a VM was running by looking for libkrun.pid. That worked when libkrun was the path everyone had in mind. It did not work for HVF, Firecracker, or QEMU.
On macOS, where HVF is the preferred backend, a live VM could therefore appear stopped to the cleanup command.
Before the nuclear rewrite, that bug was partly hidden because many state directories survived anyway. Once the command truly removed the whole tree, the stale liveness check could turn into active data loss.
The tempting fix was to add more PID filenames to cleanup.
That would have repeated the original mistake in another form.
MVM already had a shared, backend-aware liveness probe that knew which marker each backend owned, how to distinguish a live process from stale state, how to treat zombies, and which instance directory belonged to the process. Cleanup needed to use that same abstraction.
Not a similar copy. The same code.
Destructive tools do not get a simplified model of the system
Operational edge commands often begin as small utilities. They reimplement enough of the runtime model to be convenient, then quietly become wrong as the system grows.
A launch path knows every backend. A cleanup path remembers the first one. A database layer knows its transaction boundaries. A compaction tool deletes files based on naming conventions. A certificate manager knows the current trust graph. An uninstaller carries a list from two releases ago.
Those shortcuts are cheap until the day they destroy state owned by a subsystem they no longer understand.
The rule I took from this work is:
A destructive tool must use the same ownership and liveness abstractions as the runtime it can destroy.
This applies to VM cleanup, cache eviction, garbage collection, certificate rotation, orphan reaping, database compaction, and uninstallers. The edge command is where old assumptions become irreversible.
Preserving identity does not preserve the environment
The flag name can sound gentler than the operation really is.
With --keep-identity, MVM still removes templates, machines, images, checkpoints, snapshots, pools, shares, binaries, and other generated state. The operator keeps continuity, not convenience.
The intended result is a host that can rebuild while still being recognizably the same host:
- old audit records remain verifiable
- stored secrets remain decryptable
- the signing identity survives
- policy configuration survives
- new artifacts can be fetched under the same trust root
Everything else can be reconstructed.
That is why the full confirmation remains mandatory even when identity is preserved. The environment is still being destroyed.
A deletion command needs a state model
The original implementation looked like ordinary filesystem code: iterate over a list and call remove_dir_all.
The corrected version had to answer a more interesting set of questions:
- Is the operation selective or complete?
- Which state is rebuildable?
- Which state carries identity?
- Which identity-bearing entries are only useful as a set?
- Which live owners may still be using the state?
- What evidence becomes unverifiable after deletion?
- What must the operator understand before the command proceeds?
Before you type DELETE-EVERYTHING
Make the operator answer these questions in the command’s own vocabulary:
- what will be removed because it is rebuildable?
- what will be preserved because it carries identity?
- what live process could still own the state?
- what history will become unverifiable?
- is the operator asking for a clean machine or a new machine?
If the command cannot answer those questions, it is not ready to delete the tree.
Once those answers existed, the deletion loop was the least important part.
The flag’s name had created a contract. “Nuclear” now means every entry currently under the root, including future ones. The one exception is explicit and narrow: preserve the connected cryptographic identity set when the operator asks.
The cleanup bug changed how I think about a machine’s local state.
Deleting files is not only a storage operation. At some point, the set of files being removed contains the evidence, secrets, and trust roots that let the machine prove it is the same participant it was yesterday.
Sometimes the correct outcome is to erase that continuity. Sometimes the useful outcome is to delete almost everything while keeping the ability to prove who you are. The command has to make that choice visible before it starts removing files.
Beneath the Agent: Start with I Took the NIC Away