nimvault/commands

Search:
Group by:

Vault commands: seal, unseal, add, rm, mv, list, status, scan.

All commands take a repo path and GpgConfig. Parallel GPG via startProcess with direct invocation (no shell).

Types

CheckResult = tuple[problems: seq[string], checked: int]

Procs

proc add(repo, path: string; cfg: GpgConfig; noGitignore = false) {....raises: [
    OSError, IOError, NimvaultError, ValueError, Exception, KeyError, RegexError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    RootEffect, WriteIOEffect, ExecIOEffect], forbids: [].}
Add a file by its target path.
proc addDir(repo, dirPath: string; cfg: GpgConfig; noGitignore = false) {....raises: [
    OSError, IOError, NimvaultError, Exception, ValueError, KeyError, RegexError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    WriteIOEffect, ExecIOEffect, RootEffect], forbids: [].}
Add a directory recursively to the vault.
proc addDirReport(repo, dirPath: string; cfg: GpgConfig; noGitignore = false): string {....raises: [
    OSError, IOError, NimvaultError, Exception, ValueError, KeyError, RegexError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    WriteIOEffect, ExecIOEffect, RootEffect], forbids: [].}
proc addReport(repo, path: string; cfg: GpgConfig; noGitignore = false): string {....raises: [
    OSError, IOError, NimvaultError, ValueError, Exception, KeyError, RegexError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    RootEffect, WriteIOEffect, ExecIOEffect], forbids: [].}
proc check(repo: string; cfg: GpgConfig) {.
    ...raises: [OSError, Exception, IOError, ValueError, NimvaultError], tags: [
    ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}
Terminal check. Raises when anything is inconsistent so a hook or a continuous-integration job fails rather than reporting and passing.
proc checkReport(repo: string; cfg: GpgConfig): string {.
    ...raises: [OSError, Exception, IOError, ValueError, NimvaultError], tags: [
    ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}
Library-friendly check (no colors, no exit).
proc checkVault(repo: string; cfg: GpgConfig): CheckResult {.
    ...raises: [OSError, Exception, IOError, ValueError, NimvaultError], tags: [
    ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}

Verify the vault is internally consistent, without decrypting anything.

status answers a different question: it compares the plaintext on this machine against the manifest's contentHash, so it says nothing about whether each blob still matches the hash the manifest records for it. A blob committed without the manifest that vouches for it therefore looks perfectly healthy right up until an unseal on some other machine, where the plaintext is gone and the failure is unrecoverable rather than inconvenient.

The manifest is a signed list of per-blob digests, which is a one-level Merkle construction (Merkle, CRYPTO 1987, doi:10.1007/3-540-48184-2_32): checking each leaf against it is enough to detect a blob that has drifted, and needs no key material.

proc doctor(repo: string; cfg: GpgConfig) {.
    ...raises: [IOError, OSError, NimvaultError, Exception, ValueError], tags: [
    WriteIOEffect, ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect,
    RootEffect, TimeEffect], forbids: [].}
proc doctorReport(repo: string; cfg: GpgConfig): string {.
    ...raises: [OSError, NimvaultError, Exception, IOError, ValueError], tags: [
    ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}
What a human or CI needs to know before seal/unseal: binaries, recipient, backend, pointer, lock directory, and check.
proc gc(repo: string; cfg: GpgConfig; dryRun = false) {.
    ...raises: [OSError, IOError, NimvaultError, Exception, ValueError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    ExecIOEffect, RootEffect, WriteIOEffect], forbids: [].}
Remove blobs no manifest entry points at.
proc get(repo, path: string; cfg: GpgConfig; allowUnsigned = false): string {....raises: [
    OSError, Exception, IOError, ValueError, NimvaultError, OSError, IOError,
    NimvaultError, ValueError, KeyError], tags: [ReadDirEffect, ReadEnvEffect,
    ReadIOEffect, ExecIOEffect, RootEffect, WriteIOEffect, TimeEffect,
    WriteDirEffect], forbids: [].}

Decrypt one tracked entry and return its plaintext. Nothing is written to disk and nothing else in the vault is touched.

unseal is all-or-nothing and materialises every entry, which is the wrong shape for a caller that wants one credential: a forced-command SSH gate serving a single allowlisted secret, a service reading one token at start up. Without this those callers shell out to gpg against the blobs directly, which works but puts secrets outside anything list, status or scan can see.

The same checks unseal performs still apply. A caller asking for one entry has no less need of a path-safety check and an integrity check than one asking for all of them.

proc initVault(repo, recipient: string) {.
    ...raises: [OSError, IOError, NimvaultError],
    tags: [ReadDirEffect, WriteDirEffect, ReadEnvEffect, WriteIOEffect],
    forbids: [].}
Write .vault/config so later commands have a recipient.
proc installHooks(repo: string) {....raises: [NimvaultError, IOError, OSError], tags: [
    ReadDirEffect, WriteIOEffect, WriteDirEffect], forbids: [].}
Install a pre-push hook that runs nimvault check and a pre-commit hook that runs nimvault scan. Refuse to overwrite an existing hook.
proc list(repo: string; cfg: GpgConfig) {.
    ...raises: [IOError, OSError, Exception, ValueError, NimvaultError], tags: [
    WriteIOEffect, ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect,
    RootEffect, TimeEffect], forbids: [].}
proc listReport(repo: string; cfg: GpgConfig): string {.
    ...raises: [OSError, Exception, IOError, ValueError, NimvaultError], tags: [
    ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}
Library-friendly list (no terminal styling). Used by CLI and C ABI.
proc lockPlaintext(repo: string; cfg: GpgConfig) {.
    ...raises: [OSError, IOError, NimvaultError, Exception, ValueError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    ExecIOEffect, RootEffect, WriteIOEffect], forbids: [].}
Remove in-sync plaintext. The vault entry stays; unseal brings it back. A modified file is refused: lock is not a way to discard unsealed edits.
proc move(repo, oldPath, newPath: string; cfg: GpgConfig) {....raises: [OSError,
    IOError, NimvaultError, Exception, ValueError, KeyError, RegexError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    RootEffect, ExecIOEffect, WriteIOEffect], forbids: [].}
proc moveReport(repo, oldPath, newPath: string; cfg: GpgConfig): string {....raises: [
    OSError, IOError, NimvaultError, Exception, ValueError, KeyError, RegexError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    RootEffect, ExecIOEffect, WriteIOEffect], forbids: [].}
proc orphans(repo: string; cfg: GpgConfig): seq[string] {.
    ...raises: [OSError, Exception, IOError, ValueError, NimvaultError], tags: [
    ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}

Blob files in .vault/ that no manifest entry points at.

Reachable when a mutation is interrupted between writing a blob and writing the manifest that names it, which the lock makes rarer but does not remove. An orphan is not merely clutter: it is a readable copy of a secret that nothing tracks, so nothing will ever rotate or remove it.

proc remove(repo, path: string; cfg: GpgConfig) {....raises: [OSError, IOError,
    NimvaultError, Exception, ValueError, KeyError, RegexError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    ExecIOEffect, RootEffect, WriteIOEffect], forbids: [].}
proc removeReport(repo, path: string; cfg: GpgConfig): string {....raises: [
    OSError, IOError, NimvaultError, Exception, ValueError, KeyError, RegexError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    ExecIOEffect, RootEffect, WriteIOEffect], forbids: [].}
proc rotate(repo: string; cfg: GpgConfig; rekey = false) {....raises: [OSError,
    Exception, IOError, ValueError, NimvaultError, RegexError, KeyError], tags: [
    ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect, WriteDirEffect], forbids: [].}

Re-wrap the data keys to the current recipients, or re-encrypt everything.

These are different operations, and the difference is the reason v6 exists, so they are separate flags rather than one command that guesses.

Default: the data-key file is decrypted and written back to whoever .vault/config now names. Every payload is untouched, so adding a machine costs one small file instead of the whole vault. What that gives is rotation of the wrapping: a recipient key that should no longer open the vault stops being able to.

rekey: every payload is re-encrypted under a fresh data key. Only that answers a suspected compromise of a data key or of the plaintext, because rewrapping leaves the old ciphertext openable by whoever already holds the old key (Everspaugh et al., CRYPTO 2017, doi:10.1007/978-3-319-63697-9_4). Churn is the intent, so it deliberately bypasses the incremental skip.

proc scan(repo: string; target: string; cfg: GpgConfig) {....raises: [ValueError,
    OSError, NimvaultError, Exception, IOError, RegexError], tags: [
    ReadEnvEffect, ReadIOEffect, ReadDirEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}
Walk target (file or dir) and flag any file containing a secret pattern that is NOT already in the vault.
proc scanReport(repo, target: string; cfg: GpgConfig): string {.
    ...raises: [ValueError, OSError, RegexError, Exception, IOError], tags: [
    ReadEnvEffect, ReadIOEffect, ReadDirEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}
Scan without process exit. Returns report; raises only on I/O errors. If secrets found, still returns the report text (no raise) so MCP can show it.
proc seal(repo: string; cfg: GpgConfig; force = false) {....raises: [OSError,
    IOError, NimvaultError, Exception, ValueError, RegexError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    ExecIOEffect, RootEffect, WriteIOEffect], forbids: [].}

Encrypt every tracked file whose plaintext changed since the last seal.

Encryption output is not deterministic: GPG and age both draw a fresh session key per run, so re-encrypting a file nobody edited still produces a different blob. Sealing unconditionally therefore rewrote every blob in the repo on every run, and a one-file change arrived as a diff touching the whole vault. Files are skipped when the plaintext hash, the blob and the blob's own hash all still agree with the manifest. force re-encrypts regardless, as does any change to the seal key.

proc sealReport(repo: string; cfg: GpgConfig): string {....raises: [OSError,
    IOError, NimvaultError, Exception, ValueError, RegexError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    ExecIOEffect, RootEffect, WriteIOEffect], forbids: [].}
proc status(repo: string; cfg: GpgConfig) {....raises: [OSError, Exception,
    IOError, ValueError, NimvaultError, NimvaultError, IOError, OSError,
    ValueError], tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect,
                        ExecIOEffect, RootEffect, WriteIOEffect, TimeEffect,
                        WriteDirEffect], forbids: [].}
Terminal status (colors). Delegates logic to statusReport for library reuse.
proc statusReport(repo: string; cfg: GpgConfig): string {....raises: [OSError,
    Exception, IOError, ValueError, NimvaultError, NimvaultError, IOError,
    OSError, ValueError], tags: [ReadDirEffect, ReadEnvEffect, ReadIOEffect,
                                 ExecIOEffect, RootEffect, WriteIOEffect,
                                 TimeEffect, WriteDirEffect], forbids: [].}
Library-friendly status (no colors). Fast path uses contentHash when present.
proc unseal(repo: string; cfg: GpgConfig; allowUnsigned = false;
            only: seq[string] = @[]) {....raises: [OSError, IOError, NimvaultError,
    Exception, ValueError, OSError, IOError, NimvaultError, ValueError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    ExecIOEffect, RootEffect, WriteIOEffect], forbids: [].}

Decrypt tracked entries back to their target paths. With no selector this restores the whole vault; with one it restores only what was named.

The selective form exists because all-or-nothing forces the caller to express any partial restore somewhere else. A machine that should hold a subset of the vault otherwise has to model that split in whatever tool sits above this one, which is both duplicated and invisible from here.

proc unsealReport(repo: string; cfg: GpgConfig; allowUnsigned = false;
                  only: seq[string] = @[]): string {.
    ...raises: [OSError, IOError, NimvaultError, Exception, ValueError], tags: [
    ReadEnvEffect, ReadDirEffect, ReadIOEffect, WriteDirEffect, TimeEffect,
    ExecIOEffect, RootEffect, WriteIOEffect], forbids: [].}
proc who(repo: string; cfg: GpgConfig) {....raises: [IOError, OSError, Exception,
    ValueError, NimvaultError, RegexError], tags: [WriteIOEffect, ReadDirEffect,
    ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect, TimeEffect],
    forbids: [].}
proc whoReport(repo: string; cfg: GpgConfig): string {....raises: [OSError,
    Exception, IOError, ValueError, NimvaultError, RegexError], tags: [
    ReadDirEffect, ReadEnvEffect, ReadIOEffect, ExecIOEffect, RootEffect,
    WriteIOEffect, TimeEffect], forbids: [].}
Recipients that the current wrap rules assign to each entry.