secrets-vault
AES-256-GCM encrypted key-value vault for API keys and tokens, instead of plaintext
dotfiles. Ships a secrets CLI and an embeddable Rust library.
QVLT v2 encrypts per entry: reading one secret decrypts exactly one record, never the whole vault, and writes splice in without touching unrelated ciphertext.
Install
[]
= { = "2", = false } # library only, 6 deps
CLI
set KEY [VALUE] Store a secret (prompts if no value; pass on stdin to keep it off argv)
gen KEY Generate a random secret — the value is never printed
get KEY Retrieve to stdout, no trailing newline
delete KEY Remove a secret
list List key names (sorted)
env [--json] Emit all as `export KEY='VALUE'` (or JSON) for eval
import / export KEY=VALUE lines in / out
migrate Upgrade a v1 vault to v2 per-entry encryption (keeps a .v1.bak)
rekey Re-encrypt everything under a fresh salt (run after revoking access)
eval $(secrets env)puts the entire vault in the environment, where every child process inherits all of it — includingnpm/pippostinstall scripts and AI agents. On macOS, prefer the scoped commands below.
macOS: biometric vault + scoped injection
)
)
;
; )
);
)
Declare what a project needs in .secrets.toml — names only, values stay in the
vault (see .secrets.toml.example):
[]
= ["DATABASE_URL", "STRIPE_SECRET_KEY"]
# one Touch ID tap → decrypts only those keys → injects into the child → zeroizes
The master passphrase lives in a data-protection Keychain item in a team-prefixed
access group: only this Developer-ID-signed binary can reach it, and only after a tap.
--strict stores it with BiometryCurrentSet and a zero-reuse LAContext, so every
read re-prompts instead of riding macOS's Touch ID grace window.
A lease trades that tap away knowingly, once, for a bounded window: its key sits in a non-biometric Keychain item in the same entitlement-gated access group — tap-free for this signed binary, unreachable for anything else — and the authoritative expiry lives beside the key (not in the agent-writable filesystem), enforced on every read. Tap-free means tap-free for any same-user caller of the CLI: the lease narrows a compromise to one project's declared keys, until expiry, one audited read at a time.
Agent identity is resolved from process ancestry and is a soft layer (spoofable by a same-user adversary); the hard boundary is the Touch ID tap.
Other backends
A project can pull from Google Secret Manager ([gsm]) or any secret manager with a
CLI via a generic [backend] block — reads return on stdout and writes are piped to
stdin, so values never land in argv. AWS Secrets Manager, HashiCorp Vault, Doppler
and 1Password recipes are in .secrets.toml.example.
Library
use Vault;
let mut vault = new;
vault.set;
let encrypted = vault.encrypt?;
let vault = decrypt?;
assert_eq!;
new from_map get set delete keys iter len is_empty to_map
encrypt decrypt to_shell_exports to_json, plus is_valid_key,
parse_env_lines, encrypt_blob/decrypt_blob and the v2 MasterSecret /
is_v2 helpers.
match decrypt
Cryptography
| Component | Algorithm |
|---|---|
| Encryption | AES-256-GCM (NIST SP 800-38D) |
| Key derivation | PBKDF2-HMAC-SHA256, 600k iterations (RFC 8018) |
| Per-entry / manifest / registry keys | HKDF-SHA256 |
| Manifest integrity | HMAC-SHA256, constant-time verify |
| Salt / nonce | 128-bit / 96-bit, random, fresh per save |
Authenticated encryption — tampered data is rejected, not decrypted to garbage. Secret values are zeroized on drop. A wrong passphrase fails GCM authentication immediately.
The file format is binary-compatible with the Zig implementation.
Environment
| Variable | Description |
|---|---|
SECRETS_PASSPHRASE |
Passphrase for non-interactive use. Convenient for CI; it is visible to anything that can read the process environment, so prefer unlock + exec on macOS. Stripped from exec children. |
SECRETS_DIR |
Vault directory (default ~/.config/secrets) |
SECRETS_GSM_ACCOUNT |
Override the active gcloud account for the GSM backend |
SECRETS_GSM_IMPERSONATE |
Service account to impersonate for GSM |
SECRETS_APPROVAL_DIR |
Approval handshake dir (default ~/.secrets/pending_approvals) |
SECRETS_APPROVAL_TIMEOUT_SECS |
Approval wait before failing closed (default 30) |
License
MIT