---
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](https://restic.net/). It resolves layered config presets and exec's restic with the merged result.

## Quick reference

```bash
# 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):

1. `/usr/share/keld/config.toml`, then sorted `/usr/share/keld/conf.d/*.toml`
2. `/etc/keld/config.toml`, then sorted `/etc/keld/conf.d/*.toml`
3. `~/.config/keld/config.toml`, then sorted `~/.config/keld/conf.d/*.toml`
4. Paths/globs from `KELD_CONFIG_PATHS` (colon-separated)
5. `KELD_CONFIG_FILE` replaces **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}`:

```toml
[vars]
cache-root = "~/.cache/keld"

[global]
cache-dir = "${vars.cache-root}/restic"
```

## Writing config

### Minimal example

```toml
["@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:

```toml
# 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:

```toml
["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):

```toml
["media@borgbase_media"]
repository = "rest:https://user:pass@user.repo.borgbase.com"
```

### Multi-value flags

Use TOML arrays for flags that accept multiple values:

```toml
["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:

```toml
exclude = """
*.tmp
node_modules
.git"""
```

### Environment variables

Use the `.environ` suffix on any section to set env vars for restic:

```toml
["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:

```toml
["@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 -`:

```toml
["@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()` uses `syscall.Exec` — it replaces the process. No Go code runs after, no defers execute
- String-form `_arguments` splits on whitespace — paths with spaces require array form
- Always verify new config before running live:
  ```bash
  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](references/config-examples.md).
