I asked an agent to list every place a constant was used. It came back with two files. Running git grep by hand turned up six.
The agent had not cut corners. The search defaults simply never put the other four files in scope.
Antigravity CLI 1.1.21, released on August 26, now ships a ripgrep binary for agent code search. Not depending on whatever happens to be installed on the machine is a real improvement. What bundling does not change is the set of things ripgrep declines to look at by default.
A file the search never opens is, from the agent's point of view, a file that does not exist. It will happily tell you the list is complete. I find that silent gap more costly than a slow search, and it is much harder to notice.
Reproducing a six-into-two search
Here is the smallest repository I could build that shows the behaviour. The same constant, NEEDLE_TOKEN, goes into six files. Three of them land somewhere .gitignore covers, and one goes into a hidden directory.
mkdir -p src/generated src/legacy .config build
echo 'const NEEDLE_TOKEN = 1;' > src/app.ts
echo 'const NEEDLE_TOKEN = 2;' > src/generated/api-client.ts
echo 'const NEEDLE_TOKEN = 3;' > src/legacy/old.ts
echo 'const NEEDLE_TOKEN = 4;' > .config/settings.ts
echo 'const NEEDLE_TOKEN = 5;' > build/bundle.js
printf 'NEEDLE_TOKEN\x00binary\n' > src/blob.bin
printf 'build/\n*.bin\nsrc/generated/\n' > .gitignore
# Ignored by pattern, tracked anyway. A very common arrangement.
git add -f src/generated/api-client.ts
git add -A -f && git commit -m initThe git add -f line is the interesting part. Keeping generated code out of .gitignore's reach while force-tracking one specific artifact is a normal thing to do. Working solo as an indie developer, I carried a generated API client in exactly that shape for a long stretch, without once thinking about what it did to search.
Three searches over the same commit:
git grep -l NEEDLE_TOKEN | wc -l # 6
rg -l NEEDLE_TOKEN . | wc -l # 2
rg -l -uuu NEEDLE_TOKEN . | wc -l # 6git grep returns all six tracked files. ripgrep's defaults return src/app.ts and src/legacy/old.ts. Four files that Git is version-controlling sit outside the search.
These numbers come from ripgrep 13.0.0 on Linux. The bundled binary will not necessarily report the same version as your system rg, but the default filters described below behave the same way across that lineage.
Three separate exits, not one
The four missing files did not disappear for the same reason.
| File | Why it dropped out | Flag that restores it |
|---|---|---|
| build/bundle.js | build/ in .gitignore | --no-ignore |
| src/generated/api-client.ts | src/generated/ in .gitignore, tracked or not | --no-ignore |
| .config/settings.ts | Hidden directory | --hidden |
| src/blob.bin | Contains a NUL byte, treated as binary | --binary or --text |
Adding the flags one at a time walks the count from two up to six:
rg -l NEEDLE_TOKEN . # 2
rg -l --no-ignore NEEDLE_TOKEN . # 4 (+ generated, + build)
rg -l -uu NEEDLE_TOKEN . # 5 (+ .config)
rg -l -uuu NEEDLE_TOKEN . # 6 (+ blob.bin)The second line is the one that surprised me. ripgrep never asks whether a file is tracked. It only asks whether the path matches an ignore pattern. A file you force-added is a first-class citizen to Git and an ignored path to ripgrep, and nothing in either tool's output points out that the two disagree.
Binary detection reports itself inconsistently, which makes it worse. During a directory walk, a binary file is dropped in silence. Name the file directly and you get an explanation:
rg NEEDLE_TOKEN src/blob.bin
# binary file matches (found "\0" byte around offset 12)You never see that line for files skipped during traversal. "No matches" and "never opened" produce identical output.
The scope can differ from machine to machine
This is the part that is hardest to catch, because the cause is not in the repository at all. .gitignore is not the only ignore source ripgrep honours.
echo 'src/legacy/' >> .git/info/exclude
rg -l NEEDLE_TOKEN . # now only src/app.ts.git/info/exclude never gets committed. It exists only in your clone. The global core.excludesFile setting behaves the same way and applies across every repository on the machine.
So two people on the same commit can hand their agents different search scopes. Solo development is not exempt either: if a desktop and a laptop have drifted apart, the same prompt produces different answers on each. When someone reports that a symbol is findable on their machine but not yours, this is now on the list of things to check.
--debug names the rule that did it
There is no need to guess. Passing --debug prints one line per exclusion decision.
rg --debug --files . 2>&1 | grep "ignore::walk"The output looks like this:
ignoring ./build: Ignore(IgnoreMatch(Gitignore(Glob { from: Some("./.gitignore"),
original: "build/", actual: "**/build", is_whitelist: false, is_only_dir: true })))
ignoring ./.config: Ignore(IgnoreMatch(Hidden))
ignoring ./src/blob.bin: Ignore(IgnoreMatch(Gitignore(Glob { from: Some("./.gitignore"),
original: "*.bin", actual: "**/*.bin", is_whitelist: false, is_only_dir: false })))from tells you which file supplied the pattern, and hidden paths are tagged separately as Hidden. When .git/info/exclude is responsible, its path shows up in from, which is how you catch a rule that was never shared with anyone.
For a summary rather than a list, pipe the tail into a counter:
rg --debug --files . 2>&1 | grep "ignore::walk" \
| sed -E 's/.*IgnoreMatch\(([A-Za-z]+).*/\1/' | sort | uniq -c | sort -rnWidening the scope, temporarily and permanently
For a one-off investigation, -uu or -uuu is enough. Asking an agent to remember the flags is not a plan, though. It holds for the session you wrote it in and quietly lapses in the next one.
For files you want back permanently, put a negation pattern in an .ignore file. ripgrep gives .ignore precedence over .gitignore.
printf '!src/generated/\n' > .ignore
rg -l NEEDLE_TOKEN . # 3 now (app / legacy / generated).ignore can be committed, so the decision travels with the repository. You can hand back exactly the generated code you want an agent to read and nothing else.
One caveat I ran into: writing !*.bin into .ignore does not bring the binary back. The two exclusions live in different layers, and .ignore only reaches the ignore-rules layer. Binary content needs --binary. In practice there are very few reasons to want an agent reading binaries, so leaving that one shut seems right to me.
If what you actually want is "everything Git tracks, no more and no less", run git grep alongside. It keys off tracking status directly, so how .gitignore happens to be written stops mattering.
One line that shows you the gap
Whether the two scopes have drifted apart is a single command:
comm -23 <(git ls-files -z | tr '\0' '\n' | sort) \
<(rg --files | sed 's|^\./||' | sort)Anything Git tracks that ripgrep does not walk gets printed. An empty result means the scopes agree.
The -z matters. The repository behind this site contains src/app/[locale]/HomeClient.tsx, and plain git ls-files emits that path quoted and escaped, which manufactures one phantom difference. Setting core.quotePath=false does not fix it either, which I confirmed rather than assumed. Switching the separator to NUL makes the two lists directly comparable.
Here is what the command reports against the repository that serves this site:
| Measurement | Value |
|---|---|
| Files tracked by Git | 2,264 |
Files returned by rg --files | 2,252 |
| Difference | 12 (.gitignore, .npmrc, ten per-category .gitkeep files) |
| Exclusion reasons | All Hidden |
I measured this with node_modules absent, so nothing dropped out through .gitignore here. The only losses were hidden files, none of which contain searchable code. No harm done in this case. A repository that tracks generated code will behave like the synthetic example instead.
I also checked what the extra flags cost. Across 2,252 files: 20 ms for the defaults, 17 ms with -uu, 20 ms with -uuu, and 20 ms for git grep. At this size the differences are noise. Scope is not a decision you need to trade against speed yet.
A slow search announces itself. A search that skipped half the codebase does not. That asymmetry is the reason I now keep the comm line in the same checklist as my build and test commands. If you want it running from inside the editor, I covered that loop in building a verification loop with Antigravity 2.10.0's embedded terminal.
The related failure mode, where the search engine itself swaps out underneath you, is measured separately in what happens when /codesearch cannot run ripgrep. If your exclusion patterns are the suspect rather than the defaults, four places to check when .antigravityignore has no effect walks through that.
For the wider question of how much context to hand an agent and what it costs, how to pass a read-only .git to an agent documents cutting the same history transfer from 38 seconds to 0.4.
Start by running that comm line against your own repository. The number of lines it prints tells you how much of your code your agent is currently able to see.