with-env: keeping .env files encrypted
Most of my projects have a .env file: API keys, database URLs, the odd password. They’re wonderfully convenient and slightly alarming in the same breath. They sit on disk in plain text, they’re one bad .gitignore away from being committed, and anything running as me can read them. That last one is what bothers me most. With Claude Code poking around in my project directories all day, I don’t want it slurping up a .env file by mistake.
with-env is a small command-line tool that keeps those files encrypted with age, and decrypts them only at the moment a command needs them. The aim is for it to be no more effort than the plain file. Once a project is set up, you just put with-env in front of whatever you were going to run:
with-env init # register this directory as a project
with-env edit dev # opens $EDITOR, age asks for a passphrase
with-env uvicorn app.main:app --reload # runs with dev's variables loaded
with-env prod psql # or name the environment explicitly
How it works
The encrypted files live in ~/.config/with-env/, one folder per project and one .age file per environment (dev, staging, prod and so on). Keeping them out of the project directory means there’s nothing sensitive in the tree to commit by accident, encrypted or not.
You don’t have to be in the project root. It walks up from the current directory to find the registered project that contains it, so it works from deep inside src/ too.
When you run a command, it decrypts the file in memory, parses the KEY=value lines and then execs your command with those variables added. exec replaces the Python process outright, so nothing of with-env is left hanging around holding the plaintext while your server runs.
Editing is the one time the plaintext has to exist as a file, because editors edit files. with-env decrypts it into /dev/shm, a RAM-only file system on Linux, then re-encrypts it when the editor exits and shreds the working copy. That’s also why it’s Linux only. The catch is the editor. Vim cheerfully writes swap files, backups and undo history next to whatever you open, which would put the secrets right back on disk. The README has the incantations to stop vim and nano doing that.
For long sessions, source with-env-load dev decrypts once and exports everything into the current shell, and source with-env-unload takes it all out again. It keeps a note of which variables it added, so unloading doesn’t touch anything else.
A banner for production
One feature is refreshingly low-tech. Any environment called prod or production prints a yellow warning banner before it runs anything. You can set your own message, or ask it to make you type the environment’s name before it carries on. The warning appears before the passphrase prompt, so if you’ve typed prod by mistake, you find out before you’ve committed to it.
What it doesn’t do
The README is blunt about the threat model. with-env protects secrets at rest: a file-system scan, an accidental commit or an unencrypted backup of the home directory finds nothing it can use. It does not protect them in use. Once your app is running, the variables are in its environment, and anything running as you can read /proc/<pid>/environ. Anyone who can log your keystrokes gets the passphrase too.
The proper fix for that is a hardware key, so decrypting needs a physical touch. age has a plugin for FIDO2 keys, and the README sketches how it would slot in, but for now it’s passphrases only.
Where it stands
It’s on GitHub as with-env, under an MIT licence: three small scripts and an installer.
Project assisted by Claude Code.