Capability · Code search · updated 2026-09-05

How does each harness find code it has not read yet?

1/7
content searches whose line numbers disagree with their own read tool
an invariant, not a preference
8
orderings decide which results you see
and all but one of these tools caps its results
18/18
say so when the cap bites
the one thing every implementation gets right
Locating files and matching content across a repository. The split between glob-style and grep-style tools, and whether a harness gives one tool or two, shapes how models explore an unfamiliar codebase.

Twenty search tools across eleven harnesses were read from source. Code search is two jobs — finding a file by its name, and finding a string inside one — and almost every harness ships a separate tool for each, so the comparison below is two tables rather than one.

The engine question turns out to be nearly settled: ripgrep, everywhere, in one packaging or another. What is not settled is everything around it — where the line numbers start, what happens when the results run out, and whether the file you are looking for was in scope at all.

The one that is off by one

Qwen Code’s search reports 1-indexed line numbers. Its read_file takes a 0-indexed offset — the schema says so in as many words: “the 0-based line number to start reading from”. A match reported at L42 is at offset 41.

This is the one row on this page where two tools inside the same harness contradict each other, and it is listed as an invariant rather than a design choice. Either base is defensible on its own; the read page makes exactly that argument about offset_base and declines to score it. But a search result is a pointer into a file, and a pointer that does not resolve in the tool it is meant to be handed to is not a preference. The other six harnesses where the question applies use 1-indexed on both sides.

Nothing surfaces this. Both descriptions are accurate in isolation; neither mentions the other; and the failure mode is an agent that reads the line after the one it was looking for and concludes the code is not there.

A search that counts as having read the file

Qwen Code’s grep does one more thing on the way out. Every path it matched is passed to recordGrepResultFileReads, which stats the file and records it in the read cache as a partial read.

That is a defensible cache optimisation and it is also a hole in the read tracking the read page spends most of its length on: a file the model has seen one line of is now, as far as the harness is concerned, a file it has read. It is the only harness here where the interaction was found; in nine other cases it was not verified either way, so this is a lead rather than a comparison. Codex is the one confirmed negative, and only because it has no read tracking to subvert.

Half the answers depend on what is installed on the machine

OpenHands picks its search backend once, at start-up, from whatever is present: ripgrep if it can find it, the system grep binary if not, and a Python os.walk loop if neither. All three are reachable in normal use and nothing in the tool result says which one ran.

Two of this page’s rows change underneath that choice. The regex dialect becomes Rust’s regex crate, POSIX, or Python’s re — so a pattern using lookahead works on one machine and silently returns nothing on the next. And .gitignore stops being respected: ripgrep honours it by default, grep -R and a Python directory walk do not. The same agent, the same repository, the same prompt, and a different set of files in scope.

Qwen Code has the same shape of fallback chain — ripgrep, then git grep, then grep, then an in-process JavaScript scan — with the same consequence for the dialect.

Codex takes this to its conclusion by not shipping a search tool at all. It searches the way it reads: the model types rg or grep or find into exec_command, and every row on this page becomes a property of the machine rather than of the harness. That is a coherent position — the shell is already there and it composes — but it is the only implementation here where a search that found nothing and a search that failed are the same empty string, and the only one with no result cap, so a careless pattern spends the context window before anyone can intervene.

Everyone caps, and everyone says so

Eighteen of the twenty tools limit what they return, and all eighteen announce the limit. That is worth stating plainly because it is the invariant the read page found broken: on read, six of nine will let an agent overwrite a concurrent edit. Here the equivalent question comes back clean. The two exceptions are not failures — Codex has no cap to announce, and Grok Build’s list_dir was not checked.

What the announcements offer differs, and the differences are larger than the fact of announcing. Most name the cap and suggest narrowing the pattern. Kimi Code and Hermes return an offset to page with; pi names the exact argument value that would fetch the rest. MiMo Code reports how many matches are hidden rather than only that some are. dsh does something no one else does: it writes the complete result to a file and hands back the locator, so the part that did not fit is recoverable rather than merely acknowledged — and over the cap its glob can return a sample spread across top-level directories instead of the first hundred paths, on the grounds that the first hundred paths of a large tree are all in the same corner of it.

Grok Build is the one that worries about announcing too much: it reads one line past its budget specifically so a result that exactly fills the cap is not reported as truncated, and where it does truncate it reports “at least” counts, because ripgrep was killed early and the true total is not known.

.gitignore is a ceiling, and dsh refuses it

Thirteen of the eighteen tools that answered respect .gitignore — eight outright, and five with a qualification: an opt-out argument the model can set, an extra ignore file of the harness’s own, or a documented way to override it. Two more respect it only when ripgrep is the backend that happened to be picked, as above, and one leaves it to whatever binary the model invokes. dsh goes the other way on purpose: --no-ignore --hidden are fixed in its argv, VCS metadata is the only exclusion, and the tool description says so to the model in as many words.

Neither is wrong, which is why this is recorded as a design choice. But it is the one setting on this page that decides whether a file is findable at all, and a file the agent cannot find is indistinguishable, from inside the model, from a file that does not exist.

Two tools, or one

Eighteen of the twenty are half of a pair: a glob tool and a grep tool, and the model has to know which job it is doing before it has looked at anything. Hermes ships one search_files with a target argument instead, which lets the model defer that choice by one turn. Codex ships neither.

Whether that matters is not something this page can answer — it is a question about tool-selection behaviour, which needs a model in the loop and a run, not a reading of the source. It is on the list.

The same code, one line apart

MiMo Code’s search tools are a fork of opencode’s, close enough that whole functions are identical. They disagree on one line: MiMo sorts matches by modification time before applying the cap, opencode returns ripgrep’s traversal order untouched.

Neither ordering is better. But both cap at a hundred, so the line decides which matches the model never sees — and on a repository where the hundred-and-first match is the one that matters, the two harnesses give different answers to the same question while running what is recognisably the same code. Eight distinct orderings appear across this page, which makes ordering the least discussed and most consequential row on it.

What is not here

Five tools across three harnesses register a code-search tool that has not been read from source, including both of Claude Code’s, which publishes no tool source to read. They are named under the candidate table rather than shown as empty rows.

Two rows are thin even for the harnesses that were read. Case sensitivity was confirmed for six of twenty: three let the model choose and default to case-sensitive, two hardcode case-insensitive with no way to ask for anything else, and one inherits whatever the shell was given. The binary guard was confirmed for three. Those cells say ?, which means nobody checked, not that the behaviour is missing.

Two corrections came out of this pass. OpenClaw’s find and grep were missing from the inventory entirely, which had it recorded as searching through its shell; it does not. And MiMo Code’s codesearch was filed here on the strength of its name — it calls Exa over HTTP and returns library documentation from the public internet, never touching the repository, so it has moved to web search.

Candidates

25 tools · 20 read from source
OwnerToolSearcheschoiceRegex dialectchoiceAnnounces truncationinvariantAgrees with readinvariantGitignorechoiceSource
Codexexec_commandshellsrcwhatever is on the machinesrc——whatever is on the machinesrcrepo
dshglobby-namesrc—holdssrc—no, deliberatelysrcrepo
dshgrepby-contentsrcRust regexsrcholdssrcholdssrcno, deliberatelysrcrepo
Grok Buildgrepby-contentsrcRust regexsrcholdssrc?yes, model can overridesrcrepo
Grok Buildlist_dirby-namesrc—?—?repo
Hermessearch_filesbothsrcRust regexsrcholdssrcholdssrc?repo
Kimi Codeglobby-namesrc—holdssrc—yes, unless include_ignoredsrcrepo
Kimi Codegrepby-contentsrcRust regexsrcholdssrcholdssrcyes, unless include_ignoredsrcrepo
MiMo Codeglobby-namesrc—holdssrc—yessrcrepo
MiMo Codegrepby-contentsrcRust regexsrcholdssrcholdssrcyessrcrepo
OpenClawfindby-namesrc—holdssrc—yessrcrepo
OpenClawgrepby-contentsrcRust regexsrcholdssrc?yessrcrepo
opencodeglobby-namesrc—holdssrc—yessrcrepo
opencodegrepby-contentsrcRust regexsrcholdssrcholdssrcyessrcrepo
OpenHandsglobby-namesrc—holdssrc—varies by backend: yes under ripgrep, no under the Python fallbacksrcrepo
OpenHandsgrepby-contentsrcvaries by backend: Rust regex, POSIX, or Python resrcholdssrc—varies by backend: yes under ripgrep, no otherwisesrcrepo
pifindby-namesrc—holdssrc—yessrcrepo
pigrepby-contentsrcRust regexsrcholdssrcholdssrcyessrcrepo
Qwen Codeglobby-namesrc—holdssrc—yes, plus .qwenignoresrcrepo

Present but not verified. Another 5 tools, registered by 3 harnesses, have not been read from source: Claude Code Glob, Claude Code Grep, OpenClaw exec, ZCode Glob, ZCode Grep. They are left out of the comparison rather than shown as empty rows — a blank cell would claim the behaviour is absent, which is a different thing from not having checked.

Full comparison

every cell links to where it was read
Finding files by name
DimensionCodexexec_commanddshglobGrok Buildlist_dirHermessearch_filesKimi CodeglobMiMo CodeglobOpenClawfindopencodeglobOpenHandsglobpifindQwen Codeglob
Shape
What it searchesshellsrcby-namesrcby-namesrcbothsrcby-namesrcby-namesrcby-namesrcby-namesrcby-namesrcby-namesrcby-namesrc
Tools per harnessno search toolsrctwo toolssrctwo toolssrcone toolsrctwo toolssrctwo toolssrctwo toolssrctwo toolssrctwo toolssrctwo toolssrctwo toolssrc
Search enginewhatever is on the machinesrcpackaged ripgrep --filessrcin-process directory walksrcripgrepsrcripgrep --filessrcbundled ripgrepsrc?bundled ripgrep --filessrcripgrep --files, else Python globsrcbundled fdsrcin-process glob traversalsrc
Query language
Glob syntaxwhatever is on the machinesrcripgrep --globsrc—?ripgrep --globsrcripgrep --globsrc?ripgrep --globsrcripgrep -g, or Python glob with an implicit **/srcfd --globsrcglob library patternssrc
Case sensitivitywhatever is on the machinesrc?—????????
Result shape
Result capnonesrc100 results, rest spilled to a filesrccharacter budgetsrc50 results, paginatedsrc100 matchessrc100 filessrc1000 results or 30KBsrc100 filessrc100 filessrc1000 results or 30KBsrc100 shown, 1000 scannedsrc
Announces truncationinvariant—holdssrc?holdssrcholdssrcholdssrcholdssrcholdssrcholdssrcholdssrcholdssrc
How "no matches" readsempty stdoutsrcsentencesrc??sentencesrcsentencesrcsentencesrcsentencesrcsentencesrcsentencesrcsentencesrc
Scope
Respects .gitignorewhatever is on the machinesrcno, deliberatelysrc??yes, unless include_ignoredsrcyessrcyessrcyessrcvaries by backend: yes under ripgrep, no under the Python fallbacksrcyessrcyes, plus .qwenignoresrc
Searches hidden fileswhatever is on the machinesrcyessrc??yessrcyes, except .gitsrc?no unless askedsrcnosrcyessrc?
Ordering
Result orderingwhatever is on the machinesrcmodification timesrcpath, case-insensitivesrcbounded discovery order; newest-first on requestsrcmodification time, newest firstsrcmodification time, newest firstsrc?ripgrep traversal ordersrcmodification time, newest firstsrc?recent files by mtime, then the rest alphabeticallysrc
Searching file contents
DimensionCodexexec_commanddshgrepGrok BuildgrepHermessearch_filesKimi CodegrepMiMo CodegrepOpenClawgrepopencodegrepOpenHandsgreppigrepQwen Codegrep_search
Shape
What it searchesshellsrcby-contentsrcby-contentsrcbothsrcby-contentsrcby-contentsrcby-contentsrcby-contentsrcby-contentsrcby-contentsrcby-contentsrc
Tools per harnessno search toolsrctwo toolssrctwo toolssrcone toolsrctwo toolssrctwo toolssrctwo toolssrctwo toolssrctwo toolssrctwo toolssrctwo toolssrc
Search enginewhatever is on the machinesrcpackaged ripgrep, via --jsonsrcbundled ripgrepsrcripgrepsrcripgrepsrcbundled ripgrepsrcbundled ripgrepsrcbundled ripgrepsrcripgrep, else system grep, else Python resrcbundled ripgrepsrcripgrep, else git grep, else grep, else JS regexsrc
Query language
Regex dialectwhatever is on the machinesrcRust regexsrcRust regexsrcRust regexsrcRust regexsrcRust regexsrcRust regexsrcRust regexsrcvaries by backend: Rust regex, POSIX, or Python resrcRust regexsrcvaries by backend: Rust regex, git grep, POSIX, or JS RegExpsrc
Case sensitivitywhatever is on the machinesrc?sensitive, model can asksrc???sensitive, model can asksrc?always insensitive, not configurablesrcsensitive, model can asksrcalways insensitive, not configurablesrc
Result shape
Line numbers in resultsonly if the model askssrcLine N:srcN: under a path headingsrcline numbers, or paths only, or countssrcon by defaultsrcLine N:srcpath:N:srcLine N:srcnone — file paths onlysrcpath:N:srcLN: and path:line:textsrc
Result capnonesrc250 matches inline, rest spilled to a filesrchead limit, mode-dependentsrc50 results, paginatedsrc250 lines, paginatedsrc100 matchessrc100 matches or 30KBsrc100 matchessrc100 filessrc100 matches or 30KBsrcline and character limits, both announcedsrc
Announces truncationinvariant—holdssrcholdssrcholdssrcholdssrcholdssrcholdssrcholdssrcholdssrcholdssrcholdssrc
How "no matches" readsempty stdoutsrcsentencesrc??sentencesrcsentencesrcsentencesrcsentencesrcsentencesrcsentencesrcsentencesrc
Agreement with read
Search line N is read line Ninvariant—holdssrc?holdssrcholdssrcholdssrc?holdssrc—holdssrcfailssrc
Search counts as having readno read tracking at allsrc?????????yes — matched files are recorded as partial readssrc
Scope
Respects .gitignorewhatever is on the machinesrcno, deliberatelysrcyes, model can overridesrc?yes, unless include_ignoredsrcyessrcyessrcyessrcvaries by backend: yes under ripgrep, no otherwisesrcyessrcyes, plus .qwenignoresrc
Searches hidden fileswhatever is on the machinesrcyessrc??yessrcyes, except .gitsrcyessrcyes, except .gitsrcnosrcyessrc?
Binary content never enters contextinvariantfailssrcholdssrc??????holdssrc??
Ordering
Result orderingwhatever is on the machinesrc?ripgrep traversal ordersrcbounded discovery order; newest-first on requestsrcfiles by modification time, newest firstsrcmodification time, newest firstsrcripgrep traversal ordersrcripgrep traversal ordersrcmodification time, newest firstsrcripgrep traversal ordersrc?

Rows marked invariant are things every implementation should hold to, so they render as pass/fail. Everything else is a design choice: differing is recorded, never scored. ? means we could not verify it; — means the question does not apply.