Treating Code Search as a Contract: Measuring the /codesearch Regex Default
Antigravity CLI 1.1.3 interprets /codesearch queries as regular expressions by default. I measured the gap between regex and literal matching across a 979-article repository and turned the results into three query rules and a drift harness.
The morning after I asked an agent to rewrite every call site, one line was still on the old signature.
The rewrite itself had been careful. The problem sat one step earlier: the search the agent ran first never returned that line. The work was fine. The set it worked on was quietly incomplete.
A bad edit is visible. A missing search result is not. Nothing failed — you simply believe that what came back was everything.
What 1.1.3 made the default
Antigravity CLI 1.1.3 (July 16) added /codesearch, aliased as /cs and /search.
Three parts of the spec matter in day-to-day use:
Item
Behavior
Query interpretation
Regular expression by default
-F / --literal
Switches to exact (literal) matching
f: / file:
Globs to include or exclude paths
I skimmed past that first line. As an indie developer I had spent years assuming that whatever I typed into a search box was simply the string I wanted to find.
But config.json, useState(, and ja|en all mean something else when read as patterns. A human sees the results and thinks, that's not right. An agent doesn't. It takes the returned set and starts working on it.
Measuring the gap on a real repository
Rather than guess, I measured.
The target was the Antigravity Lab repository itself: 979 Japanese MDX articles and 73 files under src/. I reproduced the same semantics /codesearch uses — regex by default, literal under -F — with local grep -E and grep -F, then compared hit counts for identical queries.
Query
Regex (default)
Literal (-F)
Gap
config.json
125
125
0
tsconfig.json
86
86
0
package.json
200
199
+1
.mdx
41
35
+6
antigravity.google
48
35
+13
config.json showed no gap at all. The dot matches any character, sure — but if no string like configXjson exists in the tree, nothing breaks.
That is exactly what makes this hard. For most queries, the regex default breaks nothing. So you never get a reason to question it.
The gap appeared on antigravity.google: 13 of 48 hits, roughly 27%, were false positives. Here's what they actually were:
The dot matched a hyphen. I was auditing outbound links to the official site, and article slugs came along for the ride.
The six extra .mdx hits followed the same shape: |mdx, mdx, _mdx, "mdx. I meant to count file extensions and picked up the character in front of them instead.
You'd catch all of this by reading the output. The moment you hand the list to an agent with "fix all of these," nobody is reading 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
✦Measured regex-vs-literal hit gaps across 979 MDX files, with up to 27% false positives
✦How one delimiter turns a query into 4.5MB of output, or into zero results
✦Three rules for writing search queries as contracts, plus a Python drift harness
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.
The largest gap came from a query containing a delimiter.
Query
Regex (default)
Literal (-F)
ja|en
26,658 lines / ~4.5MB
0 results
As a literal, ja|en is just a string — a plausible way to write a path segment or a locale setting.
As a regex, it means jaoren. Against 979 Japanese MDX files it returned 26,658 lines: 4,564,685 bytes, about 4.5MB.
That fits in no model's context. An agent receiving it will truncate, summarize, or give up — and in every one of those cases it proceeds to the next step wearing the appearance of a successful search.
The literal form returned zero. Zero was the correct answer: the string ja|en does not exist in this repository.
Five characters. One reading gives 4.5MB, the other gives nothing. A single default decides which.
Parens stop you; dots don't
There's a second failure mode with a different character.
Looking for call sites, I tried getArticleContent(:
grep: Unmatched ( or \(
Read as a regex, that's an unclosed group. It errored out.
This is a good failure. It stops. You notice. Add -F and you get the 3 correct results.
The dangerous one doesn't stop. A dot raises no error, returns a plausible count, and slips 13 extra rows into the middle of it.
Failure type
Example
Noticeable?
Syntax error, halts
Unclosed ( or [
Yes — it errors
False positives
. matching any character
No
Inverted meaning
| becoming OR
Only if the count looks absurd
Missed matches
Strings containing $, ^, *
No
My unattended setup only ever watched the right-hand column. Paths that throw errors have retries and logging around them. Paths that drift quietly had nothing.
Errors invite design. Silence doesn't. I hadn't covered this, and there's no honest way to describe it other than a gap.
Writing the query as a contract
So I stopped treating queries as something you improvise, and started writing them as contracts. The same rules now live in the instructions I give agents.
1. Literal by default; regex only on purpose
If you're looking for a string, pass -F. Since forgetting the flag is the whole risk, I inverted the rule: use a regex only when the search genuinely requires one.
Collecting call sites across two functions requires one:
# Two call sites in one pass (a real use for regex)/codesearch 'getArticleContent\(|getArticleBySlug\(' f:src/**/*.ts# A plain string (literal is enough — and should be the default)/codesearch -F 'antigravity.google' f:content/**/*.mdx
Notice the escaped \(. Needing an escape is evidence you meant a regex. Conversely, a regex query containing no escapes at all is almost always a missing -F.
That heuristic has been useful for auditing my own habits. Since adopting it I pause for a beat before hitting enter.
2. Close the search space with f: first
Narrowing the scope shrinks the absolute number of false positives — and brings the output back within range of a human glance.
Measuring premium: on the same repository:
Scope
Matching lines
Whole repository
1,934
f:content/articles/ja/**/*.mdx
955
About 51% disappeared — mostly build artifacts and the English versions, neither of which matters when auditing Japanese frontmatter.
Closing the scope isn't a performance move. It exists to keep the set you hand an agent small enough that a human can still sanity-check it. Nobody reads 1,934 lines. At 955 you can at least judge whether the count is plausible.
3. Decide the expected magnitude in advance
Before running a search, estimate roughly how many hits it should return. If the order of magnitude is off, the query is broken.
That rule alone would have stopped the 26,658 lines instantly. If you're hunting for a locale string and get five digits, you're not hunting for what you think you are.
The agent instruction reads: "If results exceed the expected count by 10x, stop and report the query instead of proceeding." The bound doesn't need to be precise. Catching the order of magnitude is enough.
A small harness for measuring drift
Rules are one thing; compliance is another. So I keep a script that periodically measures the regex-vs-literal gap for the queries I actually use.
#!/usr/bin/env python3"""codesearch_drift.py — measure the gap between the regex default and literal matching./codesearch reads queries as regular expressions by default. This reproducesthose semantics with ripgrep and flags queries whose drift exceeds a threshold.Usage: python3 codesearch_drift.py <repo_root> queries.txt"""import subprocessimport sysfrom pathlib import Path# Warn above this share of excess hitsDRIFT_THRESHOLD = 0.05# Beyond this magnitude, the query is probably brokenVOLUME_CEILING = 5000def count(root: Path, query: str, literal: bool) -> int: """Return matching line count. rg exits 1 on zero matches, so map that to 0.""" cmd = ["rg", "--count-matches", "--no-messages"] if literal: cmd.append("--fixed-strings") cmd += [query, str(root)] proc = subprocess.run(cmd, capture_output=True, text=True) # Do not pipe this: a pipe would swallow the exit code we depend on. if proc.returncode == 1: return 0 if proc.returncode >= 2: return -1 # regex syntax error (the failure that halts) return sum(int(l.rsplit(":", 1)[1]) for l in proc.stdout.splitlines() if ":" in l)def audit(root: Path, queries: list[str]) -> int: problems = 0 for q in queries: re_hits = count(root, q, literal=False) fx_hits = count(root, q, literal=True) if re_hits == -1: print(f"SYNTAX {q!r}: invalid as a regex (should use -F)") problems += 1 continue if re_hits > VOLUME_CEILING: print(f"VOLUME {q!r}: {re_hits:,} lines exceeds ceiling {VOLUME_CEILING:,}") problems += 1 if fx_hits == 0 and re_hits > 0: print(f"INVERT {q!r}: literal 0 / regex {re_hits:,} lines — meaning is inverted") problems += 1 continue if fx_hits > 0: drift = (re_hits - fx_hits) / fx_hits if drift > DRIFT_THRESHOLD: print(f"DRIFT {q!r}: regex {re_hits} / literal {fx_hits} (+{drift * 100:.1f}%)") problems += 1 return problemsdef main() -> int: if len(sys.argv) != 3: print(__doc__) return 2 root = Path(sys.argv[1]) queries = [ line.strip() for line in Path(sys.argv[2]).read_text(encoding="utf-8").splitlines() if line.strip() and not line.startswith("#") ] problems = audit(root, queries) print(f"\nChecked {len(queries)} queries / {problems} need attention") return 1 if problems else 0if __name__ == "__main__": sys.exit(main())
queries.txt holds the searches you reach for daily:
Run against this repository, antigravity.google raises DRIFT and ja|en raises INVERT. Both were queries I had genuinely been running.
One implementation note. I initially piped rg output through head, which swallowed the exit code for syntax errors — so every broken query reported as healthy. Never pipe a command whose exit code you depend on. A harness that fails silently defeats its own purpose.
What belongs in the agent instructions
With rules and a harness in place, the last step is telling the agent. Three lines:
Pass -F when searching for a plain string. A regex query with no escapes counts as a missing flag.
Always close the scope with f:. Never target the whole repository.
If the hit count exceeds the expected magnitude, report the count and query instead of proceeding.
I resisted the urge to write more. Agent instructions get followed less the longer they get.
The same thought surfaced reading another 1.1.3 fix — the one where headless runs (-p) had been silently auto-approving tools that should have asked, now changed to a soft denial that names the required allow rule on stderr.
Where the default doesn't lean toward safety, no amount of instruction text closes the gap. Where the default is safe, short instructions suffice.
The /codesearch regex default isn't a dangerous default. It's a powerful one. But a powerful default is only powerful in the hands of someone who knows it's there.
Where to start
Write down ten queries you run regularly. Check whether any contain a filename with a dot, or a |. If they do, run the harness above and the gap shows up as a number.
Two of my ten did. I'd been running them for the better part of a year without noticing.
The more you delegate to an agent, the more the step before the delegation matters. Search is that first step.
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.