You know the freeze. git push, then half a second later: wait — is .env actually in .gitignore?
The .env file is how most of us handle secrets locally, and it's a mess. Plaintext. Sits on laptops forever. Gets pasted into Slack. Occasionally rides along into a public commit. Even teams paying for a proper secrets manager tend to do the same local dance: open the vault UI, copy the token, paste it into a .env, pray. I got tired of that, so I tried to get rid of the file entirely.
Here's the thing that bugs me about the status quo: production and local are worlds apart. Vault or a cloud SDK on a server? Fine, reasonable. But making someone run a daemon or write SDK boilerplate just to read a DB string on their own laptop is friction, and people route around friction every time. That's how plaintext creds end up on a dozen machines in the first place.
So I gave myself three rules: nothing plaintext on disk, no changes to app code (it has to work with Node, Python, Go, Rust, whatever), and no daemon running in the background.
How it works
env-pull is a small Go CLI. Rather than write secrets to a file, it wraps your command, pulls the secrets at runtime, and hands them straight to the child process's environment. Here's the path when it's pulling from AWS Secrets Manager:
env-pull run --aws-secret prod/db -- psql mydb
│
├─ 1. Load AWS default credential chain (zero new config)
├─ 2. Call secretsmanager:GetSecretValue("prod/db")
├─ 3. Parse JSON payload → map[string]string in memory
├─ 4. Merge with current environment (no overwrites)
├─ 5. exec(psql, env=[...existing..., DB_PASSWORD=s3cr3t, ...])
└─ 6. psql exits → memory is reclaimed, secrets are gone
It's plain os/exec underneath, so your app just sees normal environment variables — no idea anything injected them, no code to change. The moment the process exits, the OS takes the memory back and the secrets go with it. Nothing to clean up.
What it looks like
Here's a throwaway Node script that just prints what it finds in the environment:
// index.js
console.log("=== Application Lifecycle Initialized ===");
console.log("Database Access String : ", process.env.DATABASE_URL || "[MISSING - Connection Failed]");
console.log("Payment Gateway Token : ", process.env.STRIPE_SECRET_KEY || "[MISSING - Fallback Triggered]");
console.log("=== Application Lifecycle Terminated ===");
Run it the normal way and there's nothing there:
$ node index.js
=== Application Lifecycle Initialized ===
Database Access String : [MISSING - Connection Failed]
Payment Gateway Token : [MISSING - Fallback Triggered]
=== Application Lifecycle Terminated ===
The local encrypted flow
If you're not on a cloud provider, there's an offline path. env-pull edit opens your default editor:
$ env-pull edit
You type variables like a normal .env:
DATABASE_URL=postgres://local_dev_user:crypt_pass_99@localhost:5432/dev_db
STRIPE_SECRET_KEY=sk_test_51NxLocalOverrideKey
On save, env-pull grabs the buffer, overwrites the temp file with zeros, and encrypts the payload with AES-256-GCM. What's on disk is ciphertext and nothing else:
$ cat .env.pull.enc
U2FsdGVkX19vPlu8Y1bqm1h3S6Kx8vW9ZJ2H0m9L7qX+P... [Encrypted Binary Stream]
To run, prefix your command:
$ env-pull run -- node index.js
=== Application Lifecycle Initialized ===
Database Access String : postgres://local_dev_user:crypt_pass_99@localhost:5432/dev_db
Payment Gateway Token : sk_test_51NxLocalOverrideKey
=== Application Lifecycle Terminated ===
Launch the app the normal way right afterward and the values are already gone — not on disk, not in your shell history:
$ node index.js
=== Application Lifecycle Initialized ===
Database Access String : [MISSING - Connection Failed]
=== Application Lifecycle Terminated ===
Where this helps, and where it doesn't
Now the honest part, because there's no silver bullet here. env-pull is built for software-layer leaks: accidental commits, screen shares, plaintext sitting at rest on an SSD. It is not a hardware guarantee.
Zeroing buffers in a GC'd language like Go is best-effort, full stop. On copy-on-write or log-structured filesystems — APFS, ZFS, most NVMe SSDs — the OS shuffles physical blocks around on its own, so if you want a true physical wipe you need block-level disk encryption underneath. And anyone with root can read /proc/<pid>/environ or dump memory; if your machine is fully owned, this won't save you. What it does kill is the boring, everyday leak that actually takes teams down.
What's next
I built this for my own workflow, but I'm wiring in more upstream integrations: GCP Secret Manager, Azure Key Vault, 1Password CLI, and Bitwarden CLI are in progress.
It's open source, and I'd really like people poking at it — read the code, try the install, tell me what I got wrong. Repo's here. If a Homebrew or Scoop install chokes, open an issue.
Top comments (3)
You might like varlock.dev - it’s a mature solution in this space, with 15 plugins to pull from various providers. Also has local encryption support with biometric unlock. Built in validation and tons of other nice features.
Thanks Theo, will look into it
If you have any query, drop them here, for discussion