Large codebases punish slow exploration. GUI search tools lag on million-line monorepos; memorizing every directory is unrealistic. Three command-line utilities—ripgrep (rg), fd, and jq—form a fast, composable workflow for finding text, locating files, and extracting structure from JSON.
Together they replace much of what developers do with nested find, grep, and ad hoc scripting—while respecting .gitignore by default and staying friendly to scripts and CI.
This guide explains each tool's role, how they chain together, and patterns for navigating unfamiliar repositories without claiming you need to learn every flag on day one.
Why these three tools
| Tool | Job | Replaces |
|------|-----|----------|
| ripgrep (rg) | Search file contents by regex | grep -R, IDE search |
| fd | Find files and directories by name | find, slow tree walks |
| jq | Query and transform JSON | Python one-liners, manual parsing |
All three are single binaries, cross-platform, and designed for scripting. They share sensible defaults: ignore hidden clutter, respect gitignore, parallelize work.
Install via your package manager (brew install ripgrep fd jq on macOS, analogous packages on Linux). Pin versions in devcontainer or CI images for reproducibility.
ripgrep: search contents fast
rg PATTERN searches recursively from the current directory, skipping ignored paths.
First moves in a new repo
```bash
What is the entry point?rg "main\\(|bootstrap|createApp" -g '*.{ts,js,go,rs}'
Where is configuration loaded?rg "process\\.env|config\\.|settings" -g '!node_modules'
Find TODO markers left by the teamrg "TODO|FIXME|HACK" --heading
```
Flags worth learning
-g GLOB: Include or exclude paths (-g '*.go',-g '!test/**')--type: Built-in file type filters (--type ts,rg --type-add 'config:*.config.js')-l: List files with matches only-c: Count matches per file-n: Line numbers (default in terminal output)-C 3: Context lines around matches-i: Case insensitive--json: Machine-readable output for tooling-F: Fixed string (no regex) when searching literals with special characters
Literal searches and escaping
Searching for foo.bar as regex matches any character between foo and bar. Use rg -F 'foo.bar' or escape dots.
Multiline and structural search
rg is line-oriented by default. For multiline patterns, -U --multiline-dotall helps, but AST-aware tools (tree-sitter, language-specific LSP) beat regex for structural queries. Use rg to narrow files first, then refine in an editor.
Integration with editors
Most editors bind "project search" to ripgrep under the hood. Knowing CLI flags translates directly to faster editor search configuration.
fd: find files by name
fd PATTERN finds paths by substring (regex by default; -g for glob).
Common patterns
```bash
All TypeScript files under srcfd -e ts . src
Config files anywherefd 'config|wrangler|docker' -t f
Empty directories (cleanup audits)fd -t d --empty
Recently modified (requires fd with --changed-within on some versions)fd . -t f --changed-within 2d
```
Flags worth learning
-t f/-t d: Files or directories only-e ext: Extension filter (can repeat)-H: Include hidden files-I: Do not respect gitignore (use sparingly)-x: Execute command per result (fd -e md -x wc -l)
fd is not a full replacement for find -mtime or complex boolean logic, but covers most "where is this file?" tasks faster.
jq: slice JSON pipelines
API responses, package-lock.json, Terraform plans, and CI artifacts are JSON. jq filters and maps JSON on stdin.
Basics
```bash
Pretty-printcurl -s https://api.example.com/data | jq .
Extract a fieldjq '.name' package.json
Array mapjq '.dependencies | keys[]' package.json
Select objects matching a conditionjq '.[] | select(.status == "failed")' results.json
```
Combining with ripgrep
Search logs saved as JSON lines:
``bash``
rg '"level":"error"' app.log | jq '{time: .timestamp, msg: .message}'
Or find JSON files then query:
``bash``
fd -e json . config | xargs -I{} sh -c 'echo "==> {}"; jq ".version" {}'
Practical jq patterns for codebases
Inspect workspace package list (monorepo):
``bash``
jq -r '.workspaces.packages[]?' package.json 2>/dev/null
List npm scripts:
``bash``
jq -r '.scripts | to_entries[] | "\(.key): \(.value)"' package.json
Filter large lockfile for one package:
``bash``
jq '.. | objects | select(.name? == "lodash")' package-lock.json
Use -r for raw string output without quotes. Use --slurp or -s when input is JSON lines.
Chaining the workflow
A typical exploration session on an unfamiliar repository:
1. Orient by manifest files
``bash``
fd '(README|CONTRIBUTING|package\\.json|Cargo\\.toml|go\\.mod)' -t f
Read README and manifest for build commands and layout.
2. Locate domain concepts
``bash``
rg -n "UserService|authenticate|/api/v1" --heading
Note directories that repeat in results—that is often the module boundary.
3. Narrow file set
``bash``
fd -e ts . src/services/auth
rg "class.*Service" src/services/auth
4. Parse machine output
``bash``
rg --json "deprecated" | jq -r '.data.path.text + ":" + (.data.line_number|tostring)'
Produces path:line for quick jumping (some tools consume this format).
5. Document findings
Save commands in a scratch note or exploration script—future you (or teammates) reuse the same paths.
Performance habits
- Scope directories: Search
src/not the whole disk - Use
-gexclusions:-g '!{dist,build,coverage}' - Prefer
rg -lthen read: List files first when matches are dense - Avoid piping huge binary:
rgskips binaries by default; do not override without reason - xargs parallelism:
fd ... -x rg -H 'pattern' {}for per-file search when needed
On very large trees, ensure you are not searching node_modules or vendor dirs—defaults usually exclude via gitignore; verify with rg --debug if results look slow.
Git-aware exploration
Both rg and fd respect .gitignore. For untracked generated files you need to search, pass -u / --unrestricted (rg) or -I (fd)—then narrow with globs to avoid noise.
Search only tracked files in a pinch:
``bash``
git ls-files '*.ts' | xargs rg 'pattern'
Useful when local build artifacts are not gitignored correctly.
When to reach for something else
| Need | Better tool |
|------|-------------|
| Symbol definition across languages | LSP, ctags, scip |
| Call graph, refactor | IDE, specialized static analysis |
| Semantic code search | Code indexing services, ast-grep |
| Binary or PDF content | Not rg/fd/jq |
| Interactive fuzzy pick | fzf piped with fd or rg -l |
Add fzf (fd . | fzf) for interactive selection; it complements but does not replace scripted workflows.
Safety and scripting
- Quote patterns in shell scripts; prefer arrays over string concatenation
fd -xandxargscan run many commands—test withechofirstjqprograms can error on null—use?optional operator:.foo?.bar- Do not pipe untrusted JSON into
jqwith--rawfileexecution tricks; treat data as data
CI and automation
The same commands work in CI for guardrails:
```bash
Fail if forbidden import appearsrg -q "from 'lodash/fp'" src && exit 1 || true
Ensure all packages have license fieldfd package.json | while read -r f; do jq -e '.license' "$f" >/dev/null || echo "missing license: $f"; done
```
Keep checks fast: narrow globs, cache dependencies, run only on changed paths when possible.
Worked example: tracing an HTTP handler
Suppose you need to find where POST /api/orders is handled in an unknown service-oriented repo.
- Search route registration:
``bash``
rg -n "(/api/orders|orders.*post|router\\.(post|route))" -i -g '!node_modules'
- List likely handler files:
``bash``
fd -e 'ts|go|py' handler
fd -e 'ts|go|py' orders
- Follow imports from a hit file (example TypeScript):
``bash``
rg -n "from ['\\\"].*orders" src/
- If the service returns JSON error shapes, inspect OpenAPI or fixture files:
``bash``
fd openapi -e yaml -e json
jq '.paths["/api/orders"].post' openapi.json
- Check tests for expected behavior (often faster than reading implementation):
``bash``
rg -n "POST.orders|/api/orders" -g 'test' -g 'spec*'
This sequence typically locates the handler, its validation layer, and integration tests in minutes without opening every directory tree manually.
Aliases and shell ergonomics
Short aliases reduce friction enough that you actually use the tools:
``bash``
alias rg='rg --smart-case'
alias rgf='rg --files-with-matches'
Combine fd with fzf for interactive file open:
``bash``
vim "$(fd -e ts | fzf)"
Store project-specific search snippets in scripts/explore.sh or documented Makefile targets so onboarding engineers inherit the same commands.
Limitations
Regex search does not understand syntax. Renamed symbols break text search—supplement with language-aware tools for refactors.
jq loads entire JSON into memory for standard filters. Multi-gigabyte JSON needs streaming tools (jq --stream) or different formats.
Cross-platform shell differences (macOS BSD vs GNU) affect xargs and sed—prefer tool-native flags over fragile pipes.
Mastering these three tools is not about memorizing every flag—it is about a default loop: fd to locate, rg to search, jq to structure. Repeat until the codebase stops feeling opaque.
Summary
ripgrep finds text in code at speed. fd finds files by name without verbose find syntax. jq extracts fields from JSON outputs and config. Chained together, they form a practical workflow for large codebases: orient from manifests, search concepts, narrow paths, parse structured output. Add an IDE and language server for semantics—but keep this trio in muscle memory for every repo you do not yet know.
