kvlt /kΚlt/ noun β pronounced βcultβ; the v stands in for u, the old-Norse-runic spelling popularized by black-metal aesthetics (cf. trve kvlt).
π Pluggable secrets vault. Local-first. No daemon.
A single-binary secrets vault for projects that don't have HashiCorp
Vault and don't want one. Encrypts with age
using your existing SSH keys; named vaults give you a stable call
site (kvlt get prod API_KEY) regardless of whether the backend is
local age files today, AWS Secrets Manager tomorrow.
- π age + SSH keys β encrypts with
~/.ssh/id_ed25519.pub, decrypts with the matching private key. Borrows your existing protection chain (passphrase + ssh-agent + Touch ID via secretive); kvlt doesn't reinvent the lock. - πͺͺ Named vaults β
kvlt get prod API_KEY, neverkvlt get_aws(β¦); the backend is an implementation detail. Switching from local age files to AWS Secrets Manager later doesn't touch a single call site. - π Pluggable backends β
Providerinterface + factory registry. AWS Secrets Manager (planned) sits behind a//go:build awsguard so the base binary stays dependency-light. - π₯ Multi-recipient β encrypt to N SSH public keys, any one of those private keys can decrypt. The team-sharing escape hatch.
- π€« Stdin / TTY input modes β
echo $VAL | kvlt putkeeps secrets out of shell history; barekvlt putprompts with echo off. - π Shell-friendly β
kvlt env vaultforeval "$(β¦)"direnv integration;kvlt run vault -- cmdfor scoped env injection likeaws-vault exec/op run. - π« No service, no daemon β pure CLI; nothing listening, nothing persistent.
- π¦ Single static binary β Go, CGO off, darwin / linux / windows Γ amd64 / arm64.
curl -fsSL https://github.com/retr0h/kvlt/raw/main/install.sh | shInstalls to ~/.local/bin (or /usr/local/bin as root) β SHA256 checksums verified. Override with KVLT_INSTALL_DIR=/some/path or pin a version with KVLT_VERSION=1.1.1.
git clone https://github.com/retr0h/kvlt.git
cd kvlt
go build -o kvlt .
install -m 755 kvlt ~/.local/bin/kvltCloud backends opt in via build tags: go build -tags aws -o kvlt .
kvlt vault create --name dev # bootstrap a vault (encrypts to ~/.ssh/id_ed25519.pub)
kvlt vault create --name prod -p ~/.ssh/team.pub # encrypt to a non-default public key
kvlt secret put --vault dev --key API_KEY --value sk-1234 # store a secret (lands in shell history)
echo "$DB_PASS" | kvlt secret put --vault dev --key DB_PASS # stdin β no shell history
kvlt secret put --vault dev --key TOKEN # interactive, echo off
kvlt secret import --vault dev --env ~/.env # bulk-import a dotenv file
kvlt secret import --vault dev --file ~/kc.yaml --key KC # one whole file stored as one secret
kvlt secret get --vault dev --key API_KEY # decrypt β prompts for SSH passphrase if not in agent
kvlt secret get --vault dev --key API_KEY -i ~/work/id_ed25519 # use a specific private key
kvlt secret list --vault dev # names only, never values
kvlt secret delete --vault dev --key OLD_KEY # remove a secret (prompts unless --force)
kvlt env --vault dev # all secrets as `export KEY=VALUE` for `eval`
kvlt run --vault dev -- npm start # exec child with vault secrets in env
kvlt vault delete --name dev # delete the vault + every secret in it (prompts)Override the default decrypt key globally with KVLT_PRIVATE_KEY=/path/to/key.
Full recipe collection in docs/recipes.md.
Most secret stuff on a dev laptop is a plaintext file. ~/.aws/credentials,
~/.config/gh/hosts.yml (your GitHub PAT), .env files, npm tokens in
~/.npmrc, GitLab tokens in ~/.config/glab-cli/, every .kube/config β
all sitting there in plaintext, readable by any process running as you. Once
malware has user-level execution on your machine, every one of those is
immediate, no friction.
The clever bit isn't kvlt β it's delegating the lock to the SSH protection chain, one of the few credential systems on a dev machine that actually has a human-in-the-loop step:
| What's on disk | Attacker w/ user code-exec does | Result |
|---|---|---|
~/.aws/credentials plaintext |
cat |
full AWS access, instantly |
.env with STRIPE_KEY=β¦ |
cat |
Stripe access, instantly |
gh / glab / npm tokens |
cat |
git host access, instantly |
| Key-file vault (key.txt next to blobs) | cat key.txt && cat blob |
decrypt, instantly β key file is the secret |
| kvlt + passphrase-locked SSH key | reads .age + encrypted key file |
needs the passphrase |
| kvlt + ssh-agent (timed unlock) | tries to decrypt | needs the passphrase to (re-)unlock the agent |
| kvlt + Secretive on macOS | tries to decrypt | needs your fingerprint (Touch ID) |
Every kvlt decrypt requires something that isn't on disk: your typed
passphrase, ssh-agent's in-memory unlock state, or a Touch ID prompt routed
through the Secure Enclave. Reading every file under $HOME gets the
attacker .age blobs β useless without the key β and an encrypted
private key file, useless without the passphrase. The credentials never
exist as plaintext at rest.
This is why a .env -> kvlt swap is a real upgrade, not just a re-shuffle.
Vault designs that store the encryption key as a sibling text file in
the repo don't help either β an attacker grabbing the vault grabs the
key. kvlt moves the key out of the filesystem entirely; what's left on
disk is useless without something off-disk (your passphrase, the agent's
unlocked state, your fingerprint).
Honest about the limits:
- Cached ssh-agent unlock β once the agent is unlocked, anything running
as you can sign with it. Mitigate with
ssh-add -t 1hfor time-limited caching, or skip the agent entirely on macOS by using Secretive (key lives in the Secure Enclave, every signature requires Touch ID). - Keylogger on the box captures the passphrase the next time you type it. Beyond software's job.
.ageblobs are still copyable β an attacker with the blobs can sit on them waiting for a future key compromise. Rotate keys and the underlying secrets when threat-modeling demands it.
In short: kvlt is exactly as protective as your SSH private key is, which
is far better than "as protective as a text file in $HOME."
Sharing a vault with teammates uses age's multi-recipient model β no shared keys, each person decrypts with their own SSH private key. Walkthrough in docs/recipes.md.
kvlt is a CLI; nothing runs between invocations. Each command opens
the vault config, talks to the backend, and exits.
- πͺͺ Pick a vault by name β every verb takes a name (
dev,prod, β¦); the name resolves to a backend through.kvlt/vaults/<type>/<id>.yaml - π Default backend is
local(age + SSH keys) βkvlt putencrypts to one or more SSH public-key recipients via age; blobs land at.kvlt/secrets/local_encryption/<vault>/<key>.age. Decrypt requires the matching SSH private key β passphrase prompt fires on/dev/ttyif your key isn't in ssh-agent already. - π Backends are pluggable β
Providerinterface + factory registry. Adding AWS Secrets Manager is one new file behind a//go:build awsguard; the base binary stays dependency-light. - π
migrateis copy-then-swap β list keys, copy each value to the new backend, write the new config, delete the old one. Source stays functional until the very last step. (Planned; the backend abstraction supports it cleanly.)
The contract every backend implements is four methods (Get / Put /
List / Name) β small on purpose. Anything fancier is layered on top
by callers, not pushed into the backend.
- age β pure-Go, audited, SSH-key-friendly encryption. kvlt is a vault wrapper around it; the crypto is age's.
| Tool | Description |
|---|---|
| HashiCorp Vault | Full-featured secret-management platform |
| OpenBao | Open-source fork of Vault |
| 1Password CLI | If you already live in 1Password |
| pass | GPG-encrypted files, the Unix way |
kvlt is meant for the gap below "I need a Vault cluster" and above
"I have a .env file."
Shipped:
- πͺͺ Project scaffold + CLI tree
- π
localbackend (age + SSH-key recipients) - πͺ
vault create/secret put/secret get/secret list - π
kvlt env/kvlt runfor shell + child-process integration - π Pluggable backend registry (factory pattern)
Up next β only what earns its keep:
- π€ ssh-agent integration β friction-free decrypt; Touch ID via Secretive on macOS without re-prompting per read.
- π
vault migrateβ copy-then-swap, named-vault payoff: change backend type without touching call sites. - π AWS Secrets Manager backend (
-tags aws) β only if a real "dev local β prod cloud" use case shows up. Other tools (Azure Key Vault, 1Password, HashiCorp Vault) are intentionally not on the roadmap; if you live in those, use them directly.
- docs/recipes.md β
.envrc/direnv,kvlt run, GitLab + GitHub CI, Unix-pipe patterns, dotfiles, multi-vault setups - docs/architecture.md β provider interface, on-disk layout, backend internals, migration semantics
- docs/development.md β setup, testing, conventions
- docs/contributing.md β PR workflow
The MIT License.