Before Letting an Agent Touch My Config, I Found That .env* Doesn't Cover .dev.vars, and Wrote a Preflight Script
Auditing my own Cloudflare Workers repo showed that the .env* line in .gitignore never protected .dev.vars. Here is the full Python script that asks git itself where secrets can leak before an agent opens the repo.
I was about to ask an Antigravity agent to tidy the configuration of one of the websites I run as an indie developer. Just before typing the prompt, I stopped. I was trying to answer "where do this repo's secrets live?" from memory alone.
My .gitignore has .env* in it, so I assumed I was fine. To test that assumption, I wrote a small audit script.
The conclusion first: .env* only protects files that start with .env. The local file that Cloudflare Workers uses, .dev.vars, sits outside that pattern. That was true in my own repository.
Three things I wanted to know
Before handing a repo to an agent, I wanted answers to three questions:
Which environment variable names does the code read? (names only, never values)
Does the plaintext [vars] block in wrangler.toml contain anything that looks like a secret?
Are the local secret files actually hidden from git?
The third one is the crux. If I judge a .gitignore by reading its patterns, my misreading becomes the blind spot. So I let git decide. git check-ignore tells you whether a path would be ignored, even when the file doesn't exist yet.
The full script
It needs only the Python 3 standard library. Exit code 0 means clean, 1 means action needed, 2 means a usage error.
#!/usr/bin/env python3"""Audit where secrets live in a Workers repo. Run it before handing the repo to an agent.Exit code: 0 = clean / 1 = action needed / 2 = usage error"""import re, subprocess, sysfrom pathlib import Pathrepo = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()if not (repo / ".git").exists(): print("Not a git repository:", repo); sys.exit(2)def git(*a): return subprocess.run(["git", "-C", str(repo), *a], capture_output=True, text=True)problems, notes = [], []# 1) Collect the env var NAMES the code reads (never the values)names = set()for p in git("ls-files", "*.ts", "*.tsx", "*.js", "*.mjs").stdout.split(): try: names |= set(re.findall(r"process\.env\.([A-Z][A-Z0-9_]+)", (repo / p).read_text(errors="ignore"))) except OSError: pass# 2) Does [vars] in wrangler.toml (the plaintext side) hold secret-looking names?cfg = repo / "wrangler.toml"plain = {}if cfg.exists(): # Read only the [vars] section by hand so this runs on 3.10 (tomllib is 3.11+) sect = re.search(r"^\[vars\]\s*$(.*?)(?=^\[|\Z)", cfg.read_text(), re.M | re.S) for line in (sect.group(1).splitlines() if sect else []): m = re.match(r'\s*([A-Za-z0-9_]+)\s*=\s*"(.*)"\s*(#.*)?$', line) if m: plain[m.group(1)] = m.group(2)SECRETISH = re.compile(r"(SECRET|TOKEN|PASSWORD|PRIVATE|API_?KEY|WEBHOOK)", re.I)for k, v in plain.items(): if SECRETISH.search(k) and not k.startswith("NEXT_PUBLIC_"): problems.append(f"[vars] holds a secret-looking name: {k} (move it to wrangler secret put)") elif str(v).startswith(("sk_", "whsec_", "ghp_")): problems.append(f"[vars] value for {k} starts with a secret prefix")# 3) Secret-looking names missing from [vars] are assumed to live in Secretsfor n in sorted(names): if SECRETISH.search(n) and n not in plain: notes.append(f"{n}: read by code, absent from [vars] -> assumed to be a Secret (dashboard / secret put)")# 4) Are local-only files ignored? Ask git itself.for f in (".env", ".env.local", ".dev.vars", ".dev.vars.production"): r = git("check-ignore", "-q", f) if r.returncode != 0: problems.append(f"{f} is not ignored (git add -A will take it the moment it exists)")# 5) Is anything already tracked?tracked = [p for p in git("ls-files").stdout.split() if re.search(r"(^|/)(\.env|\.dev\.vars)", p)]for p in tracked: problems.append(f"already tracked: {p}")print(f"env vars read by code: {len(names)} / [vars] entries: {len(plain)}")for n in notes: print(" ・", n)for p in problems: print(" ✗", p)print("result:", "%d issue(s)" % len(problems) if problems else "clean")sys.exit(1 if problems else 0)
Here is why it is built this way.
It never reads values. It collects variable names and never opens .env or .dev.vars. An audit that prints secrets to the screen or into a log defeats its own purpose.
It distrusts [vars] by name. Values in [vars] stay in plaintext, both in the config and in the dashboard. A name containing SECRET, TOKEN, or WEBHOOK is my cue to move it to a Secret with wrangler secret put. Names starting with NEXT_PUBLIC_ are public by design, so I excluded them.
It asks git whether a path is ignored.check-ignore -q returns 0 when ignored and 1 otherwise. Because I don't reimplement .gitignore grammar, there is no room for me to misread it.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can state, within a few minutes and from your own environment, where your secrets live and which of them could end up in git before you let an agent open the repo
✦You can replace the belief that .env* has it covered with git's own verdict, so the same blind spot doesn't come back
✦You can stop plaintext secrets in wrangler.toml [vars] before the commit, using an exit code rather than a code review
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
env vars read by code: 4 / [vars] entries: 1 ・ STRIPE_SECRET_KEY: read by code, absent from [vars] -> assumed to be a Secret (dashboard / secret put) ・ STRIPE_WEBHOOK_SECRET: read by code, absent from [vars] -> assumed to be a Secret (dashboard / secret put) ✗ .dev.vars is not ignored (git add -A will take it the moment it exists) ✗ .dev.vars.production is not ignored (git add -A will take it the moment it exists)result: 2 issue(s)
Three things stand out.
Item
Result
What it means
Env vars read by code
4
Two are secrets (a Stripe key and a webhook secret), one is a public URL, one is a price ID
Entries in [vars]
1
Only a public URL; no secret was sitting in plaintext
Ignore status of local files
2 not ignored
.env* does not match .dev.vars
In production, the secrets were where I expected, on the Secret side. But the moment someone created a .dev.vars to run wrangler dev, git add -A would have swept it in. Nothing had leaked, since the file wasn't in the repository yet. Still, if I tell an agent "run it locally and check", creating that file is a perfectly natural thing for it to do.
I wasn't wrong about the tool, I was wrong about the file name
What surprised me was the gap between my memory and the real configuration. Writing .env* felt like covering every local secrets file. But wrangler reads .dev.vars, not .env, and you can keep per-environment variants as .dev.vars.<environment>. It belongs to a different naming family, so the .env* umbrella never reached it.
I also hit a small snag. My first version parsed wrangler.toml with the standard library's tomllib, and the Python 3.10 on my machine doesn't have it (it arrived in 3.11). I switched to reading only the [vars] section with a regular expression. An audit tool gets used more often when it runs anywhere without extra installs.
env vars read by code: 1 / [vars] entries: 2 ・ STRIPE_SECRET_KEY: read by code, absent from [vars] -> assumed to be a Secret (dashboard / secret put) ✗ [vars] holds a secret-looking name: STRIPE_WEBHOOK_SECRET (move it to wrangler secret put) ✗ .dev.vars is not ignored (git add -A will take it the moment it exists) ✗ .dev.vars.production is not ignored (git add -A will take it the moment it exists) ✗ already tracked: .dev.varsresult: 4 issue(s)exit=1
A secret in [vars], a hole in the ignore rules, and an already tracked file were each caught on their own line. The point is to stop on the exit code instead of eyeballing warnings. My rule from now on: confirm exit=0 before typing the prompt.
Make it a commit gate, so the agent's hands stop too
I wanted the same audit to run not only before I hand a repo over but also after the agent has worked in it. So I put it in pre-commit. When the audit returns 1, the commit itself stops.
#!/bin/sh# .git/hooks/pre-commit: audit where secrets live and block the commit on any issuepython3 "$(git rev-parse --show-toplevel)/tools/secret_surface_check.py" \ "$(git rev-parse --show-toplevel)" || { echo "pre-commit: secret placement needs attention. Fix the lines marked ✗ above and retry." >&2 exit 1}
In the throwaway repo from earlier, adding STRIPE_WEBHOOK_SECRET to [vars] was blocked on the first commit attempt (exit code 1), and with the line removed the commit went through (exit code 0). Even if an agent writes a value into [vars] with good intentions, it stops here once. A machine notices first, without waiting for a code review.
My routine
Here is the order I follow in practice:
Run the audit and confirm it ends with zero ✗ lines (exit code 0)
If anything is flagged, fix .gitignore and [vars], then run step 1 again
Add the "work with names only, don't open contents" sentence to the prompt template and hand over the repo
When the work is done, let pre-commit run the same audit at commit time
In my repository, 2 of the 4 environment variables the code reads (50%) were secrets. That isn't a large share, but both are keys tied directly to billing. A single wrong assumption about where things live puts the most valuable things at risk first. That stayed with me.
Pitfalls
Adding a line to .gitignore does nothing for files git already tracks. Removing them from the index is a separate step, which is why the audit reports "already tracked" separately.
If .dev.vars was ever pushed, rotate the secrets inside it. Rewriting history can't undo copies that may already exist.
The audit never looks at values, so it can't tell a real key from a dummy. It only knows a name looks secret-like. For value-level scanning, see the article on scanning a .zip before attaching it.
What to tell the agent
Even after the audit passes, I add one sentence to my prompt template:
Do not open the contents of .dev.vars or .env; work with variable names only. If you create a new file, first confirm with git check-ignore that it is ignored.
Check where secrets live with git's verdict, not with memory. The script is about sixty lines, and it turns a vague "I think it's fine" into an exit code. If your repository looks anything like mine, I'd suggest running it once before you write your next prompt.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.