name: backing-up-with-keld description: Writes and manages keld configuration for restic backups. Use when the user mentions keld, backup presets, restic config, or needs help writing TOML config for backups. license: GPL-3.0-or-later metadata: author: Amolith amolith@secluded.site
Keld is a TOML-configured wrapper around restic. It resolves layered config presets and exec's restic with the merged result.
Quick reference
# Run with a preset
keld --preset home@nas backup
# Dry-run: show what restic command would execute
keld --show-command --preset home@nas backup
# Interactive mode (TUI menu)
keld
# Override restic flags after the command
keld --preset home backup --tag daily --exclude '*.tmp'
# Override configured backup paths
keld --preset home backup /other/path
Config file discovery
Files are loaded in ascending priority order (later wins):
/usr/share/keld/config.toml, then sorted/usr/share/keld/conf.d/*.toml/etc/keld/config.toml, then sorted/etc/keld/conf.d/*.toml~/.config/keld/config.toml, then sorted~/.config/keld/conf.d/*.toml- Paths/globs from
KELD_CONFIG_PATHS(colon-separated) KELD_CONFIG_FILEreplaces all of the above if set
Config structure
Section merge order
For keld --preset home@cloud backup, sections merge in this order:
[global] → [global.backup] → [@cloud] → [@cloud.backup] →
[home@] → [home@.backup] → [home@cloud] → [home@cloud.backup] →
CLI overrides (replace, not append)
Plain presets (no @) are simpler: [global] → [global.backup] → [mypreset] → [mypreset.backup].
CLI flags and positional args replace their config counterparts — they do not merge with arrays or _arguments.
Split presets
The @ in a preset name composes two independent halves:
- Prefix (
home@): the what — defines sources, excludes, tags - Suffix (
@cloud): the where — defines repository, credentials
This lets you mix and match: home@cloud, home@nas, media@cloud all reuse their respective halves.
Special keys
| Key | Purpose |
|---|---|
_arguments |
Positional args for restic (prefer arrays; string form is whitespace-split) |
_workdir |
Directory to chdir before exec |
_command |
Restic subcommand (allows aliasing) |
*.environ |
Section suffix for environment variables |
Interpolation
Regular config values (not .environ) can reference other sections with ${section.key}:
[vars]
cache-root = "~/.cache/keld"
[global]
cache-dir = "${vars.cache-root}/restic"
Writing config
Minimal example
["@nas"]
repository = "sftp:nas:/backups/restic"
["home@".environ]
RESTIC_PASSWORD_COMMAND = "op read 'op://Vault/Backup/password'"
["home@".backup]
_arguments = ["/home/user/Documents", "/home/user/Projects"]
exclude-if-present = ".nobackup"
Invoke with: keld --preset home@nas backup
Repository paths within a backend
Restic supports paths inside rclone remotes and S3 buckets, so one remote or bucket can hold multiple independent repositories. Use the full preset section to set the path:
# Shared S3/B2 credentials — one suffix for the whole account
["@b2".environ]
AWS_ACCESS_KEY_ID = "your-key-id"
AWS_SECRET_ACCESS_KEY = "your-secret"
# Each dataset gets its own path within the bucket
["media@b2"]
repository = "s3:https://s3.example.com/my-bucket/media"
["docs@b2"]
repository = "s3:https://s3.example.com/my-bucket/docs"
The same works for rclone — one remote, different paths:
["media@hetzner"]
repository = "rclone:hetzner_restic:/media"
["docs@hetzner"]
repository = "rclone:hetzner_restic:/docs"
Services like BorgBase provide per-repository URLs with embedded credentials, so each repo is specific to one dataset. Use a split preset where the suffix is unique to that dataset (there's no shared suffix to reuse):
["media@borgbase_media"]
repository = "rest:https://user:pass@user.repo.borgbase.com"
Multi-value flags
Use TOML arrays for flags that accept multiple values:
["home@".backup]
_arguments = ["/home/user/Documents", "/home/user/Projects"]
exclude = ["*.tmp", "node_modules", ".git"]
tag = ["daily", "home"]
Multi-line strings also work — each line becomes a separate flag value:
exclude = """
*.tmp
node_modules
.git"""
Environment variables
Use the .environ suffix on any section to set env vars for restic:
["media@".environ]
RESTIC_PASSWORD_COMMAND = "op read 'op://Vault/Media Backup/password'"
["@b2".environ]
AWS_ACCESS_KEY_ID = "your-key-id"
AWS_SECRET_ACCESS_KEY = "your-secret"
Fetching secrets with _COMMAND
For env vars that restic doesn't natively support fetching via command
(anything other than RESTIC_PASSWORD_COMMAND), keld recognises a
_COMMAND suffix. The value is executed as a shell command, stdout is
captured (trailing newlines stripped), and the result is set as the env
var with the suffix removed:
["@b2".environ]
AWS_ACCESS_KEY_ID_COMMAND = "op read 'op://Vault/B2/access-key-id'"
AWS_SECRET_ACCESS_KEY_COMMAND = "op read 'op://Vault/B2/secret-access-key'"
This keeps secrets out of config files entirely. Any command that prints
the secret to stdout works (1Password CLI, pass, vault, etc.).
RESTIC_PASSWORD_COMMAND and RESTIC_FROM_PASSWORD_COMMAND are passed
through to restic as-is — restic handles those natively.
rclone credentials via env vars
rclone config values can be supplied as env vars named
RCLONE_CONFIG_<REMOTE>_<KEY>. Combined with _COMMAND, this lets
keld fetch rclone credentials from a secret manager too. Remote names
must use underscores (not hyphens) for this to work. Passwords must
be piped through rclone obscure -:
["@hetzner".environ]
RCLONE_CONFIG_HETZNER_RESTIC_USER_COMMAND = "op read 'op://Vault/Hetzner/username'"
RCLONE_CONFIG_HETZNER_RESTIC_PASS_COMMAND = "op read 'op://Vault/Hetzner/password' | rclone obscure -"
Key alias
repository in config maps to restic's --repo flag automatically.
Wrapped commands
Keld's interactive menu exposes: backup, restore, snapshots, forget, check, init. All other restic commands pass through as subcommands.
Environment variables
| Variable | Purpose |
|---|---|
KELD_CONFIG_FILE |
Single config file (replaces discovery) |
KELD_CONFIG_PATHS |
Colon-separated additional config paths |
KELD_DRYRUN |
Enable dry-run mode |
KELD_EXECUTABLE |
Override restic binary path |
Gotchas
- Across multiple config files, later files win at the top-level table layer
- Nested tables like
[preset.backup]and[*.environ]are replaced wholesale, not deep-merged across files restic.Run()usessyscall.Exec— it replaces the process. No Go code runs after, no defers execute- String-form
_argumentssplits on whitespace — paths with spaces require array form - Always verify new config before running live:
keld --show-command --preset <preset> <command> # Or test a specific file before placing it in a discovery path: keld --config ./keld.toml --show-command --preset <preset> <command>
Automation with systemd
For scheduled backups (with daily structural checks) and monthly full integrity verification with systemd user timers, see config-examples.md.