We still send .env files over chat
Here’s how secrets actually get shared on most projects I’ve seen, including mine: someone zips up their .env, drops it in a DM, and the other person pastes it into their editor. Slack, WhatsApp, Discord, email — it doesn’t matter which. The file lands in a chat log that gets backed up forever, on a server neither of you controls.
Everything downstream of that is broken too. The copy goes stale the moment anyone adds a key, and you find out when the app crashes. Nobody knows who has which version. When someone leaves the project, there’s no revocation — there’s just a shared understanding that they still have the production database URL in their downloads folder. And the .env.example we all dutifully commit documents the shape of the config while telling you nothing about the values, so the new person’s first task is a scavenger hunt through Slack history.
This is a solved problem. That’s what bothers me. The solutions exist — they just each ask you to give up something that, for a small project, costs more than the problem does.
The SaaS answer is genuinely good
Doppler, Infisical, and 1Password all landed on the same model, and it’s the right one: don’t put the secrets on disk at all. You prefix your command and the tool injects the values straight into the process:
doppler run -- npm start Your app sees the environment variables. There’s no file to leak, no file to stale out, no file to accidentally commit. Rotate a value in the dashboard and the next run picks it up. You get an audit log, real revocation, and versioning. On the security axis, this beats anything file-based, including what I built.
The cost is everything around it. You need an account, and so does everyone else on the project. Your secrets live on somebody else’s servers, which is fine right up until it’s a decision you have to defend. And it’s per-seat: Doppler’s team tier runs about $7 per user per month, Infisical’s paid tier more. For a company that’s a rounding error. For two students on a side project it’s a subscription, a signup flow, and a conversation.
But my real objection is the wrapping itself, and this is a preference more than an argument. Putting a process in front of every command has a cost you pay constantly. Each run does a network round-trip before your app starts, so npm run dev gets slower and booting offline stops working. There’s an extra process in the tree, which matters more than you’d think once containers and signal handling are involved.
It also quietly breaks things, because plenty of tooling reads the file, not the environment. Docker Compose’s env_file: wants a path. Prisma looks for .env. Your editor’s database explorer and REST client are launched by your editor, not by the wrapper, so they see nothing. Each of these has a workaround, and the workarounds are the tax.
Deployment is where it gets properly annoying. Now the CLI has to exist inside your production image, with a service token, with network egress to the provider at boot — a new runtime dependency and a new way for a deploy to fail at 2am. Meanwhile Vercel, Railway, and Fly all have their own environment variable UIs, so you end up running two systems and reconciling them by hand.
And underneath all of that: I genuinely like having a .env file. I can cat it and know exactly what my app sees. It works on a plane. Every framework in every language already understands it, with no adapter and no wrapper. It’s one of the few truly universal interfaces we have, and I didn’t want a secrets tool to be the reason I gave it up.
The git-native answer has the right shape
The other family keeps secrets in the repo, encrypted. SOPS, git-crypt, dotenvx. I like this model much better for small projects: the secrets are versioned alongside the code that needs them, they arrive with git clone, code review shows you when a value changed, and there’s no service to be down.
I tried to adopt SOPS and bounced off it. It’s a good tool with a real user base, but the standard path runs through GPG, and I never got the GPG approach working. Key generation, trust, agents, expiry — I lost an afternoon and gave up. The cloud KMS backends are fine if you already live in AWS, which is another way of saying they solve it for people who already solved it.
dotenvx is closer to what I wanted and comes from the person who wrote dotenv itself, so the instincts are right. My hesitations were practical: it grew out of Node, and I don’t love needing a JavaScript runtime in a container just to read a config value. The team features sit behind a paywall.
So: the SaaS tools want an account, the KMS options want a cloud, and the friendly git-native option wants a runtime. None of these are unreasonable asks. They were just each bigger than the thing I was trying to fix.
What I actually wanted
One test, and I think it’s the only one that matters: the second person clones the repo and runs the app. Not “the second person creates an account, gets invited to an org, installs a runtime, generates a keypair, and exchanges it with me.” Clone, one command, running.
Which turns out to be mostly a key-distribution problem, and key distribution is the part everyone makes hard. Except GitHub already solved it and nobody uses it: every GitHub user’s public SSH keys are served, publicly, at github.com/<username>.keys. If someone can push to the repo, they almost certainly have a key there already.
So that’s the whole idea behind hush:
hush add gh:alice That fetches Alice’s public keys, adds her as a recipient, and commits an encrypted .env.enc that lives in the repo like any other file. She clones, runs hush sync, and has a working .env. No account, no invite, no key exchange, no “can you send me your public key” thread. It’s one Rust binary, so there’s no runtime to install, and it’s built on age so I’m not the one who wrote the encryption. Removing someone is deleting a line and pushing.
What it doesn’t do, on purpose
Being honest about my own tradeoff, since I just spent several paragraphs on everyone else’s: after hush sync, a plaintext .env sits on your disk, exactly like it does today. Any process on your machine can read it — including, increasingly, coding agents with shell access. The wrapping tools genuinely beat me on that, and I’m not going to pretend the file is secure because I like it.
What I’ll claim is that it’s the same exposure you already have, with the sharing problem fixed on top. Your .env is on disk right now. The difference is where the copy came from, whether it’s current, and whether you can take it away from someone. If disk exposure is your threat model, use Doppler — I mean that.
An exec command that keeps values off disk is on the roadmap, for people who want it or for CI. But it’ll be an option, not the destination. The file isn’t a limitation I’m working around; it’s the thing I wanted to keep.
It’s new, so the disclaimer from the README applies: get a second pair of eyes on it before you trust it with production secrets. If you want to be that second pair of eyes, it’s at github.com/mercho40/hush.