Newsletter, guides
ripgrep for coding agents: A fast, safe code search tool
Give your coding agent a ripgrep search tool with fixed flags, JSON output, capped results and exit code handling, plus the tool spec the model sees.
Give your coding agent ripgrep (rg) as its code search tool, not grep -r. ripgrep skips files listed in .gitignore, hidden files and binary files by default, so the model sees source code instead of node_modules, .git and minified bundles. Wrap it in a small function that fixes the flags, asks for --json output, resolves the path inside the workspace and caps the results. The model only chooses the pattern and an optional file type.
That is also how the big agents do it. Claude Code’s own Grep tool is built on ripgrep. This guide gives you the wrapper, the flags worth fixing, the tool description the model reads, and the mistakes that show up in the first week.
Key takeaways
- Use
rginstead ofgrep -r: it respects.gitignoreand skips hidden and binary files without any extra flags. - Ask for
--jsonand parse thematchevents. Do not regex-parsefile:line:textoutput. - Fix the flags in the wrapper and expose only
pattern,file_typeandliteralto the model. - Cap matches per file with
--max-countand cap the total in your code, then tell the model when the cap fired. - Treat exit code 1 as “no matches”, not as an error, and return exit code 2’s message so the model can fix its pattern.
- Pass the pattern with
-e(or after--) so a pattern that starts with a dash is never read as a flag.
Why should a coding agent use ripgrep instead of grep?
Because grep -r searches everything under the directory, and in a real repository most of that is noise. Dependencies, build output, lockfiles, the .git folder and binary assets all match. The model gets thousands of lines back, most of them irrelevant, and spends its context reading them.
ripgrep was built for searching code. By default it:
- Respects
.gitignore,.ignoreand.rgignorefiles, so ignored folders such asnode_modulesanddistare skipped. - Skips hidden files and directories.
- Skips binary files.
- Searches recursively, in parallel, with no flags needed.
It is also fast, which matters more in an agent than for a person. An agent may search many times in one task, and every slow search is time the whole loop waits.
Which ripgrep flags belong in an agent’s search tool?
A handful, set once in the wrapper. These are the ones I fix for every agent:
| Flag | What it does for the agent |
|---|---|
--json |
One JSON object per line: begin, match, context, end and summary. |
--max-count 3 |
At most 3 matching lines per file, so one file cannot fill the result. |
-C 2 |
Two lines of context around each match, which helps the model judge it. |
--type py |
Search one language only. rg --type-list shows the built-in types. |
-F |
Fixed-string search, for names like foo() that contain regex characters. |
Leave the rest out of the model’s reach. Models pick flags badly and waste turns debugging their own options.
How do you wrap ripgrep as a safe agent tool?
Run rg with a token list and a fixed flag set, parse the JSON events, and return a short structured result. Here is the full wrapper.
# filename: rg_tool.py
# description: A ripgrep search tool for a coding agent. Fixed flags, a path
# guard, JSON parsing, capped results and exit code handling.
import json
import subprocess
from pathlib import Path
WORKSPACE = Path('/work/repo').resolve()
MAX_RESULTS = 50
FLAGS = ['--json', '--max-count', '3', '-C', '2']
MAX_LINE = 300 # characters kept per matching line
def code_search(pattern: str, path: str = '.', file_type: str | None = None,
literal: bool = False) -> dict:
target = (WORKSPACE / path).resolve()
if not target.is_relative_to(WORKSPACE):
return {'ok': False, 'error': f'path escapes workspace: {path}'}
cmd = ['rg', *FLAGS]
if literal:
cmd.append('-F')
if file_type:
cmd += ['--type', file_type]
cmd += ['-e', pattern, str(target)] # -e: a pattern like "-v" stays a pattern
try:
run = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
except subprocess.TimeoutExpired:
return {'ok': False, 'error': 'search timed out, narrow the path or pattern'}
if run.returncode == 1:
return {'ok': True, 'matches': [], 'note': 'no matches'}
if run.returncode == 2 and not run.stdout:
return {'ok': False, 'error': run.stderr.strip()[:500]}
matches, truncated = [], False
for line in run.stdout.splitlines():
event = json.loads(line)
if event['type'] != 'match':
continue
if len(matches) == MAX_RESULTS:
truncated = True
break
data = event['data']
matches.append({
'path': str(Path(data['path']['text']).relative_to(WORKSPACE)),
'line': data['line_number'],
'text': data['lines'].get('text', '').rstrip('\n')[:MAX_LINE],
})
result = {'ok': True, 'matches': matches}
if truncated:
result['note'] = f'over {MAX_RESULTS} matches, use a more specific pattern'
return result
Three details carry most of the safety. The path is resolved and checked against the workspace, so ../../ cannot leave the repository. The command is a token list, never a shell string, so the pattern cannot chain another command. And -e pattern tells ripgrep the next argument is the pattern, so a model that searches for -rf does not pass a flag by accident.
The exit codes follow grep’s convention: 0 means matches were found, 1 means none, 2 means an error. The man page says exit code 2 also covers soft errors, such as a file that could not be read while others matched, which is why the wrapper only treats it as a failure when there is no output. A “no matches” result is useful information for the model. Returning it as an error makes the model think the tool is broken.
A match event’s lines field holds text for normal UTF-8 lines, and bytes (base64) for lines that are not valid UTF-8. The .get('text', '') keeps those rare lines from crashing the tool.
The wrapper also cuts each line to 300 characters itself. ripgrep has a --max-columns flag that omits long lines in its normal output, but in my tests on ripgrep 14 it did not shorten lines in --json output, so a minified bundle that matched came back whole. Trim in your code.
For context lines, read the context events the same way and attach them to the nearest match. Many agents skip them and let the model open the file instead.
What should the tool description tell the model?
What the tool searches, what it skips, the pattern syntax, one example, and what to do when there are too many results. The example does more work than any amount of flag documentation, because models copy it.
# filename: tool_spec.py
# description: The tool definition the model sees.
CODE_SEARCH_TOOL = {
'name': 'code_search',
'description': (
'Search file contents in the repository with ripgrep. Skips gitignored, '
'hidden and binary files. pattern is a Rust regex; set literal=true to '
'match the exact text (use it for names like foo() or a.b[0]). '
'file_type limits the search to one language, for example "py" or "ts". '
'Returns up to 50 matches. Example: pattern="def validate_token", '
'file_type="py". If the note says there are too many matches, search again '
'with a more specific pattern or path.'
),
'input_schema': {
'type': 'object',
'properties': {
'pattern': {'type': 'string', 'description': 'Regex, or exact text if literal is true'},
'path': {'type': 'string', 'description': 'Folder inside the repo, default "."'},
'file_type': {'type': 'string', 'description': 'Optional language, e.g. "py"'},
'literal': {'type': 'boolean', 'description': 'Match the exact text, no regex'},
},
'required': ['pattern'],
},
}
Name the regex flavour. ripgrep uses Rust regex syntax, not POSIX grep. Claude Code’s docs make the same point about its Grep tool: to find interface{} in Go you search for interface\{\}. A literal switch removes most of those escaping mistakes.
What goes wrong when you first give an agent ripgrep?
Four things, in roughly this order.
- The wrong directory. The agent process starts somewhere other than the repository, and
rgsearches the wrong tree or the whole container. Always pass an explicit path resolved against the workspace, as the wrapper does. Never rely on the process’s current directory. - Regex characters in names. The model searches for
getUser(and gets a regex error, or searches fora.band matchesaxb. Offerliteraland mention it in the description. - Too much output. A pattern such as
.matches every line in the repository. The per-file--max-countplus the total cap keep that bounded. Test it on purpose. - Noise that is not gitignored. Vendored code, fixtures and generated files are often committed. Add a fixed list of exclude globs in the wrapper, such as
--glob '!tests/fixtures/**', or put them in a.rgignorefile. Do not let the model write globs.
ripgrep only reads files. That makes it one of the safest tools you can give an agent, but it still runs as a subprocess on your machine. Run it with the same limits as your other tools. The bash tool guide covers timeouts, output caps and sandboxing for any command an agent runs, and the coding agent loop guide shows where a search tool sits in the loop.
What to do next
- Install ripgrep in the agent’s runtime. On Debian or Ubuntu images that is
apt-get install -y ripgrep. Checkrg --versionin the container, not on your laptop. - Replace your grep tool with
code_searchabove. Keep the flags fixed. - Add the tool description with one example, and name the regex flavour.
- Test four cases on purpose: no matches, a pattern of
., a pattern that starts with-, and a path with../. - Commit a
.rgignorefor fixtures and generated code that are not gitignored. - See search, file editing and bash wired into one coding agent in the free autonomous Claude Code agent session.
Frequently asked questions
Why should coding agents use ripgrep instead of grep?
ripgrep skips gitignored, hidden and binary files by default, so search results contain source code instead of dependencies and build output. It also offers JSON output that is easy to parse, and it is fast. Each of those saves context and time inside an agent loop, where the model may search many times per task.
Does Claude Code use ripgrep?
Yes. Claude Code’s tools reference says its Grep tool is built on ripgrep and uses ripgrep’s regex syntax, not POSIX grep. Like ripgrep, it skips gitignored files. When ripgrep rejects a pattern, Claude Code returns ripgrep’s error so Claude can fix the pattern and search again.
What ripgrep flags should an agent’s search tool use?
Use --json for structured output, --max-count to cap matches per file and -C 2 for context, and cut long lines in your own code. Allow --type for a language filter and -F for literal searches. Fix these in the wrapper and let the model set only the pattern, path, file type and literal switch.
What do ripgrep’s exit codes mean?
Exit code 0 means at least one match was found, 1 means no matches, and 2 means an error, such as an invalid regex. An agent tool should return “no matches” as a normal result for exit code 1, and pass the error message back to the model for exit code 2 so it can correct the pattern.
How do you stop an agent search from returning too many results?
Cap matches per file with --max-count, cap the total number of matches in your wrapper, and cut each line to a few hundred characters. When the cap fires, add a note telling the model to use a more specific pattern or path. Test it with a pattern of ., which matches every line.
Sources
- ripgrep user guide, BurntSushi/ripgrep, October 2026: automatic filtering of gitignored, hidden and binary files, file types, and the
.rgignorefile. - ripgrep changelog, BurntSushi/ripgrep, October 2026: 15.0.0 in October 2025 and 15.2.0 on July 15, 2026, the current release.
- Tools reference: Grep tool behavior, Anthropic Claude Code docs, October 2026: Grep is built on ripgrep, uses its regex syntax, skips gitignored files and returns ripgrep’s errors.
- rg man page template, BurntSushi/ripgrep, October 2026: exit status 0 for a match, 1 for no match, 2 for any error, including soft errors such as an unreadable file.