Tracking files and writing plugins
Rivet ships with a curated set of configuration groups. From v0.6 you can go further without touching Rust:
rivet trackcaptures any file or directory under your home directory.- Declarative plugins add a whole application as a reusable group.
Both flow through the same capture pipeline the built-in groups use, so they inherit every safety guarantee: the exclusion rules, the credential scanner, home containment, real file modes, in-root symlinks, and rollback copies on restore. There is no “just copy it” path.
Design rationale lives in ADR-0004.
Tracking a path
Section titled “Tracking a path”$ rivet track ~/dev/scripts✓ Tracking ~/dev/scripts. Run `rivet snapshot` to capture it.That writes a [[track]] entry into the repository’s rivet.toml. The file
stays the source of truth — track and untrack are conveniences for editing
it, and you can hand-edit it instead.
[[track]]path = "~/dev/scripts"include = ["**/*.sh", "**/*.py"]exclude = ["**/node_modules/**", "target"]secrets = "plaintext-blocked"mode = "preserve"symlinks = "preserve-in-root"| Field | Values | Meaning |
|---|---|---|
path |
~/... |
Must resolve inside $HOME. Absolute paths elsewhere and .. are refused. |
include |
globs | Optional allow-list, matched relative to path. Empty means everything. |
exclude |
globs | Deny-list, applied after include. Naming a directory drops its whole subtree. |
secrets |
plaintext-blocked (default), vault |
What happens to credential material — see below. |
mode |
preserve (default), normalize |
Keep real permissions, or record a canonical 0644/0755. |
symlinks |
preserve-in-root (default), skip |
Record in-$HOME links (never dereferenced), or ignore links entirely. |
allow_excluded_names |
false (default) |
See “Relaxing name exclusions”. |
Command-line equivalents:
rivet track ~/dev/scripts --include '**/*.sh' --exclude 'node_modules'rivet track --listrivet untrack ~/dev/scriptsuntrack only stops future capture; existing snapshots are unchanged.
Secrets in a tracked path
Section titled “Secrets in a tracked path”By default a tracked path is plaintext-blocked: if the scanner finds
credential material (an SSH private key, a GitHub token, an AWS key), Rivet
refuses the snapshot rather than publishing it. That is the v0.1 behaviour
and it still applies to everything you track.
If a path genuinely holds secrets you want captured, declare it for the vault:
rivet vault init # once, if you have not alreadyrivet track ~/dev/private --vaultEvery file the entry covers is then sealed with age into
secrets/<name>.age and never written to the repository in the clear. It
restores to its original path and mode on a machine holding the vault
identity. See SECRETS.md.
Relaxing name exclusions
Section titled “Relaxing name exclusions”Rivet skips any path with a component named cache, secrets, credentials,
.ssh, and similar. Occasionally that is wrong — a source directory
legitimately named cache, for instance. For one tracked entry only:
[[track]]path = "~/dev/app"allow_excluded_names = trueThis relaxes the name match for that entry, warns loudly in every snapshot, and does not disable the content scanner: real credentials are still refused.
Writing a declarative plugin
Section titled “Writing a declarative plugin”Drop a TOML file in ~/.config/rivet/plugins/ (honouring $XDG_CONFIG_HOME).
It becomes a configuration group indistinguishable from a built-in: it appears
in rivet scan and rivet checklist, is selectable, and is captured and
restored the same way.
id = "helix"title = "Helix editor"description = "Helix configuration and runtime queries."roots = [".config/helix"]packages = { pacman = ["helix"], apt = ["helix"], dnf = ["helix"] }
[[hooks.export]]argv = ["hx", "--health"]artifact = "helix-health.txt"
[[hooks.import]]argv = []artifact = ""| Field | Required | Meaning |
|---|---|---|
id |
yes | Stable slug (letters, digits, -, _). Must not collide with a built-in. |
title |
yes | Shown in the checklist. |
description |
no | One line of context. |
roots |
yes¹ | Home-relative paths this group owns. No absolute paths, no ... |
packages |
no | Per-manager package names, so a restore can install the app that owns the config. |
hooks.export |
no | Commands whose stdout is stored as hooks/<artifact>. |
hooks.import |
no | Commands fed hooks/<artifact> on stdin during restore --apply. |
¹ Either roots or at least one export hook — a plugin that captures nothing
is rejected.
List what is loaded, and from where:
$ rivet plugins # ids, titles, and source file$ rivet plugins --verbose # plus every root and hook commandThe trust model
Section titled “The trust model”A declarative plugin runs no code of its own. The only commands that ever
execute are the argv arrays its author wrote, through the same hook machinery
the built-in GNOME/Cinnamon dconf hooks use. argv is an explicit program
plus arguments — never a shell string, so there is nothing to interpolate or
escape — and nothing is ever fetched or inferred.
Installing someone else’s plugin file means trusting the commands it runs, exactly like sourcing a shell script. Rivet therefore:
- prints every hook command during
rivet scan, with the plugin and the file that authorised it, before anything could run; - names that file again in the
restoredry-run, before--apply --yes; - refuses a user plugin whose
idcollides with a built-in, rather than letting an unreviewed file silently shadow reviewed behaviour; - validates ids, roots, and hook artifacts, so a hostile file cannot use them
to escape
$HOMEor the snapshot’shooks/directory; - warns about unknown keys instead of ignoring them, so
root =written forroots =is visible; - skips a broken plugin file with a warning rather than failing your snapshot.
A signed plugin registry is deliberately post-1.0. Until then, read the file.