It started at 1am on a Tuesday. I was rotating a Cloudflare API token — routine stuff, the old one was about to expire. I updated the .env in the project I was working on, ran the deploy, everything was fine. Then the staging environment for a different project broke. Same token, different .env, still pointing to the old value.
I fixed it. Then a third project broke the next morning.
That's when I ran the command that changed everything:
$ find ~/dev -name ".env" -not -path "*/node_modules/*" -not -path "*/.git/*"
./boundless-learning/.env
./gitpulse/.env
./gitpulse/api/.env
./noxterm/website/.env
./blindspot/.env
./blindspot/api/.env
./112schade/.env
./bitz-snoek/.env
./playnist/.env
...
$ find ~/dev -name ".env" -not -path "*/node_modules/*" -not -path "*/.git/*" | wc -l
47
Forty-seven .env files. On one machine. I sat there staring at the terminal for a full minute.
The audit
I spent the next hour opening every single one. Here's what I found:
The duplicates. My Cloudflare API token — the one that just broke three projects — appeared in 6 different files. An OpenAI API key was in 8. The same Postmark server token was in 4 projects, two of which I hadn't touched in over a year.
The expired ones. A Stripe test key that had been rotated months ago was still sitting in three .env files. It didn't work anymore, but I'd never cleaned it up. If I'd accidentally used it in production, I would have gotten silent auth failures with no explanation — just mysterious 401s at 2am.
.env with a production database connection string. Full admin credentials. The project was archived — I hadn't opened it in 8 months. But the credentials were still valid. Anyone with access to my machine could have connected to a production database with patient-adjacent data, and I wouldn't have known.
The forgotten ones. A side project from early 2024 — a weekend experiment I'd completely forgotten about — still had a live Stripe secret key in its .env. Not a test key. The real one. Connected to a real account with a real credit card.
That was the moment. Forty-seven plaintext files with zero authentication, scattered across my filesystem, containing credentials I couldn't even remember storing. Some valid, some expired, no way to tell which without checking each one manually.
The migration
I decided to move everything to the macOS Keychain using NoxKey and delete every .env file on my machine. The whole process took one afternoon.
The workflow for each project was the same:
# Plaintext file, no auth
$ cat .env
DATABASE_URL=postgresql://...
OAUTH_CLIENT_SECRET=abc...
OPENAI_API_KEY=sk-proj-...
# Hope you .gitignored it
# Hope no agent reads it
# Hope you remember to update it
# Step 1: Import the .env file
$ noxkey import noboxdev/gitpulse .env
✓ Imported 4 secrets
# Step 2: Verify everything landed
$ noxkey ls noboxdev/gitpulse/
# Step 3: Peek to confirm
$ noxkey peek noboxdev/gitpulse/OPENAI_API_KEY
sk-proj-...
# Step 4: Test it works
$ eval "$(noxkey get noboxdev/gitpulse/OPENAI_API_KEY)"
# Step 5: Delete the liability
$ rm .env
For projects that shared secrets — like the Cloudflare token that lived in 6 places — I stored it once under a shared prefix:
$ noxkey set shared/CLOUDFLARE_API_TOKEN --clipboard
✓ Stored shared/CLOUDFLARE_API_TOKEN
One token. One location. Accessible from any project. When I rotate it next time, I update it once. Not six times. Not three-out-of-six times.
The healthcare API credentials got an extra layer:
$ noxkey strict noboxdev/healthcare-api/DATABASE_URL
✓ Marked as strict — always requires Touch ID
Strict mode means that secret always requires Touch ID, even during a session unlock. No shortcuts for credentials that could expose patient data.
The first week
I won't pretend it was seamless. The first two days were friction city.
Every time I opened a terminal to work on a project, my muscle memory reached for the .env that wasn't there anymore. Instead of source .env or letting dotenv auto-load, I had to type eval "$(noxkey get noboxdev/project/KEY)" and Touch ID for each secret.
I nearly caved on day two. I was debugging a webhook integration and needed to restart the server about fifteen times in an hour. Touch ID fifteen times. It felt excessive.
$ noxkey unlock noboxdev/blindspot
✓ Session unlocked — Touch ID skipped for noboxdev/blindspot/* until session expires
One Touch ID authentication, then every get under that prefix skips the prompt for the rest of the session. That changed everything. I'd unlock at the start of a work session and forget about it until the session expired.
By day four, the new workflow felt natural. By the end of the week, I didn't think about it anymore.
The AI agent problem I didn't know I had
Two weeks after the migration, I was pair-programming with Claude Code on the Blindspot project. The agent needed the Postmark token to test an email integration. Old workflow: it would have read my .env and the raw token would be sitting in the conversation context. Logged. Visible. Potentially leaked in an error message.
Instead, the agent ran noxkey get. NoxKey detected the agent by walking the process tree, encrypted the value with AES-256-CBC, wrote a self-deleting temp script, and returned a source command. The secret reached the shell environment, but the raw value never appeared in the conversation.
I hadn't even been thinking about AI agent security when I migrated. I deleted my .env files because of the duplication and rotation nightmare. The agent safety was a side effect — and honestly, it turned out to be the more important benefit. I use AI agents every day now. Every single session would have been reading my plaintext secrets if I'd kept those .env files.
For more on this attack surface, I wrote about six specific ways agents can leak your secrets.
Six months later
It's been six months since I deleted the last .env file. Here's what's different:
Key rotation is a non-event. When my Cloudflare token expires, I update it in one place. Every project picks up the new value next time it runs noxkey get. No hunting through directories. No grepping for old values. No "which three of the six copies did I forget to update?"
I know exactly what I have. noxkey ls shows me every secret on my machine, organized by project. No more discovering a forgotten Stripe key in an archived repo. If it's in the Keychain, I can see it. If it's not, it doesn't exist on my machine.
$ noxkey ls
noboxdev/blindspot/POSTMARK_SERVER_TOKEN
noboxdev/blindspot/DATABASE_URL
noboxdev/gitpulse/DATABASE_URL
noboxdev/gitpulse/OAUTH_CLIENT_SECRET
noboxdev/gitpulse/OPENAI_API_KEY
noboxdev/healthcare-api/DATABASE_URL [strict]
shared/CLOUDFLARE_API_TOKEN
shared/CLOUDFLARE_ACCOUNT_ID
...
New projects start clean. When I start a new project, there's no .env.example to copy and fill in. I store the secrets once with noxkey set and use eval to load them. The project directory has zero credential files. Nothing to accidentally commit, nothing for an agent to read, nothing to forget about when I archive the project.
The anxiety is gone. This one surprised me. I didn't realize how much low-grade background worry I carried about those plaintext files. "Did I .gitignore that correctly?" "Is that old project's .env still sitting there with live keys?" "Did the AI just read my database credentials?" Those questions don't exist anymore. The secrets are in the Keychain, behind Touch ID. That's it.
The honest downsides
It's macOS only. I work exclusively on macOS, so this isn't a limitation for me. If you're on a mixed team, your Linux colleagues need a different solution. The principle is the same — use your OS credential store — but NoxKey won't help them.
Onboarding new team members takes an extra step. Instead of "here's the .env.example, fill in your keys," it's "install NoxKey, then noxkey set each key." It's more steps the first time. Every time after that, it's simpler.
Some tools expect .env files. Docker Compose, certain Node.js frameworks, Vercel's local dev server. For those, I generate a temporary .env from the Keychain, use it, and delete it. It's not perfect. But it's friction I'm willing to accept because the alternative is 47 plaintext files with no authentication.
Delete yours
Run this command right now:
find ~/dev -name ".env" -not -path "*/node_modules/*" -not -path "*/.git/*" | wc -l
Whatever number you see — that's how many plaintext files with zero authentication are sitting on your machine right now, containing credentials that any process, any agent, any accidental git push can expose.
I'm not telling you NoxKey is the only answer. I'm telling you .env files are the wrong answer. Use your Keychain. Use 1Password CLI. Use something with actual authentication. But stop treating plaintext files as secret storage.
noxkey import, verify with noxkey ls, then delete every .env file. One afternoon of work eliminates plaintext secrets, duplicate key rot, and AI agent exposure — permanently.
If NoxKey is the route you want to take:
brew install no-box-dev/noxkey/noxkey
Free, no account, no cloud, your secrets never leave your machine. The migration took me one afternoon. Six months later, I can't imagine going back.