Backups & Version History with Chronicle
Chronicle is the InterGenOS backup utility. It gives you a browse-by-time history of your machine on a storage stack that has no copy-on-write filesystem underneath it — no btrfs subvolumes, no ZFS snapshots. Instead of asking the filesystem for snapshots, Chronicle builds its own history out of a content-addressed store and hardlink rotation, so it works on the plain root filesystem that InterGenOS installs by default.
Chronicle ships on the system image as the intergenos-backup package. You do not install it; it is already there.
This page describes Chronicle as it exists in the tree today. It documents three things a backup tool has to get right — what is protected, how you get data back, and where the data lives — plus how Chronicle interacts with full-disk encryption.
What Chronicle protects: three layers
Chronicle does not treat your machine as one undifferentiated blob. It keeps three separate timelines, called layers, because they change at different rates, matter for different reasons, and belong in different places.
| Layer | What it holds | Where it lives |
|---|---|---|
config-state | System configuration state — the /etc surface that makes this machine this machine | Always-on local store |
restore-point | A marker taken automatically before a package transaction changes the system | Always-on local store |
user-data | Your files, versioned hourly | External backup target only |
The split is deliberate. The first two layers are compact — a content-addressed store dedupes configuration and package state tightly — so they can live on the system disk and be always on. That means a freshly installed machine has some protection before you have attached any external drive at all: if a package upgrade breaks something, the restore point taken before that transaction is already on disk.
The third layer is different. User data is bulk data, and putting a bulk history on the same disk as the original protects you from nothing — a failed system disk takes both copies. So the user-data layer is target-only: it exists on the external backup volume, and until you attach one, your documents are not being versioned. Chronicle is explicit about this rather than quietly implying otherwise, which is why chronicle status reports target health as its first concern.
Attaching a backup target
Chronicle does not guess which drive is yours. You adopt a target explicitly.
chronicle target scan
This enumerates candidate volumes. Then adopt one:
sudo chronicle target adopt /mnt/backup
There are two target classes, and the distinction matters:
whole-volume(the default) — Chronicle takes the whole volume as the backup target.directory— Chronicle takes a size-capped subtree on a volume you are already using for other things. In this class Chronicle owns a single directory namedChronicleBackupsand never touches anything else on that volume.
# Use a capped directory on an existing drive instead of a whole volume
sudo chronicle target adopt /mnt/data --class directory --cap 274877906944
The --cap value is a size cap in bytes; the example above is roughly 256 GiB. A cap is what makes the directory class safe to point at a drive that is not exclusively yours to fill.
Encrypt the target
A backup drive is a complete copy of your data that leaves the machine’s physical protection the moment someone walks off with it. Chronicle’s default policy therefore recommends a LUKS2 container on the target, so a lost or stolen backup drive is not a data breach. This is expressed as target_encryption in the configuration file and defaults to enabled; setting it false is a decision to make only for a volume that is already trusted by other means.
This is the same reasoning as Disk Encryption & LUKS applied to the second copy. Encrypting your system disk and then writing a plaintext backup of it to a USB drive undoes the protection you paid for.
Taking a version now
Captures happen on a schedule (below), but you can always take one immediately:
chronicle capture config-state --reason "before editing sshd config"
chronicle capture user-data
The --reason string is stored with the version, which is what makes a timeline readable months later. For a large capture you would rather not pay for right now, queue it for the off-peak window instead:
chronicle capture user-data --async
chronicle queue # show what is waiting
Browsing and comparing history
chronicle status # target health and capture health
chronicle list user-data # browse that layer's timeline
chronicle list config-state --since 1750000000 --until 1760000000
list takes optional --since and --until bounds as epoch seconds.
To see what actually changed in a file between a past version and the file on disk now:
chronicle diff config-state <version> /etc/ssh/sshd_config
Restoring
Restore is the operation that can destroy data, so Chronicle makes it loud by design — the command’s own help describes it as never silent.
# Show the plan without touching anything
chronicle restore user-data <version> /home/you/Documents/report.odt --dry-run
# Restore beside the current file instead of over it
chronicle restore user-data <version> /home/you/Documents/report.odt --mode beside
# Replace the current file, after confirming
sudo chronicle restore user-data <version> /home/you/Documents/report.odt
Two restore modes exist and the difference is the whole safety story:
replace-confirm(the default) — the restored content replaces what is on disk now, after a confirmation prompt.beside— the restored content is written alongside the current file, so nothing you have now is lost and you compare the two yourself.
--dry-run prints the plan and stops. -y / --yes skips the prompt, which is appropriate in a script and inappropriate the first time you run a restore by hand.
You can restore more than one path in a single command; paths accepts a list.
Restore runs through a separate, higher-capability service leg (chronicle-restore@.service) rather than inside the always-running engine. Writing to arbitrary paths as root is a strictly larger privilege than taking a backup, so it lives in its own unit instead of being a capability the long-lived engine carries at all times.
Verifying that the history is real
A backup you have never verified is a belief, not a backup. Chronicle verifies a single version, or scrubs the entire store:
chronicle verify user-data <version> # one version
chronicle verify --scrub # the whole store
A weekly scrub also runs on its own timer, so integrity checking is not something you have to remember.
Protecting a version from pruning
Old versions are pruned as space is reclaimed. To keep one — the version from just before a migration, say — pin it:
chronicle pin <version>
chronicle unpin <version>
The schedule
Chronicle installs an engine plus three timers:
| Unit | Schedule | Purpose |
|---|---|---|
chronicled.service | always running | The backup engine and sentinel |
chronicle-userdata.timer | hourly | User-data capture |
chronicle-offpeak.timer | every 2 hours | Drain the queued off-peak captures |
chronicle-scrub.timer | weekly | Integrity scrub |
Large captures are held back until outside your working hours. The default working span is 09:00–18:00, and off-peak is everything else; a capture whose estimated change size is above a threshold waits for the off-peak window rather than competing with you for disk and CPU while you are using the machine. Smaller changes capture immediately.
You can inspect the engine and timers with ordinary systemd tooling:
systemctl status chronicled.service
systemctl list-timers 'chronicle-*'
journalctl -u chronicled.service
Package transactions create restore points automatically
Chronicle installs a pkm pre-transaction handler at /usr/lib/pkm/pre-transaction.d/chronicle-restore-point. Before pkm changes installed packages, a restore-point version is captured. You do not have to remember to do this, and it means the answer to “the upgrade broke it, what did the system look like an hour ago” is already recorded.
This complements, rather than replaces, pkm’s own rollback — see The Transparent Package Manager.
Where everything lives
Chronicle derives every path from one module rather than hard-coding strings, and there are exactly two store roots:
/var/lib/chronicle/ the always-on LOCAL store on the system disk
cas/ content-addressed blobs, sha256-sharded
versions/<layer>/ committed version manifests, one JSON per version
queue/ durable capture-intent spool (local store only)
state.json engine state: sequence number, adopted target, pins
<target>/ the TARGET store on the backup volume
cas/ versions/ state.json (same shape; no queue)
On a directory-class target, that store lives under a ChronicleBackups directory and nothing outside it is touched.
Other paths worth knowing:
| Path | What it is |
|---|---|
/etc/chronicle/chronicle.conf | User-tunable policy (see below) |
/run/chronicle/engine.sock | The engine’s runtime IPC socket |
/usr/bin/chronicle | The command-line client |
/usr/bin/chronicle-gui | The graphical client |
/usr/libexec/chronicle/ | Engine and helper implementation |
The target store holds the bulk user-data trees plus a mirror of the content-addressed layers, which is the property that makes a lost system disk fully recoverable rather than partially recoverable.
Configuration
Policy lives in /etc/chronicle/chronicle.conf under a [chronicle] section. Every key is optional: an absent or partial file yields a usable default policy, and a malformed value for one key falls back to that key’s default rather than stopping the engine. The file ships fully commented with the built-in defaults shown, so reading it is the fastest way to see current policy.
The tunables cover the working-hours window, the size threshold that sends a large capture to the off-peak window, the target preference and class, target encryption, and which paths count as user data. User data defaults to /home, and caches, trash and thumbnails are excluded by default — they are reproducible churn, not history worth keeping.
Runtime state that the engine owns — the monotonic sequence, the adopted target details, the pin set — deliberately does not live in the configuration file. It lives in /var/lib/chronicle/state.json, so editing configuration can never corrupt the engine’s own bookkeeping.
There is a manual page: man chronicle, and man chronicle.conf for the configuration format.
The graphical client
Chronicle ships a GTK4/libadwaita application, org.intergenos.Chronicle, which appears in your applications list. It is the same engine underneath; the browse-by-time history, restore and verification operations are the ones described above. Privileged operations go through a polkit policy (org.intergenos.Chronicle.policy), so the graphical client asks for authorization at the point of action rather than running privileged throughout.
How Chronicle relates to full-disk encryption
These are different protections and neither substitutes for the other.
Full-disk encryption protects data at rest against someone who has the drive. If your laptop is stolen while powered off, LUKS is what stops the thief reading your files. It does nothing about a file you deleted by mistake, because the encrypted volume faithfully encrypts the deletion too.
Chronicle protects against loss and change over time. It is what has your file from yesterday. It does nothing about a stolen drive, because a backup of an encrypted volume is only as protected as the volume it was written to.
They compose as follows on an InterGenOS machine:
- The system disk is LUKS-encrypted at install time. The
config-stateandrestore-pointlayers live inside that encrypted volume, so the always-on local history inherits the system disk’s encryption automatically. - The backup target is a separate volume with its own encryption decision. This is why Chronicle recommends a LUKS2 container on the target by default: without it, the copy of your data on the backup drive sits outside every protection the system disk gave you.
- Neither layer protects the LUKS header itself. Backing up your LUKS header is a distinct task with distinct handling — see Encryption, Keys & TPM2 and Recovery & Reinstall.
The short version: encrypt the system disk and the backup target, and treat the LUKS header backup as its own separate obligation.
A sensible first setup
chronicle status # confirm the engine is running
chronicle target scan # find the drive
sudo chronicle target adopt /mnt/backup # adopt it (encrypt it first)
chronicle capture user-data # take the first full capture
chronicle list user-data # confirm the version exists
chronicle verify user-data <version> # confirm it is readable
The last two steps are the ones people skip. A backup you have listed and verified is a backup; everything before that is a configuration.