Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
envy
Encrypted secrets, zero friction.
Local-first secret management for teams who take security seriously. No SaaS. No internet. No plaintext — ever.
The Problem
Every project starts with a .env file. Every .env file eventually ends up somewhere it shouldn't.
- Committed to git — accidentally or by a junior dev following a tutorial
- Pasted in Slack — "hey, can you check this config?" becomes a security incident
- Left on disk — cloned repos, CI artifacts, and Docker image layers carry your secrets forever
- Shared as plaintext — emailed, screenshot, airdropped, or typed into a Google Doc
The tools meant to solve this — hosted vaults, secrets managers, SaaS platforms — trade one risk for another: now your secrets live on someone else's server, behind their authentication, subject to their breach.
There is no good reason for production secrets to ever exist in plaintext. Envy makes that guarantee practical.
Why Envy
🔐 Zero-Trust Storage
Secrets are encrypted with AES-256-GCM before they touch the database. The database itself is encrypted with SQLCipher. The master key lives exclusively in your OS Keychain — never written to any file, never exposed to the filesystem.
Stealing your ~/.envy/vault.db gets an attacker nothing without the OS credential entry. Stealing your OS credential entry gets them nothing without the encrypted database. Defense in depth, not defense by hope.
🧠 Memory-Safe Secret Injection
envy run -- your-app decrypts secrets in RAM and passes them to your process via std::process::Command::envs(). When the process exits, the memory is zeroed. Nothing is written to disk, shell history, or environment exports.
All secret values are wrapped in Rust's Zeroizing<T> — backing memory is overwritten to zero on drop, even if the program panics.
🌿 GitOps-Native
envy encrypt produces envy.enc — a single JSON file sealed with Argon2id + AES-256-GCM. It contains no key names, no values, no project identifiers. Commit it to your public repo. Post it on Twitter. It is pure ciphertext.
envy decrypt on any machine restores your vault from the artifact. Onboarding a new team member is a git pull and one passphrase.
🔬 Pre-Encrypt Audit Trail
envy diff shows exactly what will change before you seal — additions, deletions, and modifications — with diff(1) exit codes for CI/CD gating. Values are hidden by default. --reveal requires explicit opt-in with a stderr warning.
Know exactly what you're committing to the artifact before you commit it.
🏢 Multi-Team Progressive Disclosure
Seal development, staging, and production with separate passphrases. A developer with the dev key imports their environment; production is listed as skipped — not an error, not a prompt, not an alarm. Access follows the passphrase, not a permissions UI.
🤖 CI/CD Native, Zero Config
Set ENVY_PASSPHRASE_<ENV> in your pipeline's secret store. Envy detects it and goes fully headless — no TTY, no prompts, no code changes required. Works with GitHub Actions, GitLab CI, CircleCI, Jenkins, and any shell that supports environment variables.
SOC 2 / Compliance note: Envy eliminates the most common source of secret leakage — plaintext
.envfiles in version control, chat logs, and build artifacts. It does not replace a full secrets management platform for regulated workloads, but it is a substantial step toward auditability: every secret change is a vault write, every seal is a committedenvy.encdiff.
Quickstart
Step 1 — Install and initialise
# Homebrew (macOS & Linux)
# NPM (all platforms)
Windows, curl, or build-from-source? See the Installation section below.
Step 2 — Store secrets and run your app
# secrets injected into the child process, never written to disk
Step 3 — Seal and share with your team
# Preview what you're about to commit
# + API_KEY
# + DATABASE_URL
# 2 changes: 2 added, 0 removed, 0 modified
A teammate pulls the repo and runs envy decrypt. Done. No Slack messages, no shared spreadsheets, no plaintext ever leaving your encrypted vault.
Interactive TUI
Run envy without a subcommand from an interactive terminal to open the full-screen vault
workstation: a three-panel console showing your project tree, the selected environment's
masked secrets, and an inspector column with sync state and metadata — all without changing
the output contract of existing commands. Launching from a project directory focuses that
project's entry in the tree.
The TUI is workspace-scoped: the tree shows the projects whose envy.toml manifests sit
at or below the directory where you launched envy (full depth, .gitignore respected), and
every project carries its own artifact path and rotation threshold. Projects from the rest of
the vault stay out of the way — launch from your monorepo root or from a single project, and
only what you can reach gets shown.
The gradient banner opens expanded on terminals tall enough (32+ rows); otherwise — or after
pressing B — it collapses to a one-line identity strip with the current version and
workspace name, so the brand is present without eating screen space.
| Key | Panel | Action |
|---|---|---|
↑↓ / j k |
both | Move selection |
Tab |
both | Switch between Projects and Secrets |
Enter / → / ← |
tree | Expand/select or collapse |
Space |
secrets | Reveal selected value (re-masks on move) |
Y |
secrets | Copy value to clipboard (clears 30s after last copy) |
N / E / D |
secrets | Create, edit, or delete a secret |
F |
secrets | Filter keys (filter persists, shown in the title) |
S |
both | Seal project into envy.enc (preview confirmation) |
T / G |
tree | Status / diff against envy.enc |
Y |
tree | Import environment from envy.enc (confirms) |
R |
tree | Rotate environment passphrase in-place |
X |
tree | Delete selected project (exact-name confirmation) |
B |
both | Toggle banner (full gradient logo or compact strip) |
L / U |
both | Lock or unlock the vault |
: |
both | Command palette — every action, searchable |
? |
both | Full help (scrollable) |
Q / Ctrl+C |
both | Quit and restore terminal |
The bottom legend always shows the keys of the focused panel, so nothing has to be memorized. The inspector column on the right shows sync state (in sync / modified / never sealed), secret counts, stale secrets, and the artifact location for whatever is selected; it hides automatically on terminals narrower than 100 columns.
Color is functional: brand violet marks the focused panel, green means in sync, amber means
drift (needs re-seal), red marks errors and destructive confirmations. Set NO_COLOR=1 for
a plain presentation. The TUI never starts for subcommands such as envy run -- command;
when stdout is piped, bare envy prints help to stderr and exits successfully, keeping
scripts and CI safe.
The footer intentionally shows only primary navigation. Press ? for complete grouped help;
long project trees and project-picker results scroll while keeping the current selection visible.
Nested projects (monorepo / multi-project support)
Since v0.3.2, envy init works in subdirectories of existing envy projects. Each project gets its own UUID in the vault and its own envy.toml + envy.enc. Commands resolve the closest envy.toml automatically — running envy list from a child directory shows the child's secrets, not the parent's.
/monorepo/
envy.toml ← org-wide credentials
envy.enc
/project-a/
envy init ← project-specific credentials (different UUID)
envy.toml
envy.enc
/project-b/
envy init
envy.toml
envy.enc
Sync Status at a Glance
envy status tells you the state of every environment — no passphrase, no decryption.
$ envy status
+-------------+---------+------------------+----------------+-----------+
| Environment | Secrets | Last Modified | Status | Rotation |
+=======================================================================+
| development | 4 | 2 minutes ago | ⚠ Modified | ✓ Fresh |
| production | 3 | 3 days ago | ✓ In Sync | ⚠ 1 due |
| staging | 2 | 1 week ago | ✗ Never Sealed | ✓ Fresh |
+-------------+---------+------------------+----------------+-----------+
Artifact: ./envy.enc (last written: 3 days ago)
Sealed environments: production
See Modified? Run envy diff to see exactly what changed, then envy encrypt to seal.
CI/CD Integration
# .github/workflows/deploy.yml
- name: Decrypt secrets
env:
ENVY_PASSPHRASE_PRODUCTION: ${{ secrets.ENVY_PASSPHRASE_PRODUCTION }}
run: envy decrypt
# Gate on exact artifact state before deploying
- name: Assert no unsealed drift
env:
ENVY_PASSPHRASE_PRODUCTION: ${{ secrets.ENVY_PASSPHRASE_PRODUCTION }}
run: |
envy diff -e production # exit 1 if vault ≠ artifact
echo "✓ Artifact matches vault"
- name: Deploy
run: envy run -e production -- ./scripts/deploy.sh
The ENVY_PASSPHRASE_<ENV> env var is the only config change required. Your application code and deploy scripts are untouched.
How It Works
Local development:
envy.toml ~/.envy/vault.db OS Keyring
(project UUID) → (SQLCipher-encrypted DB) ← (32-byte master key)
AES-256-GCM per-secret
sync_markers (sealed_at per env)
Team sync via Git:
~/.envy/vault.db →[envy encrypt]→ envy.enc (Argon2id + AES-256-GCM)
│
git commit/push
│
←[envy decrypt]← envy.enc
The Two-Key Model
| Vault master key | Artifact passphrase | |
|---|---|---|
| Purpose | Encrypts secrets at rest in vault.db |
Encrypts envy.enc for sharing |
| Stored in | OS Keychain / Secret Service (never on disk) | Not stored — entered by user or ENVY_PASSPHRASE |
| Scope | Per machine, per user | Per team, per project |
| Format | 32 random bytes | Human-readable string |
These keys are entirely independent. Knowing the passphrase does not help with the vault. Copying the vault without the OS credential entry is useless.
Cryptography Stack
Passphrase (user input)
│
▼ Argon2id (64 MiB memory, 3 iterations, parallelism 4)
256-bit derived key
│
▼ AES-256-GCM (random 96-bit nonce per seal)
Ciphertext + 128-bit authentication tag
│
▼ base64ct (constant-time Base64)
envy.enc → git commit
Argon2id is the Password Hashing Competition winner (2015). Memory-hard and side-channel resistant — GPU-based brute-force against the passphrase requires 64 MiB of RAM per attempt.
AES-256-GCM provides authenticated encryption — any modification to the ciphertext is detected before a single byte of plaintext is returned. This is what makes Progressive Disclosure safe: a wrong passphrase fails authentication silently, it never returns garbage data.
Fresh nonce per seal — re-sealing the same secrets produces different ciphertext every time. Ciphertext comparison attacks are not possible.
The envy.enc Structure
Every envelope is self-describing — it carries its own KDF parameters. You can decrypt any envelope without external metadata or a version registry. The environments map is a BTreeMap so JSON keys are always alphabetically ordered, producing deterministic git diff output.
Memory Safety
Every secret value travels through the codebase in zeroize::Zeroizing<String>. When the container is dropped (on function return, scope exit, or panic), the backing memory is overwritten to zero by the OS. Secret values are never stored in a plain String.
| Command | Alias | Description |
|---|---|---|
envy init |
— | Create envy.toml, register project in vault |
envy set KEY=VALUE [-e ENV] [--stdin] |
— | Store or update a secret |
envy get KEY [-e ENV] |
— | Print a single decrypted value to stdout |
envy list [-e ENV] |
ls |
List all key names (values never printed by default) |
envy rm KEY [-e ENV] |
remove, unset |
Delete a secret |
envy run [-e ENV] -- CMD |
— | Inject secrets and run a child process |
envy migrate FILE [-e ENV] |
— | Import all KEY=VALUE pairs from a .env file |
envy encrypt [-e ENV] |
enc |
Seal vault into envy.enc (strict: passphrase must match an existing envelope — use envy rotate to change it) |
envy decrypt |
dec |
Unseal envy.enc and restore secrets |
envy export [-e ENV] [--format] |
— | Print all secrets to stdout (dotenv / JSON / shell) |
envy diff [-e ENV] [--reveal] |
df |
Compare vault against envy.enc before encrypting |
envy status |
st |
Show sync status dashboard, including a rotation reminder (no passphrase required) |
envy rotate [-e ENV] |
— | Re-seal an envelope with a new passphrase (verifies current first) |
envy scan [-e ENV] [--reveal] |
— | Scan the working tree for plaintext copies of vault secrets |
envy audit [-e ENV] [--limit N] |
au |
Show the local history of set/get/rm/run actions |
envy hooks install [--force] |
— | Install a pre-commit hook that blocks commits leaking a vault secret |
envy completions SHELL |
— | Print shell completion script to stdout |
Output Formats
Most read commands accept --format (or -f):
| Format | Description |
|---|---|
table |
Human-readable (default) |
json |
Machine-readable JSON |
dotenv |
KEY=value pairs |
shell |
export KEY='value' — safe for eval $(...) |
envy diff — the pre-encrypt review loop
# Table output (key names only, colored)
# With values (stderr warning emitted first)
# JSON for scripts — old_value/new_value absent without --reveal
Exit codes for envy diff: 0 = no differences, 1 = differences found, 2+ = error.
Shell Autocompletion
Legacy Migration
Multi-Environment with Separate Passphrases
# Smart Merge: each seal preserves the other envelopes untouched
&&
# Junior dev — has only the dev key
# ✓ development (4 secrets upserted)
# ⚠ production skipped — different passphrase or key
# exit code: 0 ← partial access is success
Rotating a passphrase
Use envy rotate as the safe path for key rotation. Unlike envy encrypt, it verifies the current passphrase against the existing envelope before accepting a new one — a typo can never silently change the envelope's passphrase.
# Passphrase for 'production': <old-pass>
# New passphrase for 'production': <new-pass>
# Confirm new passphrase: <new-pass>
# ✓ 'production' rotated. Passphrase changed.
# Previous passphrase can no longer decrypt this artifact.
In CI / headless mode, set both ENVY_PASSPHRASE_<ENV> and ENVY_PASSPHRASE_<ENV>_NEW:
ENVY_PASSPHRASE_PRODUCTION=old-pass \
ENVY_PASSPHRASE_PRODUCTION_NEW=new-pass \
The rotation is forward-only — the old passphrase can no longer decrypt the artifact, and any other envy.enc sealed with the old passphrase can never be decrypted. The team's responsibility is to distribute the new passphrase through a secure channel (1Password, password manager, secure Slack DM, etc.).
Strict envy encrypt — no silent key rotation
Since v0.3.1, envy encrypt is strict: the passphrase you provide must either match the existing envelope (re-seal) or be the first time you're creating the envelope. If neither condition holds, envy encrypt fails with:
error: passphrase input failed: passphrase does not match the existing envelope.
hint: use `envy rotate -e ENV` to change the envelope's passphrase.
Exit code 2. The artifact is left unchanged. Use envy rotate -e ENV to change the passphrase — envy encrypt will not do it for you.
Rotation reminder
envy status flags secrets that haven't been touched in a while — no decryption involved, just updated_at timestamps:
$ envy status
...
| production | 3 | 3 days ago | ✓ In Sync | ⚠ 1 due |
⚠ Rotation reminder (no changes in over 90 days):
production: LEGACY_API_KEY
The threshold defaults to 90 days and is configurable per project in envy.toml:
= 30
Vault-leak scanner
envy scan walks the working tree looking for exact occurrences of secret values already stored in the vault — not a generic regex-based secrets scanner, so false positives are essentially zero: a hit means a value you're already managing with envy was also pasted in plaintext somewhere in the repo (.env files included — dotfiles are scanned on purpose).
Respects .gitignore. Exit codes follow the diff(1) convention (0 clean, 1 found, 2+ error), so it composes with CI or envy hooks install below.
Local audit trail
envy audit lists the local history of set/get/rm/run actions — key names and timestamps only, never values:
Sync/crypto actions (encrypt/decrypt/rotate) aren't recorded here; that history already lives in envy.enc's git log and envy status.
Pre-commit hook
envy hooks install writes a pre-commit hook that runs envy scan on every commit — attempting to commit a leaked secret is blocked, not just logged:
# envy: blocked -- a vault secret's plaintext value was found in a file
# you're about to commit. Run 'envy scan --reveal' for details.
It also prints a non-blocking warning when envy status shows unsealed drift. Nothing leaves the machine. A pre-existing hook envy didn't install is never overwritten without --force, and even then the previous file is backed up first.
Rotation reminder
envy status flags secrets that haven't been touched in over rotation_reminder_days days (default 90, configurable in envy.toml):
# envy.toml
= 120
When any secret exceeds the threshold, a ⚠ Rotation reminder section lists the affected key names — values are never shown. Set rotation_reminder_days = 0 to disable the reminder entirely. This is read-only and never decrypts anything; it simply compares updated_at timestamps against the current wall-clock time.
Passphrase strength hint
When you type a passphrase by hand during interactive envy encrypt or envy rotate prompts, a non-blocking strength estimate is printed:
ℹ passphrase strength: weak (~36 bits estimated)
hint: press Enter on an empty prompt next time to accept envy's suggested Diceware phrase.
This is purely informational — a low score never blocks or rejects a passphrase. Security rests on Argon2id + AES-256-GCM, not on this heuristic. The hint nudges you toward envy's built-in Diceware passphrase generator (press Enter on an empty prompt).
Documentation
- Per-command reference — one page per command: what it does, syntax, exit codes, and related commands
- Examples — copy-pasteable, CI-verified workflows (basic, team-sync, CI/CD, monorepo)
- Developer guide — architecture, module map, and contribution notes
- Demo videos — terminal walkthroughs of quickstart, team sync, CI/CD, and the interactive TUI (generated with VHS)
| Code | Meaning |
|---|---|
0 |
Success; partial decrypt (≥ 1 env imported); envy diff/envy scan — no differences/leaks found |
1 |
Not found (manifest, secret, envy.enc, .git); zero envs imported; envy diff — differences found; envy scan — leak(s) found |
2 |
Invalid input (key name, assignment format, empty or wrong passphrase) |
3 |
Initialisation conflict; environment not found in vault or artifact; envy hooks install — conflict |
4 |
Vault or crypto failure |
5 |
envy.enc unreadable (malformed JSON or unsupported schema version) |
127 |
Child binary not found (envy run) |
N |
Child process exit code (proxied exactly by envy run) |
Note: envy diff and envy scan follow the diff(1) convention — exit 1 means "differences/leaks exist", not "an error occurred". This makes it safe to use in shell pipelines with || without masking real errors.
Installation
Homebrew (macOS & Linux)
NPM (Cross-platform wrapper)
# or run without installing:
Cargo (Rust toolchain)
# installs the `envy` command
macOS & Linux (shell installer)
|
Windows (PowerShell)
irm https://github.com/anguriatech/envy/releases/latest/download/envy-installer.ps1 | iex
Build from source (requires Rust 1.85+)
&&
Roadmap
Envy has completed Phase 1 (encrypted local vault), Phase 2 (GitOps sync & CI/CD), Phase 2.x (multi-env encrypt, output formats, sync status, pre-encrypt diff), and Phase 2.y (rotation reminders, passphrase strength hints, local audit trail, vault-leak scanner, pre-commit hook).
Phase 3 — Ecosystem & GUI: An official VS Code Extension to make secret management visual and seamless, without leaving the editor.
Built with Rust, SQLCipher, AES-256-GCM, and Argon2id by Anguria Tech. MIT License — audit the code, fork it, ship it.