Semlith

Claude Code doesn't index your codebase

Claude Code finds code with grep and find, not an index. Where that works, where it misses, and what an index changes, measured on SWE-bench Verified.

Checked on 29 October 2026 against Claude Code’s docs and changelog up to version 2.1.312, with semlith 0.49.0.

What Claude Code does today

Claude Code keeps no index of your code. Anthropic’s own words are that it “doesn’t require a codebase index to be built, maintained, or uploaded to a server” (Claude blog, May 2026). When it needs to find something it runs a search, reads what comes back, and searches again.

On macOS, Linux and WSL it searches with grep and find through its shell. Since version 2.1.117 those are embedded copies of ugrep and bfs, and the separate Grep and Glob tools are off by default there (tools reference, changelog). On Windows it keeps its Grep tool, which is built on ripgrep. Either way it opens files with its Read tool.

This was a choice, not a gap. Boris Cherny, who created Claude Code, has said the early versions used RAG with a local vector database (X, February 2026) and that searching like an agent did better for the way people use it (Hacker News, February 2025). His reasons were that an index drifts out of date, has to be stored somewhere safe and adds moving parts. The cost he named was latency and tokens (Latent Space, May 2025).

Those are good reasons, and grep is very good at what it does. If you know the name of the thing you want, nothing beats it.

Where grep misses

grep matches words. The agent has to guess the word the code uses, and when the question is asked in other words the first search lands somewhere else.

Here is one question asked both ways on Flask 3.1.3, a public repository. The question is how to limit how big an uploaded request body can be.

git clone --depth 1 --branch 3.1.3 https://github.com/pallets/flask && cd flask
rg -n -i "upload|big" src
Output
src/flask/helpers.py:542:        @app.route("/uploads/<path:name>")
src/flask/helpers.py:545:                app.config['UPLOAD_FOLDER'], name, as_attachment=True
src/flask/helpers.py:549:    files or uploads. Uses :func:`~werkzeug.security.safe_join` to
src/flask/cli.py:297:    a bigger role.  Typically it's created automatically by the

None of these lines is the answer. The limit is max_content_length in src/flask/wrappers.py, and that file never says “upload” or “big”. An agent gets there in the end by trying more words and opening more files, and that is where the latency and the tokens go.

The same question against an index of the repository.

semlith index .
semlith search -k 1 "limit how big an uploaded request body can be"
Output
1. 0.087 vf  src/flask/wrappers.py:59-71
       @property
       def max_content_length(self) -> int | None:
           """The maximum number of bytes that will be read during this request. If
           this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge`
           error is raised. If it is set to ``None``, no limit is enforced at the
           Flask application level. However, if it is ``None`` and the request has
           no ``Content-Length`` header and the WSGI server does not indicate that
           it terminates the stream, then no data is read to avoid an infinite
           stream.
   
           Each request defaults to the :data:`MAX_CONTENT_LENGTH` config, which
           defaults to ``None``. It can be set on a specific ``request`` to apply
           the limit to that specific view. This should be set appropriately based

1 hits in 602.316209ms
question-shaped · vector and keyword equal

Semlith ranks by meaning and by keywords together, which is what vf on the hit says (both lists found it). Once you have the name, grep is the right tool again.

rg -n max_content_length src
Output
src/flask/wrappers.py:55:    _max_content_length: int | None = None
src/flask/wrappers.py:60:    def max_content_length(self) -> int | None:
src/flask/wrappers.py:80:        if self._max_content_length is not None:
src/flask/wrappers.py:81:            return self._max_content_length
src/flask/wrappers.py:84:            return super().max_content_length
src/flask/wrappers.py:88:    @max_content_length.setter
src/flask/wrappers.py:89:    def max_content_length(self, value: int | None) -> None:
src/flask/wrappers.py:90:        self._max_content_length = value

So an index doesn’t replace grep. It finds the name, and grep or a read takes it from there.

What it changes, measured

We measured this on SWE-bench Verified, 500 real GitHub issues. The issue text is the question and the answer is every file the fix edits. It measures retrieval only, not whether an agent then fixes the bug.

The file the fix edits is in Semlith’s top 10 for 76.8% of the issues, against 47.0% for grep then read. Before it reaches the first right file, grep then read goes through a median of 15,491 tokens, and Semlith 224.

Finding the file is not the whole job, though. On 50 questions about a codebase it had never seen, headless Claude Code on Opus answered 92.0% correctly with grep alone, and 92.0% with Semlith installed beside it. On one repository a strong agent gets to the answer with grep. It just reads more on the way. When Claude Code was set to ask Semlith before it greps, it answered 96.0%.

Every figure here is on the benchmarks page, and the scorecard has the harness and the command behind each one, so you can run it yourself.

When an index is worth it

Grep alone is fine when the repository is small, when you know the names, or when the agent is fixing something it can already see. An index starts to pay when questions come in words the code doesn’t use, when there are many repositories to look through, and when the answer sits in a PDF, Word or Excel file that grep can’t read at all (Search code and documents together).

Anthropic’s guide for large codebases says the same. If you already run a code search or RAG index, it suggests exposing it to Claude Code as an MCP tool (large codebases).

Try it with Claude Code

cargo install semlith
semlith setup
semlith index .

semlith setup registers Semlith in Claude Code and in the other agent clients it finds. Connect Claude Code shows what it writes, and how to add it by hand with claude mcp add. Claude Code keeps its own grep and reads, and gains semlith_search beside them.