Somewhere in the last month, an agent said "add your API key to the environment file" and you pasted one in. Then it happened again, for the database, the payment provider, the email service, the model API. Each paste took five seconds and none of them felt like a decision.
Now try to answer one question from memory: how many secret values does your app depend on, and where does each one live?
If you cannot answer, this post is the hour that fixes it. Not because you are careless — because the way AI-built apps get assembled quietly multiplies the places a key can leak, and nobody stops to draw the map.
Why AI-built apps leak differently
A hand-built app usually leaks a secret one way: someone commits the .env file. It is a well-known failure with well-known guards.
AI-built apps have more paths, because the keys pass through more hands:
- Chat transcripts. You pasted a key into a conversation once, to get past an error. That transcript now exists wherever your history syncs, and it may end up in a screenshot, a shared session, or a bug report.
- Generated files. Agents write README examples, config templates, test fixtures, and docs. Any of these can end up containing a real value instead of a placeholder, and generated files get skimmed, not read.
- The "make it work" move. When a server-only key breaks the demo — a CORS error, an undefined variable in the browser — the shortest path to green is moving the key to the client. An agent optimizing for "the feature works" will take that path unless told otherwise. The feature does work. The key is now published.
- Scope creep by default. Agents ask for a key; they do not ask for the least-privileged version of a key. So the app runs on whatever you first created, which is usually the one with every permission enabled.
None of this requires a mistake in any single step. It only requires nobody looking at the whole picture.
Step 1: the inventory
Take ten minutes and list every secret your app holds. The easiest sources are your local .env, your host's environment settings, and the sign-up emails from every service you connected. For each key, write four things:
- Which service, which key. "Payment provider, secret key" — not just "payments."
- What it can do. Read-only? Charge cards? Delete the database?
- Where it lives. Every location. Be honest — if it is also in a chat transcript or an old commit, write that down.
- The blast radius. One sentence: what happens the day this specific value shows up on the public internet.
This table is the deliverable. Everything below is what you do once you can see it. Keep it in the repo as SECRETS.md — names and locations only, never values.
Step 2: the two-places rule
A secret value is allowed to exist in exactly two places:
- Your local
.envfile, which is listed in.gitignore, for development. - Your host's environment settings, for production.
Everywhere else — source code, README examples, generated docs, test files, commit history, chat messages — gets the variable's name, never its value. PAYMENT_SECRET_KEY=your-key-here in an example file; the real string nowhere a git push or a screenshot can carry it.
Two checks make the rule real:
- Scan the repo, including history. A secret committed and then deleted is still in the history, and history is what gets cloned. Open-source scanners like gitleaks do this in one command, and GitHub's push protection can block new secrets at push time. Run the history scan once; turn the push guard on forever.
- Check what reached the browser. Open your deployed app, view the page source and the network tab, and search for the first characters of your keys. In most frontend frameworks, any variable prefixed
NEXT_PUBLIC_orVITE_is compiled into the client bundle — that prefix means published. Model API keys, payment secret keys, and database URLs must never carry it. This is the specific place to double-check the agent's work, because moving a key client-side is the classic "make it work" fix.
Step 3: shrink what each key can do
The inventory usually reveals that two or three keys are far more dangerous than the rest. Shrink them:
- Use the split your provider already gives you. Payment providers issue publishable and secret keys, and most can issue restricted keys scoped to specific operations. Model APIs let you create per-project keys with spending limits. Take the narrower key wherever it exists.
- Different keys per environment. Test-mode keys in development, live keys only in the production host's settings. If your development environment cannot charge a real card or write to the real database, a whole class of accident stops being possible.
- Read-only where read-only works. Analytics, monitoring, and content-fetching integrations rarely need write access. The agent will not request the weaker key; you have to hand it the weaker key.
Step 4: assume leaked, practice rotation
The uncomfortable habit, and the one that pays for the whole exercise: rotate one key today, as a drill.
Pick a low-stakes one. Issue the new value in the provider dashboard, update the two places it lives, redeploy, confirm the app works, revoke the old value. Time it.
You are practicing for the day it is not a drill. When a key actually leaks, the difference between a bad ten minutes and a bad weekend is whether rotation is a move you have made before — whether you know which dashboard, which two places, and what breaks in between. Write the steps into SECRETS.md while they are fresh. Add a "last rotated" column to the table; a key nobody has rotated in a year is a key nobody knows how to rotate.
The agent's role
As usual, the agent that created the sprawl is good at cleaning it up. All of this is prompt-sized:
- "Scan this repository, including git history, for anything that looks like a real secret value. List findings with file and commit; do not print full values."
- "Create a
.env.examplewith every environment variable the app reads, using placeholder values. Verify the list against actualprocess.envusage in the code." - "Add one rule to the project instructions: never write a literal secret value into any file or command; always reference environment variables by name."
Review the scan results yourself — this is data you cannot afford to skim. And do the rotation drill with your own hands; knowing the moves is the point.
The receipt
You are done when you can point at a SECRETS.md that says, in a table: every key, its scope, its two locations, its blast radius, and the date it was last rotated — plus one line at the top: "History scanned [date], push protection on, client bundle checked [date]."
Secrets are one line on the broader pre-launch security checklist, but they are the line with the shortest path from "oops" to "someone else is running up your accounts." An hour, once, and then it is just a table you keep honest.