@anguriatech/envy
v0.3.2
Published
A secure, deterministic, and frictionless environment variable manager
Maintainers
Readme
envy
Encrypted secrets, zero friction.
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/envyThe 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)
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.
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.encSync Status at a Glance
envy status tells you the state of every environment — no passphrase, no decryption.
$ envy status
+-------------+---------+------------------+----------------+
| Environment | Secrets | Last Modified | Status |
+=============================================================+
| development | 4 | 2 minutes ago | ⚠ Modified |
| production | 3 | 3 days ago | ✓ In Sync |
| staging | 2 | 1 week ago | ✗ Never Sealed |
+-------------+---------+------------------+----------------+
Artifact: ./envy.enc (last written: 3 days ago)
Sealed environments: productionSee 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.shThe 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.encThe 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 commitArgon2id 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
{
"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.
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 (no passphrase required) |
| envy rotate [-e ENV] | — | Re-seal an envelope with a new passphrase (verifies current first) |
| 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)
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.
Shell Autocompletion
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 >> $PROFILELegacy Migration
envy migrate .env # import development secrets
envy migrate .env.staging -e staging
envy list # verify
rm .env .env.staging
echo '.env*' >> .gitignoreMulti-Environment with Separate Passphrases
envy 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 successRotating 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.
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.).
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.
| Code | Meaning |
|------|---------|
| 0 | Success; partial decrypt (≥ 1 env imported); envy diff — no differences |
| 1 | Not found (manifest, secret, envy.enc); zero envs imported; envy diff — differences found |
| 2 | Invalid input (key name, assignment format, empty or wrong passphrase) |
| 3 | Initialisation conflict; environment not found in vault or artifact |
| 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 follows the diff(1) convention — exit 1 means "differences exist", not "an error occurred". This makes it safe to use in shell pipelines with || without masking real errors.
Installation
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 .Roadmap
Envy has completed Phase 1 (encrypted local vault), Phase 2 (GitOps sync & CI/CD), and Phase 2.x (multi-env encrypt, output formats, sync status, pre-encrypt diff).
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.
