Grep Before Read
Reading a whole file to find one function is the small-model version of cat | grep. rg -n goes straight to the lines
that matter, returns five orders of magnitude less text, and costs almost no context. Make it your default discovery
move. read is for when you already know which region to look at.
When to grep first
Trigger rg -n (preferred) or grep -rn as the FIRST move when any of these is true:
- You're looking for a symbol: function name, class, const, type, CLI flag, env var.
- The user asks "where", "who uses", "all callers of", "find the bug in".
- You're in an unfamiliar repo and don't know the file layout yet.
- The file is over ~400 lines (the pi
read-without-limit-nudgewill call this out anyway). - You're doing a repo-wide refactor (rename, migration, API change).
Skip grep and read directly only when:
- The file was handed to you by path AND is short (≤ ~300 lines).
- You've already grepped once and know the exact line range you want.
- You're reading a well-known config you can't easily grep (
.env,tsconfig.json,package.json).
Recipes
Pick the first one that describes your goal. They're ordered by how often they solve the problem.
1. Find where a symbol is defined
# Function / class / const definition (ripgrep respects .gitignore).
rg -n "^(export\s+)?(function|class|const|interface|type)\s+MySymbol\b"
# Fallback if rg isn't available.
grep -rn -E "^(export\s+)?(function|class|const|interface|type)\s+MySymbol\b" .
Why: anchoring at the line start skips every import and call site, leaving only declarations.
2. Find all call sites / references
# Every mention, with 2 lines of context.
rg -nC 2 "MySymbol"
# Restrict to a language / path.
rg -n --type ts "MySymbol"
rg -n -g 'src/**/*.ts' "MySymbol"
Then read path --offset <line> --limit 40 on the most promising hits.
3. Search only changed / untracked files
# Things you're currently working on.
git diff --name-only | xargs rg -n "MySymbol"
# Same but including untracked.
git ls-files --modified --others --exclude-standard | xargs rg -n "MySymbol"
4. Exclude noise (build output, vendored code, tests)
rg -n "MySymbol" \
-g '!dist/**' -g '!node_modules/**' -g '!external/**' -g '!*.min.*'
# Only tests:
rg -n "MySymbol" -g '**/*.spec.*' -g '**/*.test.*'
5. Find a specific text phrase (error message, TODO, FIXME)
rg -n "Cannot read properties of undefined"
rg -n "TODO|FIXME|XXX"
Fixed strings (no regex) are faster and avoid surprises with . or [:
rg -Fn "some.literal.string[with](chars)"
6. Count occurrences before deciding to read
rg -c "MySymbol" # per-file counts
rg -c "MySymbol" | sort -t: -k2 -n -r | head
A file with 40 hits is probably where MySymbol lives; a file with 1 is probably an import.
7. List files that match / don't match
rg -l "MySymbol" # files that contain
rg --files-without-match "MySymbol" # the inverse
Before / after
Before (small-model pattern):
read src/api/search.ts→ 1,842 lines, ~22k tokens of output.read src/api/validation.ts→ 900 lines, ~11k tokens.read src/lib/ratelimit.ts→ 600 lines, ~7k tokens.- "I found the handler at line 412 of search.ts."
Total spend: ~40k tokens to answer one "where is X" question.
After:
rg -n "searchHandler" -g '!dist/**'→ 6 hits, ~200 tokens.read src/api/search.ts --offset 405 --limit 40→ ~600 tokens.- "
searchHandleris defined at search.ts:412, called from 3 places."
Total spend: ~800 tokens. Same answer.
After you find the target
Once rg points you at a specific file and line range:
read <path> --offset <line> --limit <small_window>- pull only the region you need.- Record the location in
scratchpad("searchHandler@ src/api/search.ts:412") so you don't re-grep next turn. - Only widen the
limitor drop it entirely if the first window was obviously too small.
Anti-patterns
- Don't
reada file over 400 lines withoutoffset/limit. Theread-without-limit-nudgeextension will point this out anyway; pre-empt it. - Don't grep the same pattern twice in a turn. If the first
rgresult was useful, note it inscratchpad; if it was useless, adjust the pattern (add anchors, restrict paths, escape metacharacters) - don't rerun unchanged. - Don't
rg | head-pattern in pi.rgalready caps its own output, and piping can mask errors. If the result set is huge, tighten the regex or add-gglobs. - Don't reach for
find -name X | xargs grep.rgalready walks the tree respecting.gitignore, andrg -g 'glob'handles the name filter in one process.
Quick reference
| Goal | Command |
|---|---|
| Symbol definition | rg -n "^(export\s+)?(function|class|const|interface|type)\s+NAME\b" |
| All references | rg -nC 2 "NAME" |
| Restrict by type | rg -n --type ts "NAME" |
| Restrict by path | rg -n -g 'src/**/*.ts' "NAME" |
| Exclude paths | rg -n "NAME" -g '!dist/**' -g '!node_modules/**' |
| Fixed string | rg -Fn "literal.string" |
| Files containing | rg -l "NAME" |
| Per-file counts | rg -c "NAME" |
| Only modified files | git diff --name-only | xargs rg -n "NAME" |