grep, ripgrep, ast-grep, and what AI coding agents actually need
grep is excellent. ripgrep made it faster. ast-grep made it structural. None of them are the right primitive when an AI coding agent is doing the searching. Here's why — and what to use instead.
Open any AI coding agent's transcript and one tool dominates the
log: grep. Or its faster cousin rg. The agent
greps at the start of a turn, in the middle, after every file it
reads. It's the default move.
That makes sense — grep is a fantastic tool. It's been refined for 50 years, runs on every machine, and answers a real question quickly: "where does this string appear?" If you're a human poking through a codebase, it's almost always the right first move.
The problem isn't grep. The problem is that grep was designed for humans who can read context. AI coding agents can't, not in the same way. When the same primitive that works for a developer at the terminal is fed to a model that needs ranked, typed, deterministic context, it falls over in expensive ways.
This post is about what grep (and ripgrep, and ast-grep) do well, where they fall over for agents, and what to use instead.
What grep does well
Three things grep is genuinely the best tool for:
- Exact-text matching. If you know the literal string you're looking for — an error message, a config key, a magic number — grep finds every instance with no false negatives.
- Speed. ripgrep can churn through a 100k-file monorepo in under a second. Performance was a solved problem before most current language tooling existed.
- Ubiquity. No setup, no index, no daemon. Works on a fresh checkout, works on a server you SSH'd into, works in CI.
Those properties matter. They're why grep is the first thing every agent reaches for. They're also why the answer isn't "agents shouldn't search" — they should. The question is what they're asking grep to do that grep was never built for.
The four things grep can't do
1. It can't separate signal from noise
Search for verifySession in a real codebase. You get
matches in:
$ rg -n "verifySession" .
lib/auth/dal.ts:142: return verifySession(req)
lib/auth/dal.test.ts:18: it("verifySession returns null on bad token", ...
docs/auth.md:5: The `verifySession` helper is the single...
node_modules/.pnpm/some-package/dist/types.d.ts:42: ...
test/__mocks__/lib/auth/dal.ts:3: export const verifySession = vi.fn();
.git/objects/pack/pack-1234.idx (binary file matches) Six matches. Two are useful — the source and the test. Four are noise. The agent has no idea which is which until it reads each one. That's 4 wasted turns and several thousand tokens of file content piped through context.
2. It can't follow imports
You renamed an exported function. Who depends on it? grep finds the literal string but misses:
- Re-exports through barrels:
export { verifySession } from "./dal" - Default-export imports:
import auth from "./dal"; auth.verifySession(...) - Dynamic imports:
const { verifySession } = await import("./dal") - Generated client SDKs that re-export from your source
Your AI agent does what it can — it greps for variations, hopes for the best. The result is a refactor that "passes all tests" because the broken callers were behind a barrel re-export and grep never saw them.
3. It can't reason about routes, schemas, or graphs
"What handler responds to POST /api/manager/onboarding?"
is not a string-matching question. The route file might be at
app/api/manager/onboarding/route.ts if you use Next.js
App Router, or pages/api/manager/onboarding.ts for the
older router, or wired through a custom dispatcher in
server/router.ts that pattern-matches paths.
grep finds the literal path string. It doesn't find the
handler. To get the handler, you need to know the framework's
routing convention. To get the file the handler imports its
business logic from, you need the import graph. To get the database
tables that handler touches, you need to parse .from(...)
calls and resolve them against your schema.
None of that is grep's job, and it shouldn't be.
4. It can't tell you what's current
You ran a lint check three turns ago and got a list of findings. Are those findings still accurate after the edits you made since? grep has no concept of state. It re-runs from scratch every time. The agent has to ask you, or re-run the lint, or guess.
For a human, "I just edited this file, the old findings might be stale" is obvious. For an agent dumping grep output into context, every result is unlabeled — fresh, stale, contradicted, all the same.
ripgrep and ast-grep partly help
The community has built two excellent grep evolutions, both worth knowing:
ripgrep (rg) respects .gitignore and
common ignore patterns by default, and it's faster than vanilla grep.
That fixes the noise problem on the
node_modules-and-.git axis. It does not fix
the rest.
ast-grep (and similar tools like
tree-grepper, semgrep) match on the abstract
syntax tree, not text. Asking for verifySession($ARG)
returns actual call sites, not text matches in comments and strings.
That fixes problem #1 within the file, but not the import graph,
routing, schema, or freshness problems.
If you're a human searching a codebase, ripgrep + ast-grep is a phenomenal upgrade. If you're an AI agent, you've fixed maybe 30% of the cost.
What an agent actually needs
The thing an agent needs isn't a better grep. It's a search surface that returns typed graph nodes instead of text matches. Each result should be:
- Ranked — the most likely answer first, with a reason, not a flat list of every line that matched.
- Typed — a function declaration vs. a function call vs. a comment vs. a test fixture. The agent doesn't need to guess.
- Linked — every result carries its place in the import graph, route table, or schema, so the agent can traverse one hop without another search.
- Fresh-labeled — every row says whether it's current, indexed-but-stale, or contradicted by a newer fact.
None of that is exotic. It's the same model your IDE uses to implement "Go to Definition" or "Find All References." The trick is exposing it as a tool an MCP server can hand to an agent.
Concrete: grep vs the typed alternatives
Here's the same question asked five different ways. Same codebase. Same target. Different cost and clarity.
"Where is the auth callback for the manager onboarding route?"
With grep:
$ rg -n "manager/onboarding" .
$ rg -n "verifySession" lib/
$ ls app/api/manager
$ cat app/api/manager/onboarding/route.ts
$ cat lib/auth/dal.ts
$ rg -n "resolveManager" lib/
# 6 turns, ~5,000 tokens of output, agent assembles a guess With auth_path:
auth_path({ route: "/api/manager/onboarding", verb: "POST" })
→ {
guards: ["lib/auth/dal.ts:resolveManagerScope:142"],
routeFile: "app/api/manager/onboarding/route.ts",
tablesTouched: ["manager_district", "onboarding_state"],
rlsState: "enforced",
}
# 1 turn, ~400 tokens, fully typed "Who imports this file?"
With grep:
$ rg -n "from ['\"].*queries['\"]" .
$ rg -n "import .* queries" .
$ rg -n "require.*queries" .
# misses barrel re-exports, dynamic imports, default exports,
# and generated SDK files With imports_impact:
imports_impact({ filePath: "lib/users/queries.ts" })
→ 47 dependents, including:
- 11 via barrel: lib/users/index.ts
- 3 via generated SDK: packages/sdk/users.ts
- 33 direct named imports
# 1 turn, includes everything grep would miss "Where does this database column appear in app code?"
With grep:
$ rg -n "billing_status" .
# string matches in code, comments, generated types,
# tests, and committed SQL — 80+ hits, mostly noise With schema_usage:
schema_usage({ schema: "public", object: "organizations.billing_status" })
→ {
reads: [".from('organizations').select('...billing_status')", 4 sites],
writes: [".update({ billing_status: 'active' })", 2 sites],
rpc_refs: ["set_billing_active() function"],
}
# 1 turn, distinguishes reads from writes, finds RPC references "Find every place this AST pattern appears"
With grep: can't really do this. ast-grep can.
With ast_find_pattern:
ast_find_pattern({
pattern: "supabase.from($TABLE).delete()",
languages: ["ts", "tsx"]
})
→ 6 matches, with file paths, line ranges, and ackable fingerprints
# wraps ast-grep with the typed graph layer on top "What's relevant to this task?"
This one grep can't answer at all. The shape of the question is "give me ranked context for this work."
With context_packet:
context_packet({ request: "debug failing manager role check" })
→ {
primaryContext: [/* top 3 files */],
activeFindings: [/* prior reviews */],
risks: ["touches RLS-sensitive admin_audit_log"],
recommendedHarnessPattern: "read primary → check finding → edit"
}
# 1 turn, ~600 tokens, agent goes straight to relevant code Why typed tools win programmatically
The throughline across all five examples isn't "grep is bad." It's that the agent is doing four kinds of work, and grep happens to be mediocre at three of them:
- "Find this string" — grep wins. Use it.
- "Find this code structure" — ast-grep wins.
- "Find what depends on this" — typed import graph wins.
- "Find what's relevant to this task" — typed context engine wins.
For a human, the cost of using the wrong tool is mild — you just look at the output and ignore the noise. For an agent, every wrong tool means context-window pressure, more turns, and more frontier-model tokens. The cost compounds.
That's why programmatic context tools beat grep specifically for AI agents:
- Typed results. The agent doesn't burn tokens guessing what each result means. The schema is the answer.
- Bounded output. A typed tool returns a known shape. grep can return 80,000 lines if the codebase is wrong-sized.
- Composable. One tool's output is another tool's
input.
route_traceoutput flows directly intodb_rlswithout parsing. - Cacheable. A grep run can't be cached meaningfully. A typed graph query against a fresh index can.
When you should still grep
Don't replace grep. There are real cases where it's the right tool:
- Exact text after edits. The index might be
stale; grep reads disk. Use
live_text_searchfor the same job from inside an agent — it's just ripgrep with a typed result envelope. - Searching unindexed files. Generated code, vendored binaries, log files — anything outside the AST/symbol index. grep doesn't care.
- Finding hard-coded strings. Magic numbers, config keys, error codes that aren't symbols. grep is the right shape for these.
- One-off ad-hoc questions. If you're at the
terminal, your hand reaches for
rgautomatically and it'll be correct 90% of the time.
The rule: humans grep, agents query. The same person can do both at different times.
What this looks like in practice
You don't have to migrate your whole workflow. The simplest move:
- Wire an MCP server that exposes typed search tools (agentmako or similar).
- Add a one-line rule to your
CLAUDE.md/AGENTS.md: "Before grepping, callcontext_packetorcross_search. Use grep only for exact strings or unindexed files." - Watch the next session's transcript. The agent's opening moves
shift from
rg ...tocontext_packet. Token count per turn drops by ~70%.
It's a one-line CLAUDE.md change. The cost of trying it is essentially zero. If it doesn't help on your codebase, the rule is one line to remove.
The takeaway
grep, ripgrep, and ast-grep are excellent tools. They're not the right primitive for an AI agent navigating a real codebase. Typed graph queries — file dependency graphs, route maps, schema-aware lookups, ranked context packets — produce smaller, structured, cacheable answers that compose into multi-step agent workflows without burning your token budget.
The shift isn't from grep-the-tool to no-grep. It's from grep-as-the-default-action to grep-as-the-fallback. Your agent should reach for typed tools first, and grep when the typed tools say "this isn't in the index."
That's exactly what agentmako ships. An MCP server
that gives Claude Code, Cursor, Codex, and Cline a typed search
surface — context_packet, cross_search,
imports_impact, route_trace,
schema_usage, auth_path — alongside a
live_text_search for when grep is still the right answer.
Local-first, Apache-2.0, no telemetry. Try it for one session and read
your own transcript to see the shift.
Want this for your codebase?
agentmako is local-first, Apache-2.0, and works with every MCP-compatible coding agent.