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.
Antigravity app v2.23.0 (October 12, 2026) lets you attach a .zip archive to a prompt. The official changelog says the agent unpacks it with its own tools.
I wanted to try it on the repository of a website I run as an indie developer. I had the whole directory half-compressed when I stopped. I couldn't say exactly what was inside it.
Here is the conclusion first. Decide what stays out before you hand anything over, and read secret-scan warnings by their contents, not their count. Below are the script I wrote for that and the numbers I got from my own repository.
What the changelog says, and what it doesn't
The v2.23.0 entry says two things: you can attach a .zip and the agent unpacks it with its tools, and you can add a folder from the + menu in the message box. The same release also adds a picker for the agent's permissions preset.
What it doesn't say is just as relevant: size or file-count limits, where the archive is unpacked, and which files get skipped. As far as I checked, the changelog has nothing on those. What goes in is your decision as the sender.
I used the repository of my own Next.js website: a shallow clone (only the latest commit) with 2,424 files. I made three zips to compare — everything, excluded-and-scanned, and a single subdirectory.
How it was packed
Files
Total unpacked
Zip size
Everything (with .git)
2,424
50.55 MB
29.43 MB
.git etc. excluded, scanned
2,397
36.03 MB
15.20 MB
Only the one directory needed
284
5.16 MB
2.08 MB
What surprised me was how much dropping .git mattered. The unpacked total fell by only 29%, yet the zip got 48% smaller. The contents of .git are already compressed, so zipping barely shrinks them. The excluded .git was 14.52 MB and the zip difference was 14.23 MB. .git rode into the zip at almost its full weight. With a long history, the gap would be wider.
✦
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'll be able to say exactly what goes into a .zip and what stays out, with sizes and counts, before you hand a repository to an agent
✦When a secret scan fires dozens of times, you'll be able to tell dummy values from real ones line by line, and set it up so the same warnings stop returning
✦You'll skip the manual check for .git and .env on every handoff by letting one script do it in a few seconds
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.
This is what I keep next to the repository now. It walks the directory, counts what it excludes, looks for lines that resemble secrets, and, if nothing is found, writes the zip plus a listing (_MANIFEST.tsv).
#!/usr/bin/env python3"""agzip.py: Build a .zip for the agent only after checking what goes in."""import argparse, fnmatch, hashlib, os, re, sys, zipfileSKIP_DIRS = {".git", "node_modules", ".next", "dist", "build", ".venv", "__pycache__", ".wrangler", ".turbo", ".cache"}SKIP_FILES = [".env", ".env.*", "*.pem", "*.key", "id_rsa*", "*credentials*.json", ".DS_Store", "*.log"]KEEP_FILES = [".env.example", ".env.sample"] # let these through: they only name the variablesSECRET_RES = { "github-token": re.compile(r"gh[pousr]_[A-Za-z0-9]{30,}"), "aws-access-key": re.compile(r"AKIA[0-9A-Z]{16}"), "google-api-key": re.compile(r"AIza[0-9A-Za-z_\-]{35}"), "private-key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), "generic-secret": re.compile(r"(?i)(secret|token|password)\s*[:=]\s*['\"][^'\"\s]{16,}['\"]"),}# drop obvious dummy values from warnings (widen this too far and real keys slip by)PLACEHOLDER = re.compile(r"(?i)(x{4,}|your_|<[^>]+>|\$\{|example|dummy|changeme|_test_|\.\.\.)")TEXT_LIMIT = 1_000_000def line_id(line): return hashlib.sha256(line.strip().encode()).hexdigest()[:8]def skipped_file(name): if name in KEEP_FILES: return False return any(fnmatch.fnmatch(name, p) for p in SKIP_FILES)def dir_bytes(path): return sum(os.path.getsize(os.path.join(r, f)) for r, _, fs in os.walk(path) for f in fs if not os.path.islink(os.path.join(r, f)))def collect(src): kept, dropped, links = [], {"dirs": 0, "files": 0, "bytes": 0}, [] for root, dirs, files in os.walk(src, followlinks=False): keep = [] for d in sorted(dirs): full = os.path.join(root, d) if os.path.islink(full): # a link may point outside the folder, so leave it out links.append(os.path.relpath(full, src)) elif d in SKIP_DIRS: dropped["dirs"] += 1; dropped["bytes"] += dir_bytes(full) else: keep.append(d) dirs[:] = keep for f in sorted(files): full = os.path.join(root, f); rel = os.path.relpath(full, src) if os.path.islink(full): links.append(rel) elif skipped_file(f): dropped["files"] += 1 else: kept.append((rel, os.path.getsize(full))) return kept, dropped, linksdef scan(src, kept): hits = [] for rel, size in kept: if size > TEXT_LIMIT: continue try: with open(os.path.join(src, rel), encoding="utf-8") as fh: for n, line in enumerate(fh, 1): for kind, rx in SECRET_RES.items(): m = rx.search(line) if m and not PLACEHOLDER.search(m.group(0)): hits.append((rel, n, kind, line_id(line))) except (UnicodeDecodeError, OSError): continue # binaries are out of scope for the regexes return hitsdef main(): ap = argparse.ArgumentParser() ap.add_argument("src"); ap.add_argument("out") ap.add_argument("--max-mb", type=float, default=20.0) ap.add_argument("--ack", action="append", default=[], help="a line you checked by eye and judged a false positive, as path:id (id is the 8 digits shown in the warning)") a = ap.parse_args() kept, dropped, links = collect(a.src) hits = [h for h in scan(a.src, kept) if f"{h[0]}:{h[3]}" not in a.ack] print(f"files={len(kept)} raw={sum(s for _, s in kept)/1e6:.2f}MB " f"dropped_dirs={dropped['dirs']}({dropped['bytes']/1e6:.2f}MB) " f"dropped_files={dropped['files']} symlinks={len(links)}") for rel, n, kind, lid in hits: print(f"SECRET? {rel}:{n} [{kind}] ack={rel}:{lid}") # never print the value itself if hits: print("ABORT: lines that look like secrets were found"); return 2 with zipfile.ZipFile(a.out, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z: manifest = ["path\tbytes\tsha256_8"] for rel, size in kept: zname = rel.replace(os.sep, "/") # keep "/" inside the zip on Windows too data = open(os.path.join(a.src, rel), "rb").read() manifest.append(f"{zname}\t{size}\t{hashlib.sha256(data).hexdigest()[:8]}") z.writestr(zipfile.ZipInfo(zname, (2026, 1, 1, 0, 0, 0)), data, zipfile.ZIP_DEFLATED) z.writestr("_MANIFEST.tsv", "\n".join(manifest) + "\n") mb = os.path.getsize(a.out) / 1e6 print(f"zip={mb:.2f}MB") if mb > a.max_mb: print(f"ABORT: over {a.max_mb}MB"); return 3 return 0if __name__ == "__main__": sys.exit(main())
Three notes on why it is written this way.
First, symbolic links are left out. A link pointing outside the folder could pull outside files into the archive the moment you pack it. Second, the timestamps inside the zip are fixed, so the same input produces the same zip and you can compare against last time. Third, _MANIFEST.tsv ships inside. It becomes the first listing the agent can open, and it lets me check later what I handed over.
28 warnings fired, and none were real
In the first version, the secret scan fired on 28 lines. All of them were sample code in the technical articles that live in the repository: masked values like ghp_xxxxxx, YOUR_TEST_PASSWORD, ${STRIPE_WEBHOOK_SECRET}, whsec_test_dummy_value. Every one was a dummy I had written for illustration.
So I dropped obvious dummies from the warnings — runs of x, YOUR_, ${, example, dummy. That took 28 lines down to 6. I opened each remaining one and checked: a quoting example whsec_abc#def123, a stand-in password securepassword123, and a line that only describes the header of a private key in prose. All 6 were false positives; the number of real secrets was 0.
The question I wrestled with was how to record the lines I had reviewed. At first I used "I've looked at this file, let it pass." But that lets a real key pasted into the same file later walk straight through. So the warning now carries an 8-digit hash of the line, and only a line named with --ack path:id passes. If the line is rewritten, the hash changes and the script stops again.
Make a scanner too strict and nobody reads it; make it too loose and it lets real keys by. I settled on few dummy exclusions and fine-grained, per-line records of what I've reviewed.
Three pitfalls
The first is the .env exclusion. My first version dropped .env.* wholesale, which took .env.example with it. To tell the agent which variables exist, the example file with no values should go through. It is now an exception in KEEP_FILES.
The second is that a scan finding nothing is not proof that nothing is there. It is a set of regular expressions, and it does not look at files over 1 MB or at binaries. "Not found" only means none of the shapes I know were present.
The third is the assumption that a zip is a point-in-time copy. I operate on the idea that edits to the unpacked files don't flow back to the original folder. That is my assumption, not something I confirmed in the specification, and I'd keep a separate way to bring changes back.
Choosing how to hand things over
This is my judgment for now. If you only want the agent to read your working code and fix something, adding the folder from the + menu takes less effort. I reach for a zip when I want to pass along material received from someone else as it is, or freeze "the contents as of now." Either way, the smaller the scope, the clearer the intent comes across, I find.
What I haven't verified
I haven't tested where the agent unpacks the archive, the size or file-count limits, or how actions after unpacking follow the permissions preset. Every number above comes from running the script on my own repository; none of it measures Antigravity's behavior. I added code to normalize paths inside the zip to /, but I haven't run it on Windows.
Next steps
In the directory you want to hand over, run python3 agzip.py . out.zip once and look at dropped_dirs and zip=
If a warning appears, open the file and confirm by eye whether it is real
Record only the false positives with --ack path:id and run it again
Open the generated _MANIFEST.tsv and check the count and names before you hand it over
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.