Local-first secret management for teams who take security seriously. No SaaS. No internet. No plaintext β ever.
brew install anguriatech/tap/envynpm install -g @anguriatech/envyEvery 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.
|
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 |
All secret values are wrapped in Rust's |
|
|
Know exactly what you're committing to the artifact before you commit it. |
|
Seal |
Set |
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.
Step 1 β Install and initialise
# Homebrew (macOS & Linux)
brew install anguriatech/tap/envy
# NPM (all platforms)
npm install -g @anguriatech/envyWindows, curl, or build-from-source? See the Installation section below.
cd my-project
envy init # creates envy.toml (safe to commit)Step 2 β Store secrets and run your app
envy set DATABASE_URL=postgres://localhost/myapp
envy set API_KEY=sk_live_abc123
envy run -- npm run dev
# secrets injected into the child process, never written to diskStep 3 β Seal and share with your team
# Preview what you're about to commit
envy diff
# + API_KEY
# + DATABASE_URL
# 2 changes: 2 added, 0 removed, 0 modified
envy encrypt # prompts for a passphrase (or set ENVY_PASSPHRASE in CI)
git add envy.enc envy.toml
git commit -m "chore: add encrypted secrets"
git pushA teammate pulls the repo and runs envy decrypt. Done. No Slack messages, no shared spreadsheets, no plaintext ever leaving your encrypted vault.
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
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.
# .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.shThe ENVY_PASSPHRASE_<ENV> env var is the only config change required. Your application code and deploy scripts are untouched.
π Architecture & Cryptography
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
| 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.
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.
{
"version": 1,
"environments": {
"development": {
"ciphertext": "<base64>",
"nonce": "<base64>",
"kdf": {
"algorithm": "argon2id",
"memory_kib": 65536,
"time_cost": 3,
"parallelism": 4,
"salt": "<base64>"
}
}
}
}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.
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.
π Full Command Reference
| 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 |
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 $(...) |
# Table output (key names only, colored)
envy diff [-e ENV]
# With values (stderr warning emitted first)
envy diff [-e ENV] --reveal
# JSON for scripts β old_value/new_value absent without --reveal
envy diff [-e ENV] --format jsonExit codes for envy diff: 0 = no differences, 1 = differences found, 2+ = error.
envy completions bash >> ~/.bash_completion
envy completions zsh > ~/.zfunc/_envy # then: autoload -Uz compinit && compinit
envy completions fish > ~/.config/fish/completions/envy.fish
envy completions powershell >> $PROFILEenvy migrate .env # import development secrets
envy migrate .env.staging -e staging
envy list # verify
rm .env .env.staging
echo '.env*' >> .gitignoreenvy enc -e development # dev passphrase
envy enc -e staging # staging passphrase
envy enc -e production # prod passphrase (restricted)
# Smart Merge: each seal preserves the other envelopes untouched
git add envy.enc && git commit -m "chore: rotate secrets"# Junior dev β has only the dev key
envy decrypt
# β development (4 secrets upserted)
# β production skipped β different passphrase or key
# exit code: 0 β partial access is successUse 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.
envy rotate -e production
# 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 \
envy rotate -e productionThe 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.).
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.
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:
rotation_reminder_days = 30envy 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).
envy scan # masked output, exit 1 if anything is found
envy scan --reveal # show the matched value (stderr warning first)
envy scan --format json # for CIRespects .gitignore. Exit codes follow the diff(1) convention (0 clean, 1 found, 2+ error), so it composes with CI or envy hooks install below.
envy audit lists the local history of set/get/rm/run actions β key names and timestamps only, never values:
envy audit # newest first, all environments
envy audit -e production # filter to one environmentSync/crypto actions (encrypt/decrypt/rotate) aren't recorded here; that history already lives in envy.enc's git log and envy status.
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 hooks install
git commit -m "..."
# 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.
envy status flags secrets that haven't been touched in over rotation_reminder_days days (default 90, configurable in envy.toml):
# envy.toml
rotation_reminder_days = 120When 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.
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).
- 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, and CI/CD (generated in CI)
π’ Exit Codes
| 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.
Homebrew (macOS & Linux)
brew install anguriatech/tap/envyNPM (Cross-platform wrapper)
npm install -g @anguriatech/envy
# or run without installing:
npx @anguriatech/envymacOS & Linux (shell installer)
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/anguriatech/envy/releases/latest/download/envy-installer.sh | shWindows (PowerShell)
irm https://github.com/anguriatech/envy/releases/latest/download/envy-installer.ps1 | iexBuild from source (requires Rust 1.85+)
git clone https://github.com/anguriatech/envy.git
cd envy && cargo install --path .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.
