◉ANTIGRAVITY LABJP
Articles/App Development
▣ App Development/2026-10-18Intermediate

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.

Cloudflare Workers8wrangler.dev.vars.gitignoresecrets3Python20indie development21

✦ Premium Article

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, sys
from pathlib import Path
 
repo = 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 Secrets
for 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.

or
Unlock all articles with Membership →
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.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

▣ App Dev2026-09-19
I Asked for the Whole Quote PDF Tool at Once, Then Split It Into Four Requests
How I moved my client quote paperwork from copy-and-edit to a small PDF tool built with Antigravity, why the first single-shot request had to be thrown away, and the four smaller requests that replaced it, with working code.
▣ App Dev2026-08-19
Turn Off the Liquid Glass Compatibility Flag Yourself, Before Xcode 27 Does It for You
UIDesignRequiresCompatibility is expected to be ignored in Xcode 27. Flip it to false yourself and you can rehearse next year's forced appearance today, on the SDK you already have. Here is what actually broke in my wallpaper apps, and the order I checked things in.
◈ Agents & Manager2026-10-16
Before Handing an Agent a .zip: Dropping .git Cut 29 MB to 15 MB, and 28 Secret Warnings Turned Out to Be Noise
Antigravity app v2.23.0 lets you attach a .zip. I packed my own repository to count what .git weighs and how many secret-scan warnings are false alarms, and I leave the full Python script that excludes, scans, and writes a manifest.