Cleanup Bootstrap Root performs one controlled filesystem transition:
Remove verified filesystem content from a retired Btrfs bootstrap root after the running system has permanently transitioned to a nested snapshot root.
It is a standalone post-transition utility. It does not perform the transition, create snapshots, select snapshots, modify boot configuration, or manage BootPrep.
The utility expects the following state:
Btrfs filesystem
├── <retired-root>
│ ├── retired bootstrap-root content
│ └── .snapshots
│ └── <number>/snapshot
│ └── current running root
└── other discovered subvolumes
The current root must match:
<retired-root>/.snapshots/<number>/snapshot
where <number> contains only decimal digits. The derived retired-root path must contain only safe components and cannot itself be nested beneath another .snapshots directory. The running snapshot must also be the Btrfs default subvolume.
The cleanup target is the derived <retired-root>.
Cleanup Bootstrap Root owns:
- Environment validation.
- Retired-root derivation and temporary mounting by numeric subvolume ID.
- Filesystem and subvolume discovery.
- Comparison of the bootstrap root with the running snapshot.
- Protection and retention policy.
- Construction of typed deletion queues.
- Optional execution of the verified cleanup plan.
- Post-operation policy verification.
- Temporary mount cleanup.
It does not own:
- Snapshot creation or deletion.
- Migration into a snapshot.
- Selection of the default subvolume.
- Snapper configuration.
- GRUB or EFI configuration.
- BootPrep installation or operation.
- Cleanup inside separate Btrfs subvolumes.
The script establishes these invariants before analysis:
- The process is running as root.
- The root filesystem is Btrfs.
- The current filesystem root matches
<retired-root>/.snapshots/<number>/snapshot. - The derived retired-root path passes component and nesting validation.
- The running root's numeric Btrfs ID equals the default subvolume ID.
- The filesystem source resolves to a block device.
- The derived retired root resolves to exactly one numeric subvolume ID.
- The corresponding
<retired-root>/.snapshotssubvolume exists exactly once. - Every temporary mount uses that numeric ID and resolves to the expected filesystem-root path.
Failure of any invariant terminates the program.
The operating mode must also be explicit:
--dryrun
--execute
No argument is treated as a usage error.
The script maintains discovered state in scalar variables, indexed arrays, and associative maps.
DEVICEROOT_FSTYPECURRENT_SOURCECURRENT_SUBVOLCURRENT_SUBVOL_IDDEFAULT_SUBVOL_IDBOOTSTRAP_ROOTBOOTSTRAP_SUBVOLBOOTSTRAP_SUBVOL_IDMOUNTED_BOOTSTRAP_SUBVOLSNAPSHOT_STORE_SUBVOL
| Map | Purpose |
|---|---|
SNAPSHOT_MAP |
O(1) lookup of objects in the running snapshot |
BOOTSTRAP_MAP |
O(1) lookup of objects in the retired root |
SUBVOLUME_PATH_MAP |
Actual subvolume paths nested beneath the derived root |
SUBVOLUME_PARENT_MAP |
Parent directory entries required to reach nested subvolumes |
RETAINED_MAP |
Bootstrap objects not proven safe for deletion |
KEEP_BOOTKEEP_SNAPSHOTSKEEP_SUBVOLUMERETAINED_OBJECTSDELETE_CANDIDATE
DELETE_LINKSDELETE_FILESDELETE_SOCKETSDELETE_DEVICESDELETE_DIRSDELETE_OTHER
The script reads the current root from findmnt -no FSROOT /, removes a single leading slash, and accepts only the exact nested-snapshot form.
@/.snapshots/8/snapshot -> @
@rootfs/.snapshots/17/snapshot -> @rootfs
@btrfs/.snapshots/204/snapshot -> @btrfs
systems/root/.snapshots/3/snapshot -> systems/root
Empty names, nonnumeric snapshot identifiers, suffixes after snapshot, unsafe components, repeated separators, and snapshot-of-snapshot paths are rejected.
The derived path is resolved to one numeric Btrfs subvolume ID. Mounting by ID avoids ambiguity from mount-option formatting, and checking the mounted FSROOT proves that the intended retired root was selected.
The temporary mount point is created with:
/run/bootstrap-root.XXXXXX
The mount lifecycle begins in both modes with a read-only mount:
Create temporary directory
|
v
Mount derived retired root read-only by numeric ID
|
v
Discover, compare, classify, and build execution queues
From that point, the lifecycle depends on the selected mode:
- In
--dryrun, the retired root remains mounted read-only while queue processing and directory removal are simulated. - In
--execute, it is unmounted, remounted read-write by the same numeric ID, and verified again before the queues are processed.
After queue processing, both modes mount the same numeric ID read-only for final policy verification and then rely on the exit trap to unmount it and remove the temporary directory.
The running snapshot is scanned with find -xdev.
The -xdev boundary prevents traversal into other mounted filesystems.
Every relative path is indexed in SNAPSHOT_MAP.
The retired root is mounted separately and scanned with find -xdev.
Every relative path is indexed in BOOTSTRAP_MAP.
The script discovers and reports every path returned by btrfs subvolume list /.
Only subvolumes beneath the exact derived root prefix can appear within the retired bootstrap-root namespace. For each of those subvolumes, the script protects:
- The subvolume path.
- Every parent directory entry required to reach it.
- Everything beneath the protected path during classification.
Sibling subvolumes outside the derived root are beyond the cleanup namespace and cannot be modified. This prevents cleanup from crossing any Btrfs subvolume boundary it can reach.
Every bootstrap-root object is compared with the running snapshot.
An object is treated as matched when:
- The same path exists in the running snapshot; or
- The object is absent from the snapshot but its immediate parent still exists there.
Objects absent from the snapshot whose immediate parent is also absent are placed in the retained map. They represent abandoned or otherwise unproven directory trees and are not deleted automatically.
This comparison is intentionally conservative: absence alone is not sufficient proof that an entire tree should be removed.
Classification applies ordered rules to every object in BOOTSTRAP_MAP.
boot and boot/* are always retained.
.snapshots and .snapshots/* are always retained.
Every discovered subvolume within the derived retired-root namespace, its required parent directory entries, and everything beneath its protected path are retained.
Objects in RETAINED_MAP are retained.
Only objects that pass all preceding protection and retention rules become delete candidates.
The order matters. A protected object can never fall through into the delete plan.
Delete candidates are divided by filesystem object type:
Symbolic links
Regular files
Unix-domain sockets
Verified /dev device nodes
Directories
Other
Directories are sorted by path depth, deepest first.
The complete queue count must equal the delete-candidate count. A mismatch terminates the program.
Unix-domain sockets are explicitly recognized because retired roots can contain stale runtime sockets, including GnuPG agent sockets. They are removed with the same non-recursive single-object deletion primitive used for files and links.
Character and block device nodes are accepted only beneath dev/. The stored node and active /dev counterpart must resolve to the same node type and major/minor numbers. A live symlink such as /dev/ptmx is permitted only when it resolves to the matching device within /dev. The comparison is repeated immediately before Phase 1, so a changed or missing counterpart stops the run before deletion begins.
FIFOs, mismatched device nodes, device nodes outside dev/, and unknown special types remain in DELETE_OTHER. A nonempty DELETE_OTHER queue rejects the execution plan.
The verified execution order is:
- Symbolic links
- Regular files
- Unix-domain sockets
- Verified device nodes
- Empty directories
- Final verification
--dryrun performs the complete discovery, comparison, classification, planning, queue-processing, and validation pipeline.
The delete helper returns without calling rm. Directory removal is counted but not performed.
--execute removes queued symbolic links, regular files, Unix-domain sockets, and verified device nodes with rm --.
Queued directories are processed with rmdir, not recursive removal. A directory remains when it is not empty.
Failures removing links, regular files, sockets, or verified device nodes stop execution. Directory removal is best-effort because retained children may legitimately keep a parent directory nonempty.
The architecture depends on verified Btrfs state, not distribution identity.
Version 1.1.0 completed cleanup execution and final validation on Debian, Kubuntu, and TUXEDO OS for Debian/Ubuntu-based coverage and on Manjaro, CachyOS, and EndeavourOS for Arch-based coverage.
Additional @rootfs compatibility testing proved dynamic retired-root derivation and covered persistent character devices in the stored /dev. Dry run, execution, and the subsequent reboot passed with the guarded device-node queue.
Version 1.0.1 previously added an explicit Unix-domain socket queue after CachyOS exposed stale GnuPG sockets in the retired bootstrap root.
After queue processing, the script remounts the derived retired root read-only by numeric ID and builds FINAL_MAP.
In execution mode it verifies:
- Non-directory delete candidates no longer exist.
- Every protected boot, snapshot, and subvolume object still exists.
- Every intentionally retained object still exists.
Directories are excluded from the delete-remaining failure count because they may legitimately remain as parents of protected or retained content.
In dry-run mode, the script reports the planned delete, protected, and retained totals and exits successfully without expecting candidates to be absent.
The script uses set -euo pipefail and explicit fail checks.
A failure exits immediately. The exit trap then attempts to:
- Unmount the temporary bootstrap-root mount.
- Remove the temporary mount directory.
The utility does not attempt automatic rollback of already completed deletions. Its safety strategy is prevention through explicit mode selection, environment refusal checks, conservative classification, typed queues, pre-execution device verification, and final verification.
| Module | Responsibility |
|---|---|
| 1 | Environment Verification |
| 2 | Mount Bootstrap Root |
| 3 | Discover Btrfs Subvolumes |
| 4 | Discover Active Snapshot Tree |
| 5 | Build Snapshot Lookup Table |
| 6 | Build Bootstrap Lookup Table |
| 7 | Compare Bootstrap to Active Snapshot |
| 8 | Policy Classification |
| 9 | Cleanup Verification Report |
| 10 | Build Execution Plan |
| 11 | Execute Cleanup Plan |
| 12 | Final Validation |
Each module consumes verified state from the preceding modules.
cleanup-bootstrap-root/
├── ARCHITECTURE.md
├── CHANGELOG.md
├── LICENSE
├── README.md
├── cleanup-bootstrap-root.sh
└── tests/
├── test_device_nodes.py
└── test_root_detection.py
The script is intentionally standalone. No installed runtime, service, plugin, or configuration file is required.
The source file declares GPL-3.0-or-later.