1 unstable release
Uses new Rust 2024
| new 0.2.5 | May 15, 2026 |
|---|
#901 in Filesystem
Used in revenant-cli
310KB
6.5K
SLoC
Warning
Beta software. Revenant has been in daily use on a handful of systems for several weeks without an incident, and is no longer "early development". That said, it is a tool that renames live subvolumes and rewrites ESP contents — bugs at this stage can still leave a system unbootable or destroy data. Treat it as beta software on a production system and always keep an independent backup. If you would rather not take that risk, run revenant in a VM until tagged releases stabilise the on-disk layout and the public APIs.
revenant
A system snapshot tool for Linux, written in Rust. Inspired by Timeshift, but focused on CoW filesystems with systemd-boot integration and the EFI partition snapshotted alongside the rootfs.
Motivation
Timeshift works, but it comes with opinions: it assumes GRUB, it ignores your EFI partition, it expects Ubuntu-style subvolume layouts. If your system uses systemd-boot, a non-standard Btrfs layout, and you actually want your bootloader files to be part of the snapshot, Timeshift leaves you on your own.
Revenant was built to fill that gap. It works with any bootloader, but the EFI backup is
most valuable with systemd-boot, where kernel images and boot entries live on the ESP.
GRUB-based setups typically keep kernels in /boot on the root filesystem (already covered
by the btrfs snapshot), so the EFI sync adds little there — but revenant's core snapshot and
restore functionality works just the same.
An EFI partition is not required. On BIOS systems, or any setup where /boot is a
directory inside the rootfs, set sys.efi.enabled = false in the config and revenant
operates as a pure rootfs snapshot tool. revenantctl init detects this automatically.
How it compares
Only the rows where the three tools actually differ — all of them can take snapshots,
apply retention, and restore, so those are omitted. Third-party add-ons like grub-btrfs,
snap-pac, btrfs-assistant or snapper-gui are deliberately not counted toward
Timeshift or snapper, because they are not part of the respective upstream projects.
| Feature | revenant | Timeshift | snapper |
|---|---|---|---|
| Filesystem backends | btrfs (ZFS/bcachefs planned) | btrfs, rsync/ext4 | btrfs, ext4, LVM-thin |
| EFI partition snapshotted in sync with rootfs | ✓ | ✗ | ✗ |
| Bootloader integration | any (EFI sync most useful with systemd-boot) | GRUB2 (reinstalled on restore) | — (rollback just flips btrfs default subvol) |
| Independent snapshot profiles with own retention | ✓ (strains) | ✗ (fixed Hourly/Daily/Weekly/Monthly/Boot) | ✓ (per-subvolume configs) |
| Package-manager pre/post hooks (upstream) | ✓ pacman (init --pacman); apt/zypp planned |
— | zypp (openSUSE) |
| JSON / scriptable CLI | ✓ --json |
✗ | ✓ --jsonout |
| Upstream GUI | ✓ (GTK4 / libadwaita, talks to a privileged D-Bus daemon) | GTK | — |
Sources: Timeshift README,
snapper(8),
snapper-configs(5).
What it does
Revenant creates atomic, point-in-time snapshots of a Btrfs system and restores them cleanly. The key difference from other tools: the EFI partition is backed up too, so a snapshot and the corresponding ESP state are always kept together. This is especially valuable with systemd-boot, where kernels and boot entries live on the ESP. A restore is rejected if the EFI snapshot for a given ID is missing. On systems without an EFI partition, EFI sync can be disabled entirely and revenant runs as a rootfs-only snapshot tool.
Snapshot naming
Snapshots follow the pattern {subvol}-{strain}-{timestamp}, e.g.
@-default-20260316-143022-456. The timestamp is YYYYMMDD-HHMMSS-mmm UTC
with a millisecond suffix; older 15-character IDs without the suffix
remain readable. The subvolume name carries the snapshot's identity —
strain, exact creation moment, and source subvolume — so there is no
central snapshot database. Optional per-snapshot metadata is stored
next to the subvolume as a TOML sidecar (see below) and is strictly
additive.
Snapshot metadata
Alongside each snapshot, revenant writes an optional TOML sidecar
{strain}-{timestamp}.meta.toml in the snapshot directory (e.g.
@snapshots/, next to the snapshot subvolumes themselves) that
records why the snapshot exists:
- A free-form
--messagesupplied by the user on manual snapshots. - The trigger kind (
manual,pacman,systemd-boot,systemd-periodic,restore). - For pacman-triggered snapshots, the list of target packages read from the hook's stdin.
- For systemd-triggered snapshots, the unit that fired them.
- For
restore-triggered snapshots (created byrestore --save-currentbefore the rollback), the id of the snapshot the restore was heading to. - A local-time
created_attimestamp (so the sidecar is readable on its own, independent of the UTC id in the subvolume name).
Sidecars are strictly additive: snapshots without a sidecar (legacy or externally created) list fine and restore normally. A failure to write the sidecar never fails the snapshot itself — metadata loss is preferable to a stranded half-created snapshot.
list and snapshot surface the metadata in both text and JSON output;
check flags sidecars whose snapshot subvolume has disappeared as
orphaned-sidecar, and cleanup removes them.
Strains
A strain is a named snapshot namespace with its own configuration: which subvolumes to
snapshot, whether to include the EFI partition, and how many snapshots to retain. You can
define multiple strains for different purposes (e.g. default for manual snapshots,
pacman triggered before package upgrades).
Retention
Each strain defines its own retention policy (last, hourly, daily, …).
By default retention is applied automatically every time a new snapshot
is created (sys.auto_apply_retention = true). Set it to false if you
want snapshot creation to never delete anything; retention then runs
only when you invoke revenantctl cleanup explicitly.
revenantctl cleanup does three things in one pass: apply per-strain
retention, expire DELETE markers older than sys.tombstone_max_age_days
(default 14 days; see Restore and the DELETE marker below), and sweep
up orphaned metadata sidecars whose snapshot subvolume is gone. Pass
--dry-run to preview the plan, or --force to skip the DELETE-marker
undo window and purge every marker right away (per-strain retention is
still honoured — --force only bypasses the tombstone cooldown).
Restore and the DELETE marker
revenantctl restore <id> is destructive and refuses to run without an explicit
--yes. Without the flag it prints what it would do and exits with code 1; this
makes the user's acknowledgement a single explicit step without requiring an
interactive prompt (so it is still script-friendly via --yes).
A restore renames the current live subvolume to {subvol}-DELETE-{ts} and
creates a fresh writable copy from the chosen snapshot in its place. The
renamed subvolume is not purged immediately: it lives at the top level of
the btrfs filesystem as a volatile undo buffer for the previous live state. If
you need a file from the pre-restore system after rebooting, it is still
there, writable, until it ages out.
DELETE markers expire automatically sys.tombstone_max_age_days days after
they were created (default 14 days); the next retention pass — whether
triggered by revenantctl snapshot with auto_apply_retention = true or by
an explicit revenantctl cleanup — drops every DELETE marker older than
that. Recent markers survive the routine cleanup, so the undo buffer is
actually available when you need it. To skip the cooldown and purge every
marker now, run revenantctl cleanup --force. Setting
sys.tombstone_max_age_days = 0 disables auto-expiry entirely; markers then
stay until you remove them explicitly (cleanup --force, or the GUI's
review dialog).
If you want a retained, strain-integrated copy of the current state instead
of (or in addition to) the volatile DELETE marker, pass --save-current to
restore: it captures the pre-restore state as a snapshot in the target
strain (tagged with the restore trigger kind) just before the rollback, so
you have a named, retention-managed point to return to.
After a restore, revenantctl list marks the snapshot that the current live
subvolume was cloned from with a leading *, so the rollback anchor is always
visible in the snapshot listing. In JSON mode the same information is exposed
as an optional top-level live_parent field alongside snapshots.
EFI backup strategy
The EFI partition is not a Btrfs subvolume, so revenant maintains a staging subvolume (e.g.
@boot) on the Btrfs filesystem. Before each snapshot, the EFI partition contents are
rsync-like-copied into the staging subvolume (block-level, inplace, only changed blocks), then
a read-only snapshot of that staging subvolume is taken with the same ID as the root snapshot.
On restore, both snapshots must be present for the ID.
Current state
- Btrfs backend via direct ioctls (no external
btrfsbinary required) - Bootloader-agnostic (EFI sync optimised for systemd-boot)
- EFI partition backup and restore
- Snapshot creation, listing, deletion
- Retention policy / cleanup
-
revenantctl init— auto-detects system configuration from/proc/self/mountinfoand generates a ready-to-useconfig.toml - Multiple strains with independent retention settings
- Restore flow with
--yesconfirmation and DELETE-marker undo buffer - Snapshots stored in dedicated
@snapshotssubvolume (configurable) - Per-snapshot metadata sidecars (
--message, trigger context, package targets) - GUI (
revenant-gui, GTK4 + libadwaita) backed byrevenant-daemon(system-bus service, polkit-gated privileged operations) -
revenantctl init --systemd— generates systemd units for boot and periodic snapshots -
revenantctl check— health checks for config, orphaned snapshots and nested subvolumes - ZFS / bcachefs backends (trait is defined, implementations pending)
Architecture
Revenant is a Cargo workspace with four crates:
| Crate | Role |
|---|---|
revenant-core |
Library: all snapshot logic, backend trait, config, EFI sync |
revenant-cli |
Binary revenantctl: the command-line interface |
revenant-daemon |
Binary revenantd: system-bus D-Bus service, polkit-gated privileged operations |
revenant-gui |
Binary revenant-gui: GTK4 + libadwaita desktop client, talks to the daemon over D-Bus |
The CLI is standalone and does not depend on the daemon — revenantctl runs as root and
talks to the backend directly. The GUI is the unprivileged client of revenant-daemon,
which owns the btrfs toplevel mount and gates every write through polkit. The wire contract
is documented in crates/revenant-daemon/dbus-interface.md.
The FileSystemBackend trait abstracts all COW filesystem operations, making it straightforward
to add ZFS or bcachefs backends later without touching the core logic.
No external binaries are required at runtime — all Btrfs operations go through ioctls directly. This means revenant works even in a minimal recovery environment.
Installation
From source
cargo build --release --workspace
sudo install -Dm755 target/release/revenantctl /usr/local/bin/revenantctl
# Optional, only if you want the GUI + privileged daemon:
sudo install -Dm755 target/release/revenantd /usr/local/bin/revenantd
sudo install -Dm755 target/release/revenant-gui /usr/local/bin/revenant-gui
The daemon and the GUI need their D-Bus and polkit policy files in place to actually
work; the recipe is in crates/revenant-daemon/README.md.
Arch Linux packages
Each tagged release attaches two *.pkg.tar.zst files for x86_64 to its GitHub
release page (no aarch64 packages — for ARM systems use the static revenantctl
musl binary from the same release page, or build the workspace from source).
Download the matching version and install with pacman -U:
sudo pacman -U revenant-<version>-1-x86_64.pkg.tar.zst
# Optional, depends on the revenant package:
sudo pacman -U revenant-gui-<version>-1-x86_64.pkg.tar.zst
revenant ships the CLI (revenantctl); revenant-gui ships the privileged daemon
(revenantd) plus the GTK4 client (revenant-gui) along with the D-Bus, polkit and
systemd policy files.
To build the same packages locally from a working tree:
cd packaging/arch
makepkg -fi # build, then install with pacman -U behind the scenes
The PKGBUILD reads pkgver from the workspace Cargo.toml, so building from any
checkout produces a package matching that revision; no manual version edit needed.
Usage
revenantctl [OPTIONS] <COMMAND>
Options:
--config <PATH> Configuration file [default: /etc/revenant/config.toml]
-v, -vv, -vvv Increase verbosity
-j, --json Emit machine-readable JSON on stdout (see below)
Commands:
init Auto-detect system and generate config file
snapshot Create a new snapshot
list List all snapshots (optionally filter by strain)
restore Restore a snapshot by ID
delete Delete a snapshot or all snapshots of a strain
cleanup Apply retention policy and remove old snapshots
status Show configuration and filesystem status
check Run system health checks
JSON output
All commands accept a global -j / --json flag that switches stdout to a
single JSON document per invocation. Tracing/log output continues to go to
stderr so that consumers can rely on clean stdout for jq, python -m json.tool,
or any other parser.
The rough shape per command is:
| Command | JSON payload |
|---|---|
list |
{"snapshots": [{"id","strain","subvolumes","efi_synced","metadata"?}, …], "live_parent"?: {"id","strain"}} |
status |
{"config": {…}, "strain_snapshots": {name: count}, "snapshots_total": N} |
snapshot |
{"created": SnapshotInfo, "retention_removed": [id, …]} |
delete |
{"strain": "…", "deleted": [id, …]} |
restore (--yes) |
{"restored": {"id","strain"}, "pre_restore_snapshot"?: {"id","strain"}, "reboot_required": true} |
restore (refusal) |
{"would_restore": {…}, "subvolumes": […], "efi_sync": bool, "proceed_with": "--yes"}, exit 1 |
cleanup |
{"removed": [id, …], "removed_sidecars": [name, …]} |
cleanup --dry-run |
the full RetentionPlan — per-strain keep/delete entries plus every DELETE marker, each tagged with would_purge and expires_at |
check |
{"findings": [{"severity","check","message","hint"?}, …], "summary": {"errors","warnings","infos"}} |
init |
{"tasks": [{"task": "detected-system" | "wrote-config" | "added-systemd-strains" | "wrote-systemd-unit" | "added-pkgmgr-strain" | "wrote-pkgmgr-hook", …}, …]} |
Errors in JSON mode land as {"error": "..."} on stdout with a non-zero exit
code, so a script can consistently read from stdout and branch on exit status.
For check and restore, the exit code also encodes the outcome (non-zero on
any error finding / on the refusal path), matching the text-mode semantics.
Note
JSON schema is not yet stable. Until revenant reaches a 1.0 release, field names, task enum variants, and overall shapes may still change. Scripts that consume JSON output should pin to a specific revenant version.
Quick start
# Detect your system and write /etc/revenant/config.toml
sudo revenantctl init
# Take a snapshot using the default strain
sudo revenantctl snapshot
# Take a snapshot with a descriptive message (recorded in the sidecar)
sudo revenantctl snapshot --message "before risky experiment"
# List all snapshots
revenantctl list
# Restore a specific snapshot (prints what would happen and exits with code 1)
sudo revenantctl restore 20260316-143022-456
# Re-run with --yes to actually perform the restore
sudo revenantctl restore 20260316-143022-456 --yes
Systemd integration
Generate systemd units for automatic snapshots:
# Generate config + systemd units
sudo revenantctl init --systemd
# Enable boot snapshot (runs once after each boot)
sudo systemctl enable revenant-boot.service
# Enable periodic snapshots (hourly by default)
sudo systemctl enable --now revenant-periodic.timer
These units drive revenantctl directly and are independent of
revenant-daemon. The daemon is only needed for the GUI and is started
on demand via D-Bus activation; there is no need to systemctl enable
it for periodic or boot snapshots to work.
The timer interval and periodic strain name are configurable:
sudo revenantctl init --systemd --timer-interval "*-*-* 00/4:00:00" --periodic-strain hourly
Pacman integration
On Arch-family systems, revenant can install a pacman PreTransaction hook
that snapshots the system before every package install, upgrade or removal:
sudo revenantctl init --pacman
This adds a pacman strain to the config (retain 10 by default) and writes
/etc/pacman.d/hooks/50-revenant-snapshot.hook. The hook is deliberately
non-blocking: if snapshotting fails for any reason, the transaction still
proceeds (revenant logs the error to stderr, where it surfaces in pacman's
output). A broken snapshot tool must never turn into a broken package
upgrade.
Because pacman acquires its database lock /var/lib/pacman/db.lck before
running PreTransaction hooks, every hook-triggered snapshot captures that
lock file. Revenant strips it from the restored tree during restore so
the first pacman invocation after a rollback does not abort with
unable to lock database. The cleanup is unconditional and covers every
package manager revenant knows about, so it is a no-op on systems that
never installed a pacman hook.
--pacman composes with --systemd, so a fresh box can be set up in a
single invocation:
sudo revenantctl init --systemd --pacman
Support for apt (Debian / Ubuntu) and zypp (openSUSE) is planned; the
package-manager backends sit behind a trait so adding them does not
require reshaping existing code.
Health checks
revenantctl check runs a set of non-destructive checks against the current
system state and reports findings as warnings or errors. It exits non-zero if
any errors are found, so it can be used in scripts or monitoring.
Current checks:
- config-missing / config-invalid — verifies that the configuration file
exists and parses. Reports a hint pointing to
revenantctl initif not. - orphaned-snapshot — scans the snapshot subvolume for entries matching
the revenant naming scheme (
{subvol}-{strain}-{timestamp}) that no configured strain claims. Useful for detecting leftovers from a strain that was removed from the config, or accidental snapshots created with a different config. - orphaned-sidecar — sidecar metadata files (
*.meta.toml) whose matching snapshot subvolume is gone. Typically left behind when a snapshot subvolume was removed by hand.revenantctl cleanupremoves these alongside DELETE markers. - nested-subvolumes — informational notice about nested subvolumes inside any snapshotted subvolume. Revenant re-attaches them across a restore at their current state, but their contents are not versioned along with the parent (see Nested subvolumes below for details).
sudo revenantctl check
Nested subvolumes
Btrfs snapshots do not include subvolumes nested inside the snapshotted subvolume — they
appear as empty directories in the snapshot. This is a fundamental property of Btrfs, not
a revenant bug. It matters in practice because systemd automatically creates
/var/lib/machines and /var/lib/portables as nested subvolumes inside the root, and
users often create their own (e.g. /var/lib/docker, database stores, …).
How revenant handles them
Snapshot and rollback treat nested subvolumes as runtime state, not versioned content:
- Snapshots capture only the parent subvolume's tree. The nested subvolumes are not copied. This is the unavoidable btrfs behaviour.
- Restore rolls back the parent subvolume and then re-attaches every nested subvolume
it found beforehand to the same path inside the restored tree, in their current state.
If the snapshot pre-dates the nested subvolume's parent path (e.g. you rolled back to a
point before
/var/libexisted), the missing path is materialised on the fly so the nested subvolume has somewhere to land. - DELETE markers (
@-DELETE-...) are emptied of nested subvolumes during restore, so retention/cleanup can remove them normally. - Crash recovery. If a restore is interrupted between renaming
@and re-attaching the nested subvolumes, the next state-changing command (snapshot,restore,delete,cleanup) will detect the orphaned nested subvolumes inside the DELETE marker and move them back into the live@automatically.
What this means for you
Nested subvolume contents are not versioned — a rollback does not revert them. The nested subvolume itself survives the restore (revenant re-attaches it), but anything written inside it stays at its current state. There is no need to flatten your layout to accommodate revenant; the only thing to keep in mind is that snapshots do not recurse into nested subvolumes, so state you want rolled back must live inside the parent, not in a nested one.
revenantctl check reports nested subvolumes inside configured snapshot sources as an
informational notice so you can make a conscious decision about where state lives.
Configuration
[sys]
rootfs_subvol = "@"
snapshot_subvol = "@snapshots"
# Apply per-strain retention automatically after `revenantctl snapshot`.
# When false, snapshot creation never deletes anything; retention runs
# only when `cleanup` is invoked explicitly.
auto_apply_retention = true
# DELETE markers (`<base>-DELETE-<ts>` subvols left behind by a previous
# restore) auto-expire after this many days as part of any retention
# run. 0 disables auto-expiry; `cleanup --force` always purges them.
tombstone_max_age_days = 14
[sys.rootfs]
backend = "btrfs"
device_uuid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
[sys.efi]
enabled = true
mount_point = "/boot"
staging_subvol = "@boot"
[sys.bootloader]
backend = "systemd-boot"
[strain.default]
subvolumes = ["@"]
efi = true
[strain.default.retain]
last = 5
daily = 7
The annotated, field-by-field reference is in
config/revenant.toml.example.
Requirements
- Linux with a Btrfs root filesystem
- Optional: EFI system partition (for EFI sync; most valuable with systemd-boot)
- Rust 1.85+ (to build)
Testing in a VM
Revenant performs btrfs subvolume operations and syncs an ESP — both are easier to
exercise without risk inside a throwaway VM. For an end-to-end test you need a guest
that mirrors the supported target shape: UEFI firmware, systemd-boot, a Btrfs root
filesystem with a snapshottable subvolume layout (e.g. @ / @home / @snapshots).
Any installer that can produce that layout works; building such a VM by hand is
faster than auditing a third-party script that promises to do it for you. Once the
VM boots, sudo revenantctl init detects the layout and writes a starter config,
and the rest of the workflow is identical to the real-system commands above.
Disclaimer
Revenant is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the author be liable for any claim, damages, or other liability — including but not limited to data loss, filesystem corruption, unbootable systems, or any other direct, indirect, incidental, special, exemplary, or consequential damages — arising from, out of, or in connection with the software or the use or other dealings in the software.
This software has been written to the best of the author's knowledge and ability, but it may contain bugs. Revenant is not a backup solution. It takes point-in-time snapshots of a live system and is designed to complement, not replace, a proper backup strategy. Always use revenant alongside independent backup software that stores your data on a separate device or medium. By using this software, you accept full responsibility for any consequences of its operation on your system.
See the LICENSE file for the full legal terms.
License
GPL-3.0-only — see LICENSE.
Dependencies
~8–11MB
~131K SLoC