
βββ βββ βββ βββ
βββ βββ βββ βββ
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.
β¨ Features
- π 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, never kvlt 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 β
Provider interface + factory registry. AWS Secrets Manager (planned) sits behind a //go:build aws guard 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 put keeps secrets out of shell history; bare kvlt put prompts with echo off.
- π Shell-friendly β
kvlt env vault for eval "$(β¦)" direnv integration; kvlt run vault -- cmd for scoped env injection like aws-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.
π¦ Install
curl -fsSL https://github.com/retr0h/kvlt/raw/main/install.sh | sh
Installs 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.
π¨ Build from source
git clone https://github.com/retr0h/kvlt.git
cd kvlt
go build -o kvlt .
install -m 755 kvlt ~/.local/bin/kvlt
Cloud backends opt in via build tags: go build -tags aws -o kvlt .
π Quick start
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.
π‘οΈ Why SSH keys (and why this is meaningfully more secure)
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 1h for 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.
.age blobs 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.
βοΈ How It Works
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 put encrypts 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/tty if your key isn't in ssh-agent already.
- π Backends are pluggable β
Provider interface + factory registry. Adding AWS Secrets Manager is one new file behind a //go:build aws guard; the base binary stays dependency-light.
- π
migrate is 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.
π‘ Inspiration
- age β pure-Go, audited, SSH-key-friendly encryption. kvlt is a vault wrapper around it; the crypto is age's.
π Alternatives
kvlt is meant for the gap below "I need a Vault cluster" and above
"I have a .env file."
πΊοΈ Roadmap
Shipped:
- πͺͺ Project scaffold + CLI tree
- π
local backend (age + SSH-key recipients)
- πͺ
vault create / secret put / secret get / secret list
- π
kvlt env / kvlt run for 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
- 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
π License
The MIT License.