Say what each command needs in --help and config.example.yml
check / check (push) Successful in 11m11s

snapshot verify --help now says --deep needs the age private key, from
VAULTIK_AGE_SECRET_KEY or age_secret_key, as snapshot restore does.
Both now name the config key as well as the environment variable.

The help of snapshot create, list, purge, remove, prune, remote info
and remote nuke says which access to the destination it needs.
config.example.yml gains age_secret_key, and no longer tells the reader
to pipe age-keygen into grep, which discards the private key.

Only help strings and comments change.

Model: fable-5-1
This commit is contained in:
2026-11-06 18:30:34 +00:00
parent 8f34718a0f
commit 64c31f890b
7 changed files with 132 additions and 48 deletions
+7 -5
View File
@@ -289,12 +289,14 @@ func newConfigInitCommand() *cobra.Command {
Use: "init",
Short: "Write a default config file",
Long: `Creates a default configuration file with commented explanations
for every setting. If a config file already exists at the target path,
the command refuses to overwrite it.
for the settings it lists. If a config file already exists at the target
path, the command refuses to overwrite it. The file is created with mode
0600, because it can hold credentials.
The config is written to the path from --config, $VAULTIK_CONFIG, or
the platform default config directory (e.g. ~/Library/Application Support/
on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
config.yml in the platform default config directory
(~/Library/Application Support/vaultik/ on macOS, ~/.config/vaultik/ on
Linux, /etc/vaultik/ as root).`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
path := configPathForInit()
@@ -332,7 +334,7 @@ on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
func newConfigEditCommand() *cobra.Command {
return &cobra.Command{
Use: "edit",
Short: "Open the config file in $EDITOR",
Short: "Open the config file in $EDITOR (vi when it is not set)",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
path, err := ResolveConfigPath()
+5 -1
View File
@@ -17,7 +17,11 @@ func NewPruneCommand() *cobra.Command {
unreferenced blobs and directories under metadata/ with no manifest
from the backup destination store.
Local cleanup drops incomplete snapshots and any files, chunks, or
Needs permission to list, read and delete objects on the destination
store. It does not need the age private key.
Local cleanup drops snapshots whose metadata is gone from the
destination store, incomplete snapshots, and any files, chunks, or
blobs no longer referenced by a completed snapshot. Remote cleanup
scans every snapshot manifest in the destination store, builds the
set of still-referenced blob hashes, and deletes any blob not in that
+9 -4
View File
@@ -40,11 +40,13 @@ func newRemoteNukeCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "nuke",
Short: "Delete ALL snapshot metadata and blobs from the backup destination store",
Long: `Removes every snapshot's metadata and every blob from remote
storage. After this command completes successfully the bucket prefix is
Long: `Removes every snapshot's metadata, every blob and the destination
marker from remote storage, and each removed snapshot from the local
index. After this command completes successfully the bucket prefix is
empty and the next backup starts from scratch.
This is destructive and irreversible. Requires --force.`,
This is destructive and irreversible. Requires --force, and permission
to list and delete objects on the destination store.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
if !force {
@@ -75,7 +77,10 @@ func newRemoteInfoCommand() *cobra.Command {
- Size of all snapshot metadata (per snapshot and total)
- Count and total size of all blobs
- Count and size of referenced blobs (from all manifests)
- Count and size of orphaned blobs (not referenced by any manifest)`,
- Count and size of orphaned blobs (not referenced by any manifest)
Needs permission to list and read the destination store. It does not
need the age private key.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
// Use unified config resolution
+8 -5
View File
@@ -35,9 +35,10 @@ func NewRootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "vaultik",
Short: "Secure incremental backup tool with asymmetric encryption",
Long: `vaultik is a secure incremental backup tool that encrypts data using age
public keys and uploads to S3-compatible storage. No private keys are needed
on the source system.`,
Long: `vaultik is an incremental backup tool that encrypts data to age public
keys and uploads it to an S3-compatible object store, a local directory or
an rclone remote. The backed-up machine needs no private key: only
'snapshot restore' and 'snapshot verify --deep' need one.`,
SilenceUsage: true,
// Bare 'vaultik' (no subcommand): print help. The banner is
// printed once at process startup by Entry, before cobra
@@ -50,13 +51,15 @@ on the source system.`,
// Add global flags
cmd.PersistentFlags().StringVar(&rootFlags.ConfigPath, "config", "",
"Path to config file (default: $VAULTIK_CONFIG or platform config dir)")
"Path to config file (default: $VAULTIK_CONFIG, then the platform "+
"config directory, then /etc/vaultik/config.yml)")
cmd.PersistentFlags().BoolVarP(&rootFlags.Verbose, "verbose", "v", false,
"Enable verbose output")
cmd.PersistentFlags().BoolVar(&rootFlags.Debug, "debug", false,
"Enable debug output")
cmd.PersistentFlags().BoolVarP(&rootFlags.Quiet, "quiet", "q", false,
"Suppress non-error output")
"Suppress status output (warnings, errors, JSON documents, "+
"'config get' values and confirmation prompts are still printed)")
cmd.PersistentFlags().BoolVar(&rootFlags.SkipErrors, "skip-errors", false,
"Skip files that cannot be read when creating a snapshot, or "+
"that cannot be restored when restoring, instead of aborting "+
+66 -26
View File
@@ -60,6 +60,21 @@ func NewSnapshotCommand() *cobra.Command {
return cmd
}
// snapshotCreateLong is the long help text of 'snapshot create'.
const snapshotCreateLong = `Creates new snapshots of the configured directories.
If snapshot names are provided, only those snapshots are created.
If no names are provided, all configured snapshots are created.
Needs at least one public key in age_recipients, and permission to write
objects at the backup destination store. It does not need the age private
key. With --prune it also needs permission to list, read and delete objects
there.
The config is read from the path given by --config or VAULTIK_CONFIG;
otherwise from the platform config directory (~/.config/vaultik/config.yml
on Linux), then /etc/vaultik/config.yml.`
// newSnapshotCreateCommand creates the 'snapshot create' subcommand
func newSnapshotCreateCommand() *cobra.Command {
opts := &vaultik.SnapshotCreateOptions{}
@@ -67,15 +82,8 @@ func newSnapshotCreateCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create [snapshot-names...]",
Short: "Create new snapshots",
Long: `Creates new snapshots of the configured directories.
If snapshot names are provided, only those snapshots are created.
If no names are provided, all configured snapshots are created.
The config is read from the path given by --config or VAULTIK_CONFIG;
otherwise from the platform config directory (~/.config/vaultik/config.yml
on Linux), then /etc/vaultik/config.yml.`,
Args: cobra.ArbitraryArgs,
Long: snapshotCreateLong,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
err := opts.Validate()
if err != nil {
@@ -156,8 +164,15 @@ func newSnapshotListCommand() *cobra.Command {
Use: "list",
Aliases: []string{"ls"},
Short: "List all snapshots",
Long: "Lists all snapshots with their ID, timestamp, and compressed size",
Args: cobra.NoArgs,
Long: `Lists the snapshots in the local index and on the backup destination
store, with their ID, timestamp, and sizes.
Needs permission to list and read the destination store. It does not need
the age private key. A snapshot that is only on the destination store is
listed by its remote key, because its ID cannot be read without that key.
If the destination store cannot be listed, the command warns and lists the
local index alone.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return runVaultikApp(cmd, readOnly, false,
"Failed to list snapshots",
@@ -179,11 +194,19 @@ func newSnapshotPurgeCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "purge",
Short: "Purge old snapshots",
Long: `Removes snapshots based on age or count criteria.
Long: `Removes snapshots based on age or count criteria. Exactly one of
--keep-latest and --older-than is required.
Retention is per-snapshot-name: --keep-latest keeps the latest of each
configured snapshot name, not the latest globally. Use --snapshot to
restrict the operation to specific snapshot names.`,
Retention is per-snapshot-name: --keep-latest keeps the latest snapshot of
each name, not the latest globally. Use --snapshot to restrict the
operation to specific snapshot names.
Only snapshots in the local index are removed. Each one is removed from
the local index and its metadata is deleted from the backup destination
store. Blobs are not touched: run 'vaultik prune' afterwards.
Needs permission to list and delete objects on the destination store. If
the destination store cannot be listed, nothing is removed.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
// Validate flags
@@ -222,14 +245,26 @@ func newSnapshotVerifyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "verify <snapshot-id>",
Short: "Check a snapshot's blobs are present with the listed size",
Long: "Checks that every blob the snapshot's manifest lists is present\n" +
"in storage with the size the manifest records, and that the\n" +
"snapshot's encrypted database is present. It does not read blob\n" +
"contents; use --deep to download, decrypt, and re-hash every blob\n" +
"to detect corruption -- integrity, not who wrote it.\n\n" +
"The snapshot may be named by its ID or, on a host with no local\n" +
"index, by the remote key that 'snapshot list' prints for a\n" +
"remote-only snapshot (an unambiguous leading part is enough).",
Long: `Checks that every blob the snapshot's manifest lists is present
in storage with the size the manifest records, and that the
snapshot's encrypted database is present. It does not read blob
contents and does not need the age private key.
Use --deep to download, decrypt, and re-hash every blob to detect
corruption -- integrity, not who wrote it.
--deep requires the age private key, in the VAULTIK_AGE_SECRET_KEY
environment variable or as age_secret_key in the config file; the
environment variable wins. The variable may hold the whole age-keygen
file (comments and all of its identities are accepted); read it from
the file rather than typing the key, so it does not land in your shell
history:
export VAULTIK_AGE_SECRET_KEY="$(cat vaultik_backup_private_key.txt)"
The snapshot may be named by its ID or, on a host with no local
index, by the remote key that 'snapshot list' prints for a
remote-only snapshot (an unambiguous leading part is enough).`,
Args: requireSnapshotIDArg,
RunE: func(cmd *cobra.Command, args []string) error {
snapshotID := args[0]
@@ -264,7 +299,10 @@ func newSnapshotVerifyCommand() *cobra.Command {
},
}
cmd.Flags().BoolVar(&opts.Deep, "deep", false, "Download and verify blob hashes")
cmd.Flags().BoolVar(&opts.Deep, "deep", false,
"Download, decrypt and re-hash every blob; requires the age private "+
"key in VAULTIK_AGE_SECRET_KEY, or as age_secret_key in the config "+
"file")
cmd.Flags().BoolVar(&opts.JSON, "json", false, "Output verification results as JSON")
return cmd
@@ -288,8 +326,10 @@ know about), which is what 'vaultik prune' does. The command prints the
'vaultik prune' invocation to run as a follow-up only when it removed the
snapshot's metadata from the destination store, and never under --json.
An ID that names no snapshot in the local index or on the destination
store is an error.
The argument must be a snapshot ID: a remote key is not accepted. An ID
that names no snapshot in the local index or on the destination store is
an error. Needs permission to list and delete objects on the destination
store, unless --local-only is given.
Use --local-only to skip the remote half (e.g. when you want to forget a
snapshot locally without touching the destination store); the snapshot
+12 -4
View File
@@ -37,13 +37,21 @@ The snapshot may be named by its ID or, when restoring on a host with no
local index, by the remote key that 'snapshot list' prints for a
remote-only snapshot (an unambiguous leading part is enough).
Requires the age private key in the VAULTIK_AGE_SECRET_KEY environment
variable. The variable may hold the whole age-keygen file (comments and
all of its identities are accepted); read it from the file rather than
typing the key, so it does not land in your shell history:
Each file is restored under its original absolute path inside the target
directory. File owners are restored only when the command runs as root.
Requires the age private key, in the VAULTIK_AGE_SECRET_KEY environment
variable or as age_secret_key in the config file; the environment
variable wins. The variable may hold the whole age-keygen file (comments
and all of its identities are accepted); read it from the file rather
than typing the key, so it does not land in your shell history:
export VAULTIK_AGE_SECRET_KEY="$(cat vaultik_backup_private_key.txt)"
Also needs permission to read the backup destination store (and to list
it when a remote key is given), and free space in the temporary directory
for the decrypted blobs.
Examples:
# Restore entire snapshot
vaultik snapshot restore myhost_docs_2025-01-01T12:00:00Z /restore