Why I Built evnx: The AWS Incident I Can't Forget
I pushed AWS credentials to a public GitHub repo. Three services went down. A poorly formatted LLM prompt pointed me toward a solution. Here's the full story — and what I built so you never have to explain that incident to your lead.
Ajit Kumar
Creator, evnx
Status & Links
It was 11:47 PM on a Thursday.
I was pushing a hotfix to a side project — the kind of push you make when you're tired and just want the thing to work. I typed git add ., wrote a commit message like "fix: finally works," and hit enter.
I didn't notice .env was tracked.
The Next Eight Minutes
11:48 PM — GitHub email: "AWS key detected in your repository."
I didn't read it. I was already asleep.
12:03 AM — Three Slack messages from our on-call system: services down.
12:04 AM — My phone rings. My development lead. At midnight.
12:09 AM — I find the email.
GitHub's secret scanning had caught the AWS_SECRET_ACCESS_KEY almost immediately after the push. AWS had already responded: they applied a managed policy called AWSCompromisedKeyQuarantineV3 directly to the IAM user. That policy blocks access to high-risk actions across the account as a protective measure. It doesn't ask. It doesn't wait. It just locks things down.
Which is exactly why three services were down.
The automated systems had worked perfectly. I was the one who had made the mistake, and now I had to prove the account was clean before anything could be restored.
Some Context That Made It Worse
This wasn't a greenfield project. It was an Apache Airflow system I had been working on for over a year — 10+ DAGs, dozens of helper files, scrapers and ETL tasks that had started as independent experiments and graduated into the DAG over time. Each one had been tested individually in its own folder, with its own .env file.
That's how you end up with nested .env files across a monorepo. Not through carelessness — through organic growth.
The root .gitignore covered the root .env. It didn't cover packages/api/.env. I had never updated it when I added the new service directory. The project had no pre-commit hooks at the time. I was tired. It was a hotfix.
That's the full picture.
The Conversation I Don't Recommend Having
The next morning I had to explain what happened. Not just what — the technical part is easy: I committed a file I shouldn't have. The hard part is explaining why.
Why didn't I have .env in .gitignore? (I did. In the root. The monorepo structure wasn't covered.)
Why didn't I check before pushing? (I was tired. It was a hotfix. I was careless.)
Why didn't I use a secret manager? (I meant to set it up. Never got around to it.)
That conversation was more uncomfortable than the $847 AWS bill.
The Pain That Came Before — And Kept Coming After
I added a pre-commit hook with secret scanning and moved on. But the incident had surfaced a problem I had been living with for years across 7–8 projects — I just hadn't named it clearly until then.
Working across frontend and backend projects at my previous job, plus personal and consulting work, I kept running into the same friction points around .env files regardless of the stack, the team size, or the deployment target.
The .env vs .env.example drift. The idea is simple: commit an example file with all the keys but no real values so teammates know what they need. In practice, over months, keys get added to .env and never make it into .env.example. New developers clone the repo, fill out the example file, and hit cryptic runtime errors because the example was out of date. A one-line discipline problem that compounds into real onboarding pain.
Sharing and syncing across a distributed team. Every team member needs a working .env.local, .env.staging, or environment-specific variant. Add remote part-timers with high turnover — which was my situation — and you have a constant cycle of sharing, revoking, rotating, and re-syncing. Managing this was one of the most draining non-coding tasks in the workflow.
The wild west of .env formats. There is no formal specification. python-dotenv happily parses KEY="value" and strips the quotes. Docker's --env-file parser is stricter and chokes on the same syntax during image builds. Secret managers each want their own format: for one project I had to convert 30+ variables into a JSON file for AWS Secrets Manager. For another, I had to manually copy each variable into GitHub Actions secrets one by one — a focused, tedious, error-prone process that nobody should be doing by hand.
All of it — the drift, the syncing, the format fragmentation — was death by a thousand cuts. Manageable individually. Maddening collectively. Sitting quietly in the back of my mind.
The Poorly Written Prompt That Started Everything
About a year after the incident, sitting with all of these accumulated frustrations, I did what many developers do: I opened an LLM and started typing.
I wanted a comprehensive resource — a tutorial covering what .env files are, how to use them correctly, their limitations, and how different languages and ecosystems handle them.
My mistake: I forgot to specify the output format. I assumed Markdown.
The LLM returned a beautifully structured HTML document.
I read through it. It was actually quite good. And in a moment of "well, it's just one file," I decided to host it. Bought a domain, deployed the page. That became dotenv.space.
I added analytics mostly out of curiosity. Within days, organic search traffic was arriving. The data made one thing clear: there was genuine demand for better .env tooling, not just documentation.
That weekend, I decided to build a CLI.
Three Iterations of Rust
I dusted off about a year of on-and-off Rust learning and started building. The first working version came together in three to four days, with LLM assistance bridging the gaps in my Rust knowledge. I iterated on it, fixed bugs, used it on real projects, then did something that revealed the real state of the code: I read it line by line.
Even as a beginner, I could see the problems. The structure wasn't right. The abstractions were leaking. So I started over — not from scratch, but from understanding. The second iteration was better. The third was something I felt good enough to ship.
That third iteration is what you see now.
Why It's Called evnx
The name came from distribution. I wanted the tool available everywhere developers already were: Cargo, pip, npm, Homebrew, Scoop, winget, and GitHub Actions. Working through each distribution channel — each with its own packaging conventions, its own review process — was an education in itself.
When it came to naming, the name I originally had in mind was already taken across channels. I needed something close.
The solution: evnx. Phonetically adjacent, practically distinct, and backronymed into something that meant something:
Environment Variables Next Generation
What evnx Catches Now
Here's what evnx does today that would have stopped the incident entirely — and the dozens of smaller problems that came before it.
# Would have stopped me before the push
evnx scan
# [SCAN] Scanning .env files...
# [ERROR] AWS_SECRET_ACCESS_KEY matches high-entropy pattern (256-bit key)
# [ERROR] Pattern: aws_secret (confidence: high)
# [INFO] 1 secret detected. Commit blocked.
# [TIP] Use `evnx migrate --to aws-secrets-manager` to move this to a secret manager.The pre-commit hook takes 180ms on a cold start. Nobody disables it.
# Would have caught the gitignore misconfiguration
evnx doctor
# [DOCTOR] Running environment health check...
# [WARNING] .env is tracked by git in packages/api/
# [WARNING] .gitignore at root does not cover packages/api/.env
# [OK] .env.example is present and up to date
# [INFO] 1 warning, 0 errorsThe doctor command is what I actually needed most. Not the scanner — I knew the key was sensitive. I didn't know my .gitignore wasn't covering that directory.
# No more manual copy-paste into GitHub Actions or AWS Secrets Manager
evnx migrate --from .env --to github-actions
evnx migrate --from .env --to aws-secrets-manager
evnx migrate --from .env --to kubernetes
# Validate .env.example is in sync before a PR merge
evnx diff .env .env.exampleCurrent version
evnx v0.3.8 is live. It handles scanning, validation, format conversion, .env.example sync, cloud migration, encrypted backups, and a new add command for managing variables without touching the file directly.
The Pattern I See Everywhere
Since publishing evnx, I've talked to dozens of developers who've had similar incidents. The pattern is always the same:
- ›Developer knows secrets shouldn't be committed
- ›Developer has some protection in place (
.gitignore, code review) - ›Developer hits an edge case that slips through the protection
- ›Incident happens
The protection fails at the edges — monorepo structures, new team members, new service directories, rushed hotfixes at midnight.
evnx is designed to catch the edges. It's opinionated about what "safe" looks like, and it fails loudly when something doesn't match.
What's Next
The CLI is open source and free forever. But the incident I described wasn't just a solo developer problem — it was a team problem. My .env.example wasn't updated. No one else on the team knew the new service had its own .env.
The next version of evnx is about teams: shared secret management, audit logs, role-based access, and sync across machines. A web dashboard for the things that don't belong in a CLI.
If you've been in a similar situation — or you're building a system where you'd rather not be — try evnx today. The pre-commit hook alone is worth the five minutes.
Never again
Install the pre-commit hook. Even if you're careful. Especially if you're tired.
# Install (pick your package manager)
cargo install evnx
pip install evnx
npm install -g @evnx/cli
# Or use the install script
curl -fsSL https://evnx.dev/install.sh | bash
# Start here
evnx doctorGet Involved
evnx is open source and actively maintained. Everything below is a good next step depending on where you are:
| What you want | Where to go |
|---|---|
| Install and get started | evnx.dev |
| Read the source, file issues, contribute | github.com/urwithajit9/evnx |
Try commands against real .env fixtures | github.com/urwithajit9/evnx-test |
| Add evnx to your CI/CD pipeline | github.com/urwithajit9/evnx-action |
| Install via Homebrew (macOS / Linux) | github.com/urwithajit9/homebrew-evnx |
| Install via Scoop (Windows) | github.com/urwithajit9/scoop-evnx |
Status & Links
Three years and zero incidents later — evnx is the safety net I wish I'd had.