<div align="center">
# envy
### Encrypted secrets, zero friction.
**Local-first secret management for teams who take security seriously.**
No SaaS. No internet. No plaintext — ever.
[](https://github.com/anguriatech/envy/actions/workflows/ci.yml)
[](https://github.com/anguriatech/envy/releases/latest)
[](LICENSE)
```bash
brew install anguriatech/tap/envy
```
```bash
npm install -g @anguriatech/envy
```
```bash
cargo install envy-secrets
```
</div>
---
<div align="center">
<img src="docs/assets/demo.gif" alt="Envy demo" width="100%" />
</div>
---
## 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
<table>
<tr>
<td width="50%" valign="top">
### 🔐 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.**
</td>
<td width="50%" valign="top">
### 🧠 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.
</td>
</tr>
<tr>
<td width="50%" valign="top">
### 🌿 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.
</td>
<td width="50%" valign="top">
### 🔬 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.
</td>
</tr>
<tr>
<td width="50%" valign="top">
### 🏢 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.
</td>
<td width="50%" valign="top">
### 🤖 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.
</td>
</tr>
</table>
> **SOC 2 / Compliance note**: Envy eliminates the most common source of secret leakage — plaintext `.env` files 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 committed `envy.enc` diff.
---
## Quickstart
**Step 1 — Install and initialise**
```bash
# Homebrew (macOS & Linux)
brew install anguriatech/tap/envy
# NPM (all platforms)
npm install -g @anguriatech/envy
```
> Windows, curl, or build-from-source? See the [Installation](#installation) section below.
```bash
cd my-project
envy init # creates envy.toml (safe to commit)
```
**Step 2 — Store secrets and run your app**
```bash
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 disk
```
**Step 3 — Seal and share with your team**
```bash
# 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 push
```
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
<div align="center">
<img src="docs/assets/tui-demo.gif" alt="Envy TUI demo" width="100%" />
</div>
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.
| `↑↓` / `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
+-------------+---------+------------------+----------------+-----------+
| 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
```yaml
# .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.
---
<details>
<summary><strong>📐 Architecture & Cryptography</strong></summary>
## 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
```json
{
"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`.
</details>
---
<details>
<summary><strong>📋 Full Command Reference</strong></summary>
| [`envy init`](docs/commands/envy-init.md) | — | Create `envy.toml`, register project in vault |
| [`envy set KEY=VALUE [-e ENV] [--stdin]`](docs/commands/envy-set.md) | — | Store or update a secret |
| [`envy get KEY [-e ENV]`](docs/commands/envy-get.md) | — | Print a single decrypted value to stdout |
| [`envy list [-e ENV]`](docs/commands/envy-list.md) | `ls` | List all key names (values never printed by default) |
| [`envy rm KEY [-e ENV]`](docs/commands/envy-rm.md) | `remove`, `unset` | Delete a secret |
| [`envy run [-e ENV] -- CMD`](docs/commands/envy-run.md) | — | Inject secrets and run a child process |
| [`envy migrate FILE [-e ENV]`](docs/commands/envy-migrate.md) | — | Import all `KEY=VALUE` pairs from a `.env` file |
| [`envy encrypt [-e ENV]`](docs/commands/envy-encrypt.md) | `enc` | Seal vault into `envy.enc` (strict: passphrase must match an existing envelope — use `envy rotate` to change it) |
| [`envy decrypt`](docs/commands/envy-decrypt.md) | `dec` | Unseal `envy.enc` and restore secrets |
| [`envy export [-e ENV] [--format]`](docs/commands/envy-export.md) | — | Print all secrets to stdout (dotenv / JSON / shell) |
| [`envy diff [-e ENV] [--reveal]`](docs/commands/envy-diff.md) | `df` | Compare vault against `envy.enc` before encrypting |
| [`envy status`](docs/commands/envy-status.md) | `st` | Show sync status dashboard, including a rotation reminder (no passphrase required) |
| [`envy rotate [-e ENV]`](docs/commands/envy-rotate.md) | — | Re-seal an envelope with a new passphrase (verifies current first) |
| [`envy scan [-e ENV] [--reveal]`](docs/commands/envy-scan.md) | — | Scan the working tree for plaintext copies of vault secrets |
| [`envy audit [-e ENV] [--limit N]`](docs/commands/envy-audit.md) | `au` | Show the local history of `set`/`get`/`rm`/`run` actions |
| [`envy hooks install [--force]`](docs/commands/envy-hooks.md) | — | Install a pre-commit hook that blocks commits leaking a vault secret |
| [`envy completions SHELL`](docs/commands/envy-completions.md) | — | Print shell completion script to stdout |
### Output Formats
Most read commands accept `--format` (or `-f`):
| `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
```bash
# 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 json
```
**Exit codes for `envy diff`**: `0` = no differences, `1` = differences found, `2+` = error.
### Shell Autocompletion
```bash
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 >> $PROFILE
```
### Legacy Migration
```bash
envy migrate .env # import development secrets
envy migrate .env.staging -e staging
envy list # verify
rm .env .env.staging
echo '.env*' >> .gitignore
```
### Multi-Environment with Separate Passphrases
```bash
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"
```
```bash
# 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 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.
```bash
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`:
```bash
ENVY_PASSPHRASE_PRODUCTION=old-pass \
ENVY_PASSPHRASE_PRODUCTION_NEW=new-pass \
envy rotate -e production
```
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
...
⚠ 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`:
```toml
rotation_reminder_days = 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).
```bash
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 CI
```
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:
```bash
envy audit # newest first, all environments
envy audit -e production # filter to one environment
```
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:
```bash
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.
### Rotation reminder
`envy status` flags secrets that haven't been touched in over `rotation_reminder_days` days (default 90, configurable in `envy.toml`):
```toml
# envy.toml
rotation_reminder_days = 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).
</details>
---
## Documentation
- [Per-command reference](docs/commands/) — one page per command: what it does, syntax, exit codes, and related commands
- [Examples](examples/) — copy-pasteable, CI-verified workflows (basic, team-sync, CI/CD, monorepo)
- [Developer guide](docs/developer-guide.md) — architecture, module map, and contribution notes
- [Demo videos](docs/assets/) — terminal walkthroughs of quickstart, team sync, CI/CD, and the interactive TUI (generated with VHS)
---
<details>
<summary><strong>🔢 Exit Codes</strong></summary>
| `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.
</details>
---
## Installation
**Homebrew (macOS & Linux)**
```bash
brew install anguriatech/tap/envy
```
**NPM (Cross-platform wrapper)**
```bash
npm install -g @anguriatech/envy
# or run without installing:
npx @anguriatech/envy
```
**Cargo (Rust toolchain)**
```bash
cargo install envy-secrets
# installs the `envy` command
```
**macOS & Linux (shell installer)**
```bash
**Windows (PowerShell)**
```powershell
**Build from source** (requires Rust 1.85+)
```bash
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), **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.
---
<div align="center">
Built with Rust, SQLCipher, AES-256-GCM, and Argon2id by [Anguria Tech](https://github.com/anguriatech).
MIT License — audit the code, fork it, ship it.
</div>