← writing
published

Code Search for Agents: Zoekt on EKS

2026-0711 min readagentscode-searchzoekteksefsmemex

In plain terms — A coding agent cannot make good changes if it can see only one corner of the company. This story explains how more than 120 repositories became one private, fast search engine that returns only the useful matching lines. It is Ctrl+F across the whole organization, giving an agent awareness before granting it more autonomy.


The autonomy-before-awareness trap

There is a tempting order for building a coding agent. First, give it permission to edit files. Then let it run commands, open pull requests, or drive a longer workflow. Add more autonomy whenever it gets stuck.

I think that order starts too late in the problem.

An agent working in one checkout can inspect that checkout. It cannot see the other repositories that define the same deployment convention, consume the API it is changing, or contain the shared module it should reuse. If I ask it where a symbol is used across an organization, it can only answer from what happens to be on disk. It may grep thoroughly and still have poor recall.

That produces a recognizable kind of flailing. The agent searches one tree, forms a plausible theory, opens large files looking for context, and proposes a local implementation. The work can look disciplined at every step while missing the repository that would have changed the conclusion. More permission does not solve that problem. It only lets the agent act more decisively on an incomplete view.

This was the problem I wanted to solve on Memex, Auction.com's internal AI coding-agent platform. Before asking an agent to do more, I wanted to improve what it could know. The target was simple to state: one tool should search the organization's mirrored repositories and return the useful matching lines, with enough context to choose the next step.

The resulting system uses Zoekt on EKS, a shared EFS volume, nightly indexing, and a deliberately narrow agent-facing connector. The infrastructure matters, but the larger lesson is about tool design. Codebase awareness is not a prompt feature. It is a retrieval system with boundaries, freshness, failure modes, and a token budget.

What the agent actually needs

Local grep is excellent when the search space is already local. That qualification is the problem in a multi-repository organization. Searching a single clone cannot find a call site in a repository that was never cloned. Cloning every repository for every agent session would turn retrieval into setup work, consume disk, and still leave me responsible for keeping all of those copies current.

The agent also does not need every file containing a term. A file list is an intermediate result that forces another round of reads. Returning every full file is worse: broad queries can fill the context window with material the model will immediately discard. The useful unit is a match—the relevant lines, their line numbers, a small amount of surrounding context, and the repository and file that contain them.

That distinction shaped the requirements:

  1. Search all mirrored repositories through one interface.
  2. Keep source and its index inside the existing environment.
  3. Accept filters in forms a person or model will naturally produce.
  4. Return small, attributable slices rather than whole files.
  5. Bound query time and result size so one broad search cannot consume the entire agent context.

This is why I chose an indexed service instead of trying to make per-checkout search more elaborate.

The Zoekt and EFS architecture

Zoekt is a trigram-based code-search engine. Instead of scanning every repository linearly for each query, it builds an index designed for fast regular-expression search. That makes it a good fit for questions whose scope is not known in advance: a symbol, configuration key, file pattern, or repeated implementation can be searched across the mirrored organization at once.

The git mirrors and the index live on shared EFS storage. The volume is mounted at the same path on the hosts and Kubernetes workloads that participate in the system, so the indexer and query service agree on where repositories live. The shared mount also separates the durable search corpus from any one pod. A webserver restart does not require the repositories or index to be rebuilt on that pod's ephemeral filesystem.

A nightly Kubernetes CronJob refreshes the index over more than 120 mirrored repositories. That makes freshness explicit. This is not a claim that every query observes a just-pushed commit; the architecture provides a nightly view. For organization-wide discovery and pattern finding, that trade keeps the pipeline straightforward and the serving path read-only.

The query tier is a Zoekt webserver running on an arm64 EKS node pool. An nginx sidecar fronts it and owns the surrounding routing and authentication concerns, while Zoekt serves search. The service remains internal, as do the mirrors and the derived index. That perimeter was one reason to self-host instead of sending proprietary source to a hosted search service.

flowchart LR
    Mirrors[Git mirrors] --> EFS[Shared EFS volume]
    Cron[Nightly indexer] --> EFS
    EFS --> Zoekt[arm64 Zoekt webserver]
    Nginx[nginx sidecar] --> Zoekt
    Agent[Memex agent] --> Connector[Code search connector]
    Connector --> Nginx

The diagram is intentionally small because the responsibilities are small. The nightly job builds over the mirrors. EFS holds the shared corpus and index. The webserver queries it. Nginx handles the service boundary. The connector turns that service into an interface an agent can use safely.

Why not grep all the clones

The obvious alternative was to enumerate repositories and run grep against each one. It has the advantage of familiar tools, but it puts the wrong work on the request path. Every search pays for walking every eligible file. Every agent also needs access to every clone, plus a synchronization strategy for those clones. The search interface becomes coupled to repository distribution.

Zoekt moves that repeated work into indexing. The nightly job pays the cost of building the searchable representation; queries reuse it. EFS gives the indexer and server the same durable view without baking the corpus into the webserver image or copying it into each pod.

There is an operational tradeoff here. An index is another artifact to build, store, and refresh. A nightly schedule creates a known freshness window. I prefer that explicit contract to a tool that appears live but quietly searches only the repositories available in its current workspace.

The other rejected direction was hosted code search. The technical interface could have been similar, but the data boundary would not have been. With the self-hosted path, proprietary repositories and the index derived from them stay inside the environment where the agents already run.

Put the translation at the tool boundary

Exposing Zoekt directly would still leave the model with avoidable work. Zoekt accepts regular expressions, while people commonly describe file filters as globs. A user will write *.yaml; an agent will do the same unless the prompt teaches it a search-engine dialect.

I made the connector absorb that mismatch. It accepts a file glob and converts it into an anchored regular expression before assembling the Zoekt query. In conceptual terms, * becomes any sequence, ? becomes one character, literal dots are escaped, and the expression is anchored at the end. The implementation is proprietary, but the design rule is portable: accept the syntax callers already reach for and translate it at the boundary.

Repository and file filters are optional first-class inputs. The connector adds the corresponding constraints to the raw search expression. The agent can therefore start with a broad organization-wide question, narrow to a repository, or limit a query to a family of files without learning how Zoekt composes those filters.

This is more than convenience. Tool calls are generated inputs. Every piece of engine-specific syntax I require from the model is another place for a well-intentioned call to be malformed. Normalizing common inputs in code makes the behavior consistent across prompts and models. It also keeps the tool description focused on intent: what to search, where to search, and which files matter.

Return matches, not files

The connector exposes two operations: search the index and list the indexed repositories. The search response is not a passthrough of Zoekt's JSON and not a collection of full file bodies. It is reformatted into compact blocks grouped by repository and file. Each block preserves line numbers, marks the lines that actually matched, and includes a small amount of context around them.

The defaults are intentionally bounded: two context lines and at most 30 file matches. Search has a 30-second timeout; listing repositories has a 10-second timeout. Those numbers are not performance claims. They are limits on how long the connector will wait and how much evidence one call can return.

Bounds change how an agent investigates. A broad query becomes a discovery step, not an accidental request to load the codebase. If 30 matching files are too many, the agent can add a repository or file filter. If two surrounding lines are not enough, it can make a narrower follow-up request. Context expands in response to uncertainty rather than arriving all at once.

The formatter also isolates response quirks from the model. The source material records variant field names for file and line matches, plus base64-encoded match content. The connector accepts the known response shapes, decodes content, and falls back safely if decoding fails. It removes the internal mirror prefix from repository names before presenting them. What the agent sees is a stable, compact search result rather than the storage layout and transport encoding of the service behind it.

The same principle applies to errors. The connector bounds its HTTP calls, distinguishes non-success and non-JSON responses, and reports which configured service it attempted to reach. The useful error is not a shell stack trace. It is a short statement that tells the agent whether retrying, checking configuration, or choosing another path makes sense.

What changed when the agent could search the organization

I do not have recorded query-volume, latency-percentile, or adoption metrics for this system, so I will not manufacture an impact chart. The defensible change is in the agent's available workflow.

Before organization-wide search, repository discovery had to happen outside the reasoning loop. A person needed to know which repository to clone, or the agent had to operate within the accidental boundary of its current checkout. After the connector was available, discovery became a read-only tool call. The agent could ask where a symbol, configuration pattern, or implementation appeared before deciding which repository deserved deeper inspection.

The evidence also became more precise. A response could name a repository, file, and line range while carrying only the matching chunk. That is enough to form a better next question: inspect this file, compare these implementations, or narrow the search. It is not enough to authorize a change by itself, and it should not be. Search improves situational awareness; it does not replace code review, tests, or human judgment.

The repository-list operation matters for the same reason. It lets the agent check the scope of the searchable corpus instead of assuming a repository is present. An absent result and an absent repository are different facts. Giving the model a way to distinguish them removes one source of false confidence.

Most importantly, the search tool changes the order of operations. The agent can look across the organization before it commits to a local theory. That is a small capability, but it addresses the autonomy-before-awareness trap directly.

Awareness before autonomy

I increasingly think of agent infrastructure as a set of observation and action surfaces. Action tools get attention because they produce visible work: edit the file, rerun the build, open the change. Observation tools are easier to underestimate, even though they determine the evidence on which those actions rest.

Zoekt on EKS is one example of building the observation surface deliberately. The index provides organization-wide reach. EFS provides a shared, durable corpus. Nightly indexing states the freshness contract. The arm64 webserver and nginx sidecar fit the service into the existing Kubernetes environment. The connector translates ergonomic inputs, hides transport details, bounds output, and gives the agent matches instead of files.

None of those choices makes the model autonomous. That is the point. They make it informed enough to choose a narrower, more defensible next step.

When I build agent tooling, I want to ask two questions before granting another permission: Can the agent see the evidence an experienced engineer would look for, and can it retrieve that evidence without flooding its context? If the answer is no, a longer prompt or a more powerful action tool is premature.

Agents need codebase awareness before they need autonomy. Give them the ability to find the relevant code across the real system, return the smallest useful slice, and make uncertainty visible. Then decide what they should be allowed to do with what they found.