We still send .env files over chat
Here’s how secrets 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. It doesn’t matter whether that’s Slack, WhatsApp, Discord, or email. 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, you can’t revoke their access. Everyone just accepts that they still have the production database URL in their downloads folder. And the .env.example we all commit lists the keys but none of the values, so the new person’s first task is searching Slack history for them.
This is a solved problem, and that’s what bothers me. The solutions exist, but each one asks you to give up something that costs a small project more than the problem does.
The SaaS answer is good
Doppler, Infisical, and 1Password all landed on the same model, and it’s the right one. The secrets never go on disk. You prefix your command, and the tool injects the values straight into the process:
doppler run -- npm start Your app sees the environment variables, and there’s no file that can leak, go stale, or get committed by accident. Rotate a value in the dashboard and the next run picks it up. You get an audit log, real revocation, and versioning. On security, 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 you pay per seat. Doppler’s team tier costs about $7 per user per month, and Infisical’s paid tier costs more. For a company that’s a rounding error. For two students on a side project, it’s a monthly bill someone has to agree to pay.
But my real objection is the wrapping itself, and this is a preference more than an argument. A process in front of every command costs you something on every run. Each run does a network round-trip before your app starts, so npm run dev gets slower and booting offline stops working. There’s also an extra process in the tree, which matters more than you’d think once you add containers and signal handling.
The wrapper also breaks tools that read the file instead of the environment, and plenty do. Docker Compose’s env_file: wants a path. Prisma looks for .env. Your editor launches its own database explorer and REST client outside the wrapper, so they see nothing. Each of these has a workaround, and every workaround is one more thing to set up.
Deployment is the most annoying part. Your production image now needs the CLI, a service token, and network access to the provider at boot. That’s 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.
Underneath all of that, I 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 reads it, with no adapter and no wrapper. Few interfaces are that universal, and I didn’t want a secrets tool to make me give it up.
The git-native answer fits better
The other family, which includes SOPS, git-crypt, and dotenvx, keeps secrets in the repo, encrypted. I like this model much better for small projects. The secrets get versioned with the code that needs them, they arrive with git clone, code review shows you when a value changed, and there’s no service that can go down.
I tried to adopt SOPS and couldn’t. It’s a good tool with a real user base, but the standard path runs through GPG, and I never got GPG working. I lost an afternoon to key generation, trust, agents, and expiry, then gave up. The cloud KMS backends are fine if you’re already on AWS, but that means they only help people who already have key management set up.
dotenvx is closer to what I wanted, and it comes from the person who wrote dotenv. My objections 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 are also 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 those asks is unreasonable, but each one was bigger than the problem I was trying to fix.
What I wanted
I had one test, and I think it’s the only one that matters: the second person clones the repo and runs the app. They shouldn’t have to create an account, get invited to an org, install a runtime, generate a keypair, and exchange it with me. They clone, run one command, and the app starts.
That turns out to be mostly a key-distribution problem, and key distribution is the part everyone makes hard. GitHub already solved it, and few tools use the solution. GitHub serves every user’s public SSH keys at github.com/<username>.keys. If someone can push to the repo, they almost certainly have a key there already.
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. Nobody creates an account, sends an invite, or starts a “can you send me your public key” thread. It’s one Rust binary, so there’s no runtime to install. It’s built on age, so I didn’t write the encryption myself. To remove someone, you delete a line and push.
What it doesn’t do, on purpose
I spent several paragraphs on everyone else’s tradeoffs, so here’s mine. After hush sync, a plaintext .env sits on your disk, exactly like it does today. Any process on your machine can read it, and more of those processes are now coding agents with shell access. The wrapping tools beat hush on this, and I’m not going to pretend the file is secure because I like it.
My claim is that it’s the same exposure you already have, with the sharing problem fixed. 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 and for CI. It will stay optional, because the .env file is the thing I set out to keep.
It’s new, so the disclaimer from the README applies: have someone review it before you trust it with production secrets. If you want to be that reviewer, the code is at github.com/mercho40/hush.