Redrawing Your Workspace Boundary Now That Review Mode Auto-Approves Reads
Antigravity CLI 1.1.20 made in-workspace reads auto-approved in review mode. Here is what I found when I actually counted what became readable across two real working trees.
One line in the Antigravity CLI 1.1.20 release notes says that in the default review mode, read access inside the workspace is now granted automatically. Confirmations for edits and external access stay exactly as they were. Only reads and listings go quiet.
That is a welcome change. As an indie developer running a lot of small projects, I was clicking through approval dialogs dozens of times a day, and clicking had stopped being a decision. It had become a reflex.
Then I stopped, because I could not answer a simple question: what is actually inside my workspace? While I was approving reads one at a time, every path was shown to me before it was opened. Once that stops, the judgment has to happen earlier — once, up front, when you decide what the workspace even is.
So I counted. Two working trees, mechanically. The numbers did not match what I had pictured.
The workspace is not what git shows you
I started with an ordinary Node project: npm init, four direct dependencies (express, typescript, axios, ws), and a .gitignore holding node_modules/, .env*, and dist/. Nothing unusual.
Measure
Value
Files on disk
1,352
Files tracked by git
3
Packages installed
79
Size of node_modules
38 MB
Four direct dependencies pulled in 79 packages transitively. Git tracks three files: package.json, package-lock.json, and .gitignore.
This is the part that is easy to gloss over. .gitignore decides what version control sees. It does not stop anything from reading the filesystem. A file that disappeared from git's view is still sitting right there on disk, and read auto-approval looks at the disk.
"My secrets are gitignored, so I'm fine" is a correct statement about commit accidents and a meaningless one about reads.
99.7% of the readable surface was code I never wrote
I re-counted the same tree, splitting it into dependency and build directories (node_modules, dist, .next, Pods, .venv, and friends) versus everything else.
Category
Files
Share
Files I put there
4
0.3%
Dependencies and build output
1,348
99.7%
Almost the entire readable surface was code I had not written a line of. By extension: 441 .js files, 292 .ts, 193 source maps, 144 Markdown files. Markdown and LICENSE files inside node_modules alone accounted for 220.
None of that is dangerous on its own. Reading dependencies is a normal and useful thing for an agent to do. What struck me was how far off my mental model was. When I read "read access inside the workspace," I pictured my source code. The actual set of files that could be opened without a prompt was more than three hundred times larger.
There is a practical consequence too. Once dependencies are excluded, the inventory drops from 1,352 files to 4. Taking inventory is not about reviewing everything — it starts by separating what you own from what you merely have on disk.
✦
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 will be able to measure what your agent can now read without asking, using numbers from your own working tree instead of assumptions
✦You will be able to find credentials sitting inside the agent's read surface and move them out before anything goes wrong
✦You will understand why name-based scanning misfires, and how a two-stage inventory cuts what you have to eyeball by roughly ten times
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.
Next I looked for credential-shaped files by name: .env variants, .pem, .p12, id_rsa, credentials.json, google-services.json, .mobileprovision, plus anything containing token, secret, or password. A deliberately wide net.
Ten hits. When I opened them, exactly one was real.
Of course the word "token" is everywhere in library source. It is obvious once you see it. But until I actually ran the check, I would not have guessed the false positive rate was ninety percent. At that precision, you skim the output a few times and then stop running it. My first version of this script very nearly went in the bin at this point.
Two things fix it: drop dependency directories from the scan, and confirm name matches by looking at the shape of the content. The script in the next section does both.
A two-stage inventory script
Stage one casts a wide net by filename. Stage two checks the file body for a shape: a credential-like key name assigned a value of at least twenty characters, or a PEM private key header. Values are never printed. The output is paths and counts only.
#!/usr/bin/env python3"""Count what falls inside the read-auto-approval surface, split intodependencies and your own files. File contents are never printed."""import os, re, subprocess, sys, json# Readable, but not something you put there yourselfDEP_DIRS = {"node_modules", "vendor", "Pods", ".venv", "venv", "target", ".next", "dist", "build", ".gradle", "DerivedData", "__pycache__"}# Stage 1: filename only — deliberately noisyNAME_RE = re.compile( r"""(^\.env($|\.)|^\.netrc$|^\.npmrc$|^\.pypirc$|^\.git-credentials$ |^id_(rsa|ed25519|ecdsa)$|\.pem$|\.p12$|\.p8$|\.keystore$|\.jks$ |^credentials(\.json)?$|^service[-_]account.*\.json$ |google-services\.json$|GoogleService-Info\.plist$|\.mobileprovision$ |token|secret|password)""", re.I | re.X)# Stage 2: confirm by shape (key name = long value, or a PEM header)BODY_RE = re.compile( rb"""((api[_-]?key|access[_-]?token|client[_-]?secret|password|private[_-]?key) \s*[:=]\s*["']?[A-Za-z0-9_\-/+]{20,} |-----BEGIN[ A-Z]*PRIVATE KEY-----)""", re.I | re.X)def classify(root): """Walk the tree, splitting on whether a path sits under a dependency dir.""" own, dep = [], [] for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if d != ".git"] rel_dir = os.path.relpath(dirpath, root) in_dep = bool(set(rel_dir.split(os.sep)) & DEP_DIRS) for fn in filenames: rel = os.path.normpath(os.path.join(rel_dir, fn)) (dep if in_dep else own).append((rel, fn)) return own, depdef body_hit(path, limit=262144): """Read the first 256KB only, so huge build artifacts cannot stall the walk.""" try: with open(path, "rb") as f: return bool(BODY_RE.search(f.read(limit))) except OSError: return False # unreadable file or broken symlink: skip quietlydef main(root): own, dep = classify(root) name_hits = [rel for rel, fn in own + dep if NAME_RE.search(fn)] body_hits = [rel for rel in name_hits if body_hit(os.path.join(root, rel))] try: t = subprocess.run(["git", "-C", root, "ls-files", "-z"], capture_output=True, timeout=60) tracked = len([p for p in t.stdout.decode("utf-8", "replace").split("\0") if p]) \ if t.returncode == 0 else None except Exception: tracked = None # works on directories that are not git repos print(json.dumps({ "own_files": len(own), "dependency_files": len(dep), "tracked_by_git": tracked, "stage1_name_hits": len(name_hits), "stage2_confirmed": len(body_hits), "confirmed_paths": sorted(body_hits)[:20], }, ensure_ascii=False, indent=2))if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else ".")
Ten became one, in 0.04 seconds. Because the cost is negligible, running it before you open a workspace is realistic rather than aspirational.
The limit=262144 cap and the except OSError came later. My first version read entire files and stalled on a large build source map. It also died partway through on a file it lacked permission to open. An inventory that stops early is worse than useless, because you have no idea what sits past the point where it stopped. Skipping quietly and finishing the walk is the safer failure mode here.
One more thing: os.walk does not follow symbolic links by default, and leaving it that way is the safer choice. Flip followlinks=True and a single link inside your tree starts pulling in directories outside it, quietly widening the very surface you are trying to measure. When I do want to see what is behind a link, I pass that path as the argument and run the script separately.
What stage two still leaves behind
I ran the same script against a very different tree: a site repository I maintain, 2,256 files, mostly Markdown, with no dependencies installed.
Measure
Node project
Content repository
Files on disk
1,352
2,256
Dependencies and build output
1,348
0
Stage 1 (by name)
10
27
Stage 2 (by content)
1
2
Scan time
0.04 s
0.09 s
Both stage-two hits turned out to be article bodies. An authentication troubleshooting piece contained a configuration example shaped like KEY = long value. Placeholders, obviously, not credentials.
So stage two takes you from 27 to 2. It does not take you to 0, and expecting it to will send you off tuning the regex forever. What you want from a check like this is a number small enough to look at with your own eyes. Two files, you open them. Twenty-seven, you don't.
Where the inventory actually changes something
Three things changed on my machine after counting.
1. Move credentials out of the working tree. This is the one that pays. Rather than protecting a range through settings, keep the thing out of the range entirely. For reading them from environment variables or the OS keychain instead, see how to safely manage environment variables and secrets in Antigravity.
2. Reconsider what you open as one workspace. If several projects live under one parent directory and you open the parent, the auto-approved read range is that whole parent. I keep six apps and several sites on one machine, with their working trees sitting side by side under a shared folder. Unrelated projects were inside the range, so I started opening projects individually. Some days I do want to work across repositories at once. In that case I keep the credential-carrying project outside the shared parent and open the rest together, which is a compromise I can explain to myself.
3. Write exclusions in .antigravityignore, then verify they took effect. Writing a rule and assuming it works is how you miss the patterns that silently do nothing. When exclusions do not apply, four places to check when .antigravityignore is not taking effect covers where to look.
The order matters. Start at step three and you will spend your time growing an exclusion list. Start at step one and there is far less left to exclude.
The procedure
Save the script as read_surface.py.
Run it against the directory you are about to open: python3 read_surface.py ~/projects/my-app.
Compare dependency_files against own_files. If what you own is a tiny fraction, the unit you are opening is too wide.
Open each path under stage2_confirmed and decide whether it is real or a documentation example.
Move the real ones out of the working tree and switch to environment variables or the OS keychain.
Add anything that cannot move — signing profiles, for instance — to .antigravityignore.
Re-run and confirm stage2_confirmed contains only entries you can explain.
Repeat steps 2 through 7 whenever you add dependencies or change what you open as one workspace.
Step eight is the one that slips. Adding a single dependency can grow the read surface by hundreds of files: four direct dependencies became 79 packages in this measurement.
Trading a repeated judgment for a single one
The approval prompt was tedious, but it doubled as a live view of the range. Automating it away means that visibility has to live somewhere else. For me, that somewhere is a single count taken before opening a project — one decision instead of dozens.
If you do one thing today, run the script at the root of whatever you have open right now and look at the ratio between own_files and dependency_files. If it matches your expectation, your workspace boundaries are already sound. If it does not, that gap is where the review starts.
I had the shape of my own working tree wrong until I measured it. If this saves you the same detour, that is a good outcome.
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.