I deleted every .env file on my laptop

Why I built Envy, a macOS app and CLI that keeps secrets in the Keychain and hands them to your commands based on the folder you're in.

Every project I've worked on has the same file sitting in its root: .env. Plaintext database URLs, Stripe keys, API tokens. It's in .gitignore, so it feels safe. It isn't really. It's just out of sight.

Count the copies on a normal machine. One in the main checkout. One in every git worktree. One in every backup. A slightly different one in each app of a monorepo, because apps/web and apps/api drifted months ago and nobody remembers which is right. Then there's the version someone sent you over chat when you joined.

None of that is a dramatic security failure. It's just a lot of copies of secrets in a lot of places, with nothing keeping them in sync and nothing stopping one from being committed.

So I built Envy.

The goal: change nothing about how I work

I didn't want a secrets manager I had to remember to use. Every tool I tried that asked me to wrap commands (some-tool run -- pnpm dev) lasted about a week before I forgot the prefix and went back to a .env file.

The rule I set was that the workflow stays exactly the same:

$ cd ~/code/orbit/apps/api
$ pnpm dev
envy ▸ api · development · 14 vars
> api@1.0.0 dev
> tsx watch src/server.ts

You cd into a folder and run the command you'd always run. The folder decides which variables the command gets. There's no file on disk.

How it actually works

The values live in a single macOS Keychain item. envy setup puts a folder of shims first on your PATH: pnpm, npm, node, python, cargo, docker and so on. Each shim is just a symlink to the Envy binary.

When you run pnpm dev, the shim runs first. It:

  1. Looks up the current folder in a path index that contains no values, only which folders belong to which project. Folders without a project never touch the Keychain.
  2. Resolves the variables for that project and environment.
  3. execs the real pnpm with those variables, in the same process, so signals, the TTY and exit codes behave exactly as before.

Shims being real files instead of shell aliases turned out to matter a lot. Aliases only work in your interactive shell. Files on PATH also catch commands started by turbo, pnpm --filter, #!/usr/bin/env node scripts, and the run button in your editor.

Fail open, except when it matters

A tool that sits in front of pnpm can't be fragile. If it breaks your dev server, you'll uninstall it the same afternoon. So almost every failure is soft: if the Keychain is locked or the vault can't be read, the shim prints one warning and runs your command untouched.

There's one deliberate exception. If you ask for an environment that doesn't exist, it fails loudly:

$ ENVY_ENV=prodution pnpm build
envy: “api” has no “prodution” environment (available: development, staging, production).

Silently falling back to development values when you clearly meant production is the dangerous kind of helpful. A typo should stop you, not quietly change what you deployed.

Production should feel different

Once secrets lived in one place, I could treat production as special. Protected environments print a red warning and ask for Touch ID before their values are injected or printed. There's a short grace window, so turbo's parallel tasks ask once instead of six times. Over SSH or in CI, where nobody can answer the prompt, those commands refuse instead of guessing.

It's a small amount of friction in exactly the right place. You notice you're about to touch production, and nothing else gets slower.

Monorepos and git were the real work

The basic version (folder in, variables out) was the easy part. Making it fit how people actually use git was most of the work:

  • Nested projects inherit. apps/web gets the repo root's values plus its own.
  • Worktrees pick up the main checkout's projects automatically, with optional per-worktree overrides for things like API_PORT.
  • Branch variables keep changes made on a feature branch on that branch. envy branch merge brings them in with a three-way merge once the branch lands.
  • A pre-commit hook blocks staged .env files and known secret values. It reports the key, file and line, never the value itself.

That last one is the safety net for the moment you're tired and run git add ..

Getting rid of the files

The part I care about most is the last step. envy init --scan turns a whole monorepo's .env* files into projects and environments. Then envy files --clean moves to the trash every file whose values are all safely in Envy.

envy init --scan
envy hooks install
envy files --clean

After that, ls -a in a project root is just the project.

What I learned

The hard part of developer tooling isn't the core idea. It's every other process that also runs your commands: editors, task runners, monorepo tools, git hooks, CI. A tool that works in your terminal but not from VS Code's debug button isn't finished.

Envy is MIT licensed, works on macOS (app and CLI) and Linux (CLI), and has no account, no server and no telemetry. If you try it, I'd love to hear where it breaks: github.com/armanghev/envy.