devtools
guide
06 Oct 2026
Seven code search tools, twelve planted files, and the defaults that never looked
ripgrep found five of twelve. GNU grep found nine. The gap is not accuracy, it is which files each tool decided were worth opening, and every one of those decisions has an off switch.
By Purvansh Parmar
Checked 2026-10-06
Read 8 min
Entries 7
No matches is the most trusted line a terminal prints. You grep for a token, nothing comes back, and you write the ticket saying the token is gone. That inference is wrong for six of the seven tools below, because every one of them narrows the file set before it reads a byte, and none of them tells you what it skipped. I planted one string in twelve places inside a small repo and ran each tool the way a person actually types it. The best default missed three of the twelve. The worst missed eight.
How this was checkedBuilt a 12 trap repo with one unique token planted in a gitignored build dir, a dotfile, a hidden dir, node_modules, an unknown extension, a file with a NUL byte, a UTF-16LE file, a 2 MB minified line, a symlinked dir, an untracked file and a gzip member. Ran seven search tools with the command you would actually type. Default recall ranged from 4 of 12 to 9 of 12, and only ripgrep and ugrep could be argued up to 12.
What I compared on
- default recall: traps found with the shortest command that searches a tree, nothing added
- ceiling: traps found once every switch the tool offers is turned on
- whether the miss is recoverable at all, or whether the tool simply has no flag for it
- wall clock on a 255 MB tree where 12,000 of 13,501 files sit in a gitignored node_modules
The one already installed, with no opinion about your repository layout.
Wins
The question is whether a string exists anywhere under this path, and you want the answer uncontaminated by someone else's idea of which directories are boring. Nothing here is hidden from you by default.
Loses
It takes 177 ms where ripgrep takes 9, and on the binary file it prints binary file matches with no line and no column, which tells you a file is interesting but not why.
9 of 12 default10 of 12 with -Ra177 ms on 255 MB
A grep-compatible searcher that reads archives and transcodes encodings.
Wins
You need one tool that can be talked into reading everything, including the inside of a tarball, without a second pass or a pipeline. Its scan rate is the highest here: 255 MB in 54 ms, against ripgrep's 41 ms for the same bytes under -uuu.
Loses
Four separate flags to open the four doors, and the one for hidden files is a bare dot, which nobody remembers. The ugrep 7.x packages also want glibc 2.38, so on Ubuntu 22.04 I had to build it from source.
8 of 12 default12 of 12 with -R -. -z54 ms on 255 MB3,305 stars, 8 open issues
A Go searcher that respects .gitignore only when you ask it to with --git.
Wins
It is the only tool here that printed the reason for a skip. An error on stderr naming --blocksize beats silence, and silence is what the other six gave me every single time they walked past a file.
Loses
The 262,144 byte line limit is a real default that bit on real minified JS, and no amount of flags gets it to the UTF-16 file. At 1,654 stars with 45 open issues, you are also betting on a small project.
8 of 12 default10 of 12 max262,144 byte line cap62 ms on 255 MB
A single-file Perl script that filters by a built-in table of file types.
Wins
Zero install beyond a curl and a chmod, which is the whole pitch, and it ran on a box where the ugrep deb would not load. Its ignore list is auditable in one command, so you can see exactly what it decided not to read.
Loses
No flag turns binary scanning on, so the NUL byte file is permanently invisible, and the only way to reach the minified file is --ignore-ack-defaults, which discards the type table you came for. 109 ms, slowest of the seven after grep.
6 of 12 default9 of 12 max28 default ignore-dir rules109 ms on 255 MB
Searches the index, which means tracked files and nothing else.
Wins
You want the question scoped to code that is actually committed, which is the right scope for a refactor and the only scope that matters in CI. It is also six times faster than ripgrep because the index already told it the file list.
Loses
A leaked credential lives in an untracked .env.local roughly always, and that is the one file git grep structurally cannot see. --untracked --no-exclude-standard fixes it and nobody types that twice.
5 of 12 default9 of 12 max6 ms on 255 MB0 untracked files read
Rust searcher that applies .gitignore, hidden-file and binary filters before reading.
Wins
The filters are stacked on one escalating flag, so -u drops gitignore, -uu adds hidden files, -uuu adds binaries. One letter, repeated, in the order you usually want them. That is the best interface in this list by a distance.
Loses
The default is the most aggressive filter of the seven, and it is silent about it. Three of its five misses were files a person would obviously want searched, and the 9 ms headline is mostly the 237 MB it never opened.
5 of 12 default12 of 12 with -uuu -z -L9 ms default41 ms under -uuu
The C searcher that made gitignore-aware search normal, now mostly coasting.
Wins
It is in every distro's repository under a name people already know, and it reads the minified line that sift refuses. If a box has ag and nothing else, 18 ms for the common case is still fine.
Loses
Last pushed in June 2024 with 564 open issues against 27,125 stars, and -u costs it 8x: 18 ms becomes 149 ms. Everything it does, ripgrep does with better defaults and a smaller gap beween fast and complete.
4 of 12 default10 of 12 max18 ms default149 ms under -u564 open issues
Side by side
| Default | Ceiling | 255 MB tree | Cannot reach |
| GNU grep 3.7 | 9 / 12 | 10 / 12 | 177 ms | UTF-16, archives |
| ugrep 7.8.5 | 8 / 12 | 12 / 12 | 54 ms | nothing |
| sift 0.9.2 | 8 / 12 | 10 / 12 | 62 ms | UTF-16, archives |
| ack 3.9.0 | 6 / 12 | 9 / 12 | 109 ms | binary, UTF-16, archives |
| git grep 2.34.1 | 5 / 12 | 9 / 12 | 6 ms | symlinks, UTF-16, archives |
| ripgrep 15.2.0 | 5 / 12 | 12 / 12 | 9 ms | nothing |
| ag 2.2.0 | 4 / 12 | 10 / 12 | 18 ms | UTF-16, archives |
Verdict
Install ripgrep 15.2.0. Then learn two commands rather than one, because the tool is answering two different questions and only one of them suits the defaults. Plain rg for where is this defined: its filters are right nearly every time there, and the 9 ms is real. rg -uuu -z -L for is this string anywhere under here, which earns its own muscle memory. ugrep ties at 12 of 12 and scans faster per byte, but four unrelated flags lose to one letter typed three times. What has to stop is reading a silent rg as proof. On this rig that reading was wrong seven times out of twelve.
Sources