Skip to content

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 track captures 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.


Terminal window
$ 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:

Terminal window
rivet track ~/dev/scripts --include '**/*.sh' --exclude 'node_modules'
rivet track --list
rivet untrack ~/dev/scripts

untrack only stops future capture; existing snapshots are unchanged.

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:

Terminal window
rivet vault init # once, if you have not already
rivet track ~/dev/private --vault

Every 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.

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 = true

This relaxes the name match for that entry, warns loudly in every snapshot, and does not disable the content scanner: real credentials are still refused.


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.

~/.config/rivet/plugins/helix.toml
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:

Terminal window
$ rivet plugins # ids, titles, and source file
$ rivet plugins --verbose # plus every root and hook command

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 restore dry-run, before --apply --yes;
  • refuses a user plugin whose id collides 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 $HOME or the snapshot’s hooks/ directory;
  • warns about unknown keys instead of ignoring them, so root = written for roots = 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.