hushenv
v0.4.0
Published
Local secret manager for the agent era - encrypted vault, {hushenv.X} refs in .env, plaintext only at runtime.
Downloads
948
Maintainers
Readme
hushenv
Keeps your secrets hush-hush 🤫 — a local secret manager for the agent era, starting with your .env.
Your .env files hold references, not secrets:
NEXTAUTH_URL=http://localhost:3000
DATABASE_URL=postgres://app:{hushenv.DB_PASSWORD}@localhost:5432/app
RESEND_KEY={hushenv.RESEND_KEY}Real values live AES-256-GCM-encrypted in ~/.hushenv/vault.json, with the
master key in your OS keychain (macOS Keychain / Windows Credential Manager).
Plaintext exists in exactly one place: the child process environment, in
memory, at hushenv run time. The .env file is safe to commit and safe for
AI agents to read.
Quickstart
npm i -g hushenv
hushenv init # master key -> OS keychain, empty vault
hushenv set DB_PASSWORD # hidden prompt
hushenv set RESEND_KEY --stdin # or pipe it in
hushenv run -- pnpm dev # resolves refs from ./.env, injects, runs
hushenv run -f .env.local -- pnpm devMigrating an existing .env
Already have a populated .env? import moves the real values into the vault
and rewrites the file to refs, in one step:
hushenv import --dry-run # preview: what gets vaulted vs left literal
hushenv import # interactive: pick which values to vault
hushenv import --all # non-interactive: take the heuristicsIt vaults values that look like secrets and leaves config (e.g.
NEXTAUTH_URL=http://localhost:3000) literal — review the picks in the
interactive prompt or with --dry-run. Re-running is a safe no-op (refs are
skipped). Two things to know:
- It vaults whole values. A secret embedded in a larger string (e.g. the
password inside a
DATABASE_URL) is vaulted whole or skipped — split those by hand into their own{hushenv.X}ref. - If the
.envwas ever committed, the old plaintext is in your git history — rotate those secrets after importing.
Commands
| Command | What it does |
|---|---|
| hushenv init | Create the master key (keychain) and an empty vault |
| hushenv set <name> [value] | Store a secret. Hidden prompt by default; --stdin to pipe. Positional value is discouraged (shell history). |
| hushenv get <name> | Reveal a value. Requires an interactive TTY + confirmation; --force to bypass (use sparingly). |
| hushenv ls | List names and update dates. Never values. |
| hushenv rm <name> | Delete a secret |
| hushenv mv <old> <new> | Rename a secret (alias: rename). Re-encrypted under the new name; --force to overwrite. |
| hushenv import [-f file] | Migrate plaintext values from an .env into the vault, rewriting them to refs. --dry-run, --all, --force/--skip-existing, --prefix. |
| hushenv run [-f file]... -- <cmd> | Resolve refs and run the command with secrets injected |
| hushenv log [--verify] | Show the append-only audit log (names/metadata, never values); --verify checks the tamper-evident chain. --tail <n>, --json. |
Reference syntax: {hushenv.NAME} — whole-value or embedded inside a larger
string. {mysm.NAME} and {mysmtool.NAME} are accepted as legacy aliases.
Use with your stack
hushenv run injects resolved secrets into the environment of any child
process — your app's language doesn't matter:
hushenv run -- <your dev command>Per-stack recipes with the framework-specific details:
| Stack | Guide | |---|---| | Next.js | docs/nextjs.md | | NestJS | docs/nestjs.md | | Express / plain Node | docs/express.md | | Vite | docs/vite.md | | Python (FastAPI / Django / Flask) | docs/python.md | | Go | docs/go.md | | PHP / Laravel | docs/php-laravel.md | | Ruby / Rails | docs/ruby-rails.md | | Rust | docs/rust.md |
They all work for the same reason: every mainstream dotenv loader (Node
dotenv, python-dotenv, godotenv, phpdotenv, dotenv-rails, dotenvy) refuses to
overwrite environment variables that already exist — and hushenv sets the real
values before your app starts. Your framework keeps its .env loading;
the ref strings in the file are simply never used.
Semantics
- Multiple
-ffiles: loaded in order, the first occurrence of a key wins. - Existing
process.envvariables are not overridden by file values (dotenv convention). - Missing refs fail fast before your app starts, with the exact
hushenv setcommands to fix it. Exit code 2. - Exit codes:
0ok ·1error ·2missing secret.
How it compares
| | Plaintext on disk | .env safe to commit | Agent-safe .env | Works offline | Price |
|---|---|---|---|---|---|
| plain .env + dotenv | yes 😬 | no | no | yes | free |
| hushenv | no — AES-256-GCM vault, key in OS keychain | yes — refs only | yes | yes | free |
| dotenvx | no — ciphertext in .env | yes — ciphertext | partly — private key sits in .env.keys | yes | free core, paid sync |
| 1Password op run | no | yes — op:// refs | yes | mostly | subscription |
| cloud secret managers | no | n/a | yes | no | usage-based |
Honest take: if your team needs shared secrets today, dotenvx sync or a cloud manager solves that and hushenv doesn't yet — team sync is what hushenv Cloud (closed-source, paid) will add on top of this free core.
No keychain? (CI, containers)
Set the master key via the environment instead:
export HUSHENV_MASTER_KEY="<32 bytes, base64>"hushenv init prints exactly this fallback if no OS keychain is reachable.
Security model — honest version
What this protects against:
- Agents or tools reading secrets from
.envfiles (they only see refs) - Secrets leaking into git history
- Vault file theft without the OS keychain (ciphertext only)
- Ciphertext swapping between entries (AES-GCM AAD binds value to name)
What it does not protect against:
hushenv run -- envprints that project's resolved secrets — scoping arrives with per-project grants in v1hushenv get --forcefrom a non-interactive shell — pair it with an agent deny rule, e.g.Bash(hushenv get*)in Claude Code settings- Malware running as your user with an unlocked keychain — same limit as any local secret manager
- Deletion or truncation of the audit log by anything with filesystem access — see below
Audit log
Every action (init/set/get/rm/mv/import/run) is appended to
~/.hushenv/audit.log — names and metadata only, never secret values.
hushenv log # recent activity (never values)
hushenv log --verify # check the tamper-evident chainEach entry is chained to the previous one with an HMAC-SHA256 keyed by your master key, so entries can't be forged or edited without the key (which lives in your OS keychain). Honest limit: the chain proves contents weren't altered, but it can't stop something with filesystem access from deleting or truncating the log — the same "malware running as you" boundary above. Real deletion-resistance needs an off-box anchor (a trusted external timestamp or remote append-only collection); that belongs to hushenv Cloud, not the offline core.
Repo layout
packages/vault-core engine: crypto, keychain, vault storage (name-agnostic)
apps/cli the hushenv CLI: ref resolution, run, promptsvault-core never imports anything env-file- or CLI-specific. Future
surfaces (tray UI, MCP broker) sit on the same engine.
Contributing
PRs welcome — it's a small, readable codebase. Setup, tests, and how releases work are in CONTRIBUTING.md. Contributions are licensed under Apache-2.0.
Roadmap
v1: per-project grants with strict mode (import and the append-only audit log
have shipped). v2: secret versions, rotation warnings, tray UI.
