CLI Reference

1 Top-level usage

nimvault {SUBCMD} [sub-command options & parameters]

nimvault auto-detects the git repository root via git rev-parse --show-toplevel and uses .vault/ within it. All commands accept the optional --recipient KEY flag.

The CLI is built on cligen with dispatchMulti for subcommand routing. Each subcommand is a thin wrapper around the corresponding commands module proc.

2 Commands

2.1 seal

nimvault seal [--recipient KEY] [--force]

Encrypt every entry whose plaintext changed, and write the manifest.

Only changed files are re-encrypted. Encryption output is not deterministic – both backends draw a fresh session key per run – so re-encrypting an untouched file would still produce a different blob, and a one-file edit used to arrive as a diff touching every blob in the vault. An entry is skipped when its plaintext hash, its blob, and that blob’s own hash all still agree with the manifest, so a deleted or corrupted blob is rebuilt rather than skipped.

A seal that changed nothing does not rewrite the manifest either. The manifest is encrypted and signed, so writing it is a change in its own right.

Everything is re-encrypted regardless when the seal key changes, which covers the backend, recipient, identity and signer: unchanged plaintext still owes a fresh blob under a new key.

Blobs and the manifest are flushed in crash-safe order – payloads first, then the manifest that records their digests.

Exits non-zero if any plaintext file is missing.

2.1.1 --force

Re-encrypt every entry, not only the changed ones.

2.2 unseal

nimvault unseal [PATH...] [--recipient KEY] [--allow-unsigned] [--vault DIR]

With no path this restores the whole vault. Naming paths restores only those entries, and a directory restores everything beneath it. A selector that matches nothing is an error.

Before decryption, verifies:

  • Manifest GPG signature

  • Blob SHA-256 hashes against manifest

  • Path safety (no directory traversal)

Decryption writes to temporary files first. Only after all signatures and hashes pass does the tool move files to their final paths. This atomic approach prevents release of unverified plaintext.

Creates parent directories as needed. Sets file permissions to 600 (owner read/write only).

Exits non-zero if:

  • Any blob file is missing

  • Any blob hash does not match the manifest

  • Any signature is invalid or missing (unless --allow-unsigned)

  • Any target path escapes the expected boundary

2.2.1 --allow-unsigned

Accept unsigned v1 vaults. Skips signature and hash verification. Bad signatures (BADSIG) are still fatal even with this flag. Use this only for migrating legacy vaults created before v0.2.0. Run nimvault seal afterwards to upgrade the vault to signed v2 format.

2.3 add-dir

nimvault add-dir <directory> [--recipient KEY] [--no-gitignore]

Add a directory recursively to the vault. Walks the directory tree and adds each file individually with its own encrypted blob. The directory structure is preserved via the relative paths stored in the manifest.

Each file undergoes the same checks as nimvault add:

  • Verifies the file is not already in the vault

  • Checks git ls-files to ensure the file is not tracked by git

  • Appends to .gitignore unless --no-gitignore is passed

Use add-dir for directories like .beads, .env, or any folder containing multiple sensitive files.

Exits non-zero if:

  • The directory does not exist

  • The directory is empty

  • Any file is already in the vault

  • Any file is tracked by git

2.4 add

nimvault add <path> [--recipient KEY] [--no-gitignore]

Add a file to the vault. The path is stored with ~/ for portability. Generates a random 16-char hex ID, encrypts and signs the file, computes a SHA-256 blob hash, and appends to the manifest.

Checks git ls-files to verify the file is not already tracked by git. If the file is tracked, exits with an error instructing the user to untrack it first.

Appends the file path to .gitignore unless --no-gitignore is passed or the path is already gitignored.

Exits non-zero if:

  • The file does not exist

  • The file is already in the vault

  • The file is tracked by git

2.5 rm

nimvault rm <path> [--recipient KEY]

Remove a file from the vault. Deletes the encrypted blob and removes the manifest entry. Does NOT delete the local plaintext file.

2.6 mv

nimvault mv <old-path> <new-path> [--recipient KEY]

Move/rename a vault entry’s target path. If the file exists at the old path, it is physically moved. The encrypted blob is unchanged; only the manifest entry is updated.

2.7 get

nimvault get <path> [--allow-unsigned]

Print one entry’s plaintext on stdout without unsealing the vault or writing anything to disk. Applies the same path-safety and blob-integrity checks unseal does. Deliberately quiet, so nothing decorative shares the stream with a secret.

2.8 check

nimvault check [--recipient KEY]

Verify every blob against the hash the manifest records for it. Decrypts nothing and needs no key material, so it is cheap enough for a pre-push hook or a continuous-integration job. Exits non-zero when anything is inconsistent.

This answers a different question from status. status compares the plaintext on this machine against the manifest, so it says nothing about whether the blobs still match; a blob committed without the manifest that vouches for it looks healthy there right up until an unseal somewhere else. Reports a missing blob, a drifted blob, and a v1 entry that carries no hash.

2.9 gc

nimvault gc [--recipient KEY] [--dry-run]

Remove blobs in .vault/ that no manifest entry points at.

Orphans are reachable when a mutation is interrupted between writing a blob and writing the manifest that names it. The vault lock makes that rarer but does not remove the case. An orphan is not only clutter: it is a readable copy of a secret that nothing tracks, so nothing will ever rotate or remove it.

Never touches the manifest, the data-key files, or any blob a live entry names.

2.9.1 --dry-run

List what would be removed and remove nothing.

2.10 rotate

nimvault rotate [--recipient KEY] [--rekey]

Rewrap the per-file data keys to whoever .vault/config now names. Payloads are untouched, so adding a machine costs one small file rather than a full re-encrypt.

That rotates the wrapping only: a recipient key that should no longer open the vault stops being able to. It does nothing about a data key that has already leaked, because the old ciphertext is unchanged and the old key still opens it.

2.10.1 --rekey

Re-encrypt every payload under a fresh data key. This is the form that answers a suspected compromise of a data key or of the plaintext, and the only one that does. Churn is the intent, so it bypasses the incremental skip.

2.11 scan

nimvault scan [PATH...] [--vault DIR]

Look for plaintext secrets that are not in the vault.

2.12 list

nimvault list [--recipient KEY]

List all vault entries showing their ID and target path.

2.13 status

nimvault status [--recipient KEY]

Show sync status of all vault entries by comparing local plaintext SHA-256 to the manifest contentHash. Only v3-and-earlier entries that lack a content hash are decrypted:

[in-sync]

local matches vault

[modified]

local differs from vault (needs seal)

[missing]

local file not found (needs unseal)

[no-blob]

encrypted blob missing (needs seal or add)

Directory entries: When you add-dir a directory, each file appears individually in list and status output. The directory structure is implicit in the paths.

2.14 who

nimvault who [--recipient KEY] [--vault DIR]

Print which wrap-rule recipients can open each entry.

2.15 lock

nimvault lock [--recipient KEY] [--vault DIR]

Remove in-sync plaintext. Vault entries stay; unseal restores them. A modified file is refused.

2.16 init

nimvault init [--recipient KEY] [--vault DIR]

Write .vault/config with the recipient. Refuses if the file already exists.

2.17 hook

nimvault hook [--vault DIR]

Install a pre-push hook that runs nimvault check and a pre-commit hook that runs nimvault scan. Refuses to overwrite existing hooks.

2.18 doctor

nimvault doctor [--recipient KEY] [--vault DIR]

Print backend, recipient, binaries, manifest and check status. Exits non-zero when something will not work.

3 Where the vault lives

Every command accepts --vault DIR. The vault repository need not be the one you are standing in, which is what lets .vault/ sit in a private repository while the dotfiles it protects stay public. Resolution order:

  1. --vault DIR

  2. NIMVAULT_VAULT_REPO

  3. a .nimvault pointer file in the current repository, naming the vault one

  4. the current git repository

See the private vault repository how-to.

4 Global options

--recipient KEY

GPG recipient key ID. Overrides env var and config file. See configuration for the full resolution chain.

--help

Show help for the current command.

--version

Show nimvault version.

5 Exit codes

Code

Meaning

0

Success

1

Error (missing file, GPG failure, integrity check, traversal, etc)

6 Security model

All encryption operations also GPG-sign the payload. On unseal, signatures and blob hashes are verified before any plaintext is written to disk:

  1. Manifest signature is checked during decryption

  2. Blob SHA-256 hashes are compared against the manifest

  3. Target paths are validated against directory traversal

  4. Blobs are decrypted to temporary files

  5. Signatures are verified on each blob that carries one in band. gpg blobs encrypted to a recipient do; age blobs and v6 enveloped blobs do not, and take the same guarantee from the signed manifest plus the digest checked at step 2

  6. Only after all checks pass are files moved to their final paths

This design prevents forgery, ciphertext swapping, path traversal, and release of unverified plaintext.

6.1 Known limitations

These are inherent trade-offs of Git-backed OpenPGP vaults, not bugs.

6.1.1 Metadata leakage

Because the tool encrypts files individually, an observer analyzing the Git history can track update frequency, ciphertext size variations, and file count over time. GPG ciphertext sizes correlate with plaintext sizes, so relative file sizes are visible. Wrapping the entire payload in an archive before encryption would conceal this metadata but sacrifice Git delta efficiency. For most personal vault use cases this trade-off is acceptable.

6.1.2 No forward secrecy

An adversary who archives the public Git history today and obtains a key later can decrypt every historical version. Git keeps the old ciphertext whatever the current vault holds, so no operation on the working tree reaches it.

Be careful which rotation is meant here. nimvault rotate rewraps the data keys to new recipients and leaves payloads alone, so it does nothing for an archived copy: the old blobs and the old data keys are both still in history. rotate --rekey re-encrypts the payloads, which changes what is stored from now on but does not reach back either. Only rewriting the history removes the old ciphertext, and only a private remote avoids the archiving in the first place.

The distinction between rewrapping a key and re-encrypting the data it protects is treated formally in Everspaugh et al., CRYPTO 2017 (doi:10.1007/978-3-319-63697-9:sub:`4<https://doi.org/10.1007/978-3-319-63697-9_4>`_).