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-filesto ensure the file is not tracked by gitAppends to
.gitignoreunless--no-gitignoreis 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
sealoradd)
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:
--vault DIRNIMVAULT_VAULT_REPOa
.nimvaultpointer file in the current repository, naming the vault onethe current git repository
See the private vault repository how-to.
4 Global options¶
--recipient KEYGPG recipient key ID. Overrides env var and config file. See configuration for the full resolution chain.
--helpShow help for the current command.
--versionShow 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:
Manifest signature is checked during decryption
Blob SHA-256 hashes are compared against the manifest
Target paths are validated against directory traversal
Blobs are decrypted to temporary files
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
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>`_).