Comparison

rf does not replace ripgrep or fd. It wraps their engines and reports what a naive one-shot invocation silently drops.

On a synthetic corpus with the term planted in seven files behind different default filters:

Invocation Files found (of 7) Exit on the same absent term
rg pattern 1 1 (looks like an error)
rg -uu pattern 4 1
rg -uu -a pattern 5 1
rf content pattern 7, each attributed 0 with data: []

Against a shell pipe

For “find DB_DSN in *.config” with five real matches spread across a hidden file, a gitignored file, and a binary file:

  • fd -e config | xargs rg DB_DSN finds 1 of 5 (and prints a stray “binary file matches” line to stderr).
  • rf find DB_DSN . --name config finds 5 of 5, each attributed to its stage, and correctly leaves a true-negative *.config unflagged.

A pipe cannot attribute a miss to a stage: it erases fd’s exit code and conflates ripgrep’s “no files” with “no match.”

At scale

Run against a real working tree (a CPython checkout with compiled .pyc artifacts gitignored, as a normal build leaves them), naive rg misses a large fraction of real matches that rf recovers and attributes. Two measured terms:

Term naive rg rf content Recovered
unittest 1167 2220 +1053
deprecated 497 655 +158

rf is also more precise than raw rg -uu here: rf never treats .git internals as content, while rg -uu descends into .git/ and emits false positives from commit messages and reflogs.

Numbers are from specific corpora and will vary with the tree. The point is not a fixed multiplier — it is that a naive search can miss most of the matches while still exiting 0, and rf tells you exactly what it missed and why.

When to reach for what

  • Interactive grepping you trust your eyes on — plain rg/fd are perfect.
  • A search whose result a program will act on — rf, because “exit 0” from a naive search is not proof the tree was fully searched.