Today · File search
Find a file across every tracked repo
RepoOps indexes the path and the first line of every file in every repository you track, and answers a substring query across all of them from memory. No model call, no key, nothing leaves the machine. The index reads the mirror pinned to the remote branch, so it shows what is pushed, not what is open in your editor.
For: the engineer who knows part of a file name but not which repository holds it, and the reviewer following a path quoted in a pull request comment
What it does, and why it helps
Once the server is listening, it walks each ready mirror with git ls-files --cached --others --exclude-standard: tracked files plus untracked files that are not ignored. A directory that is not a git checkout gets a plain walk that skips .git, node_modules, dist, build and a few other build folders. For each file it records the path, the size, the modified time, a kind from the extension (code, doc, config, style, data, asset, other) and a title: the first heading of a markdown file, otherwise the first non-blank line with its comment leader stripped, cut at 160 characters. A file over 64 KB, an empty file, and any asset or data file gets no title. One entry per file sits in memory. The indexer writes nothing to disk.
The File search tab searches those entries as you type. The query is case-insensitive and splits on spaces. A path match scores 3, a title match 2, a whole-word match in the path 1 more, and a more recently modified file wins a tie. The tab asks for 200 hits and shows them 20 at a time. A path under .claude/brain/ or docs/ opens in the dashboard's own file viewer; any other path is plain text, because nothing serves it. The index is rebuilt when the server starts, when you add a repository, and when you press Re-index. The mirror advancing on its own does not rebuild it.
The pain. You remember a file name, or half of one, and not which of five repositories holds it. The terminal answer is a grep per checkout, after a cd per checkout, after remembering where each checkout is.
The point of view. A file lookup should read the same source the rest of the dashboard reads, the mirror of what is pushed, and it should cost nothing: no model, no key, no network. A first line is enough of a title to tell two similar names apart without opening either.
What gets easier. Finding the file. One box, every tracked repository, matches ranked with the path, the title, the size and the repository on the row, and a link when the dashboard can open it.
When it helps. You are reviewing a pull request comment that quotes a path, chasing where a topic is documented, or checking whether a file exists at all in a repository you rarely open. Also when you want a count: the stats row shows how many files each repository has.
Its limits. It matches paths and first lines, not file contents; a symbol name matches only where it appears in one of those. It reads the mirror at the remote branch, so an uncommitted or unpushed file is not in it. It does not rebuild by itself when the mirror advances; press Re-index. Rows for a repository you removed stay in the index until the server restarts. A path outside .claude/brain/ and docs/ is not a link. The hosted page is a different thing: a name filter over the files your desktop published in its brain snapshot, with no titles and no kinds.
Understand it in 30 seconds
Read the narration
- 0:00 A reviewer quotes a path.
- 0:02 You track four repos and none is open.
- 0:06 RepoOps indexes paths and first lines across your repos.
- 0:10 From the mirror, not your editor.
- 0:13 Type part of the name.
- 0:14 Every match lists its path, first line, size and repo, path hits first.
- 0:20 Nothing leaves the machine.
- 0:23 Open it, or note the repo.
- 0:25 Names and first lines, not content.
Synthetic example. Read the guide
Where to find it
Where to find it
- Desktop:
localhost:4000, then Attribution in the sidebar, then File search under All tools, in the Pull requests, blame and graphs group. - Hosted:
repoops.ai/team/file-search, from Attribution in the sidebar, then File search under All tools, in the Pull requests, blame and graphs group. - Keyboard: ⌘ K, then type “File search”.
When to use it
A path quoted in a pull request comment
Situation. A reviewer wrote that the retry cap lives in checkout/retry.mjs, and you track four repositories.
What you do. Open File search from the rail. Type retry.mjs. Leave All repos and All kinds as they are.
What you see. The line above the table reads how many matches of the total the page holds. Each row shows the kind pill, the path, the first line of the file, its bytes and its repository. A match under docs/ or .claude/brain/ is a link that opens the file in a new tab.
What it establishes. You know which repository holds the file and what its first line says, read from the mirror at the remote branch. It does not tell you whether a local branch has changed it since.
Where is this documented?
Situation. You want every note about egress across the tracked repositories, and you do not know the file names.
What you do. Type egress and pick doc under All kinds. If the list is long, pick one repository under All repos, or add a second word.
What you see. Markdown files whose path or first heading contains the word, ranked with path matches above title matches. Files over 64 KB and empty files show a dash where the title would be.
What it establishes. A list of candidate documents by heading, not a search of their contents. A note that mentions egress only in its body is not in this list; Ask the brain searches content.
Before you start
- Supported versions
- RepoOps desktop v0.3.1, the release this guide was read against. git on the machine's PATH gives the exact file list; without it the indexer walks the directory and skips a fixed set of build folders.
- Where it runs
- Local: the File search tab on the rail, served from public/file-search.html over GET /api/index/stats and GET /api/index/search. Hosted: /team/file-search lists and filters the file names in your team's published brain snapshot; it has no index of your working repositories.
- Permissions
- Local: none beyond running the app; the API answers on 127.0.0.1 unless REPOOPS_BIND_HOST says otherwise. Hosted: a signed-in member of the team, narrowed to the repositories that member may see.
- Connections
- None for the local index. It reads the mirror the app already keeps for every other tab, which fetches the remote on the sync interval. The hosted list needs the Brain snapshot publish toggle on, a bound device and cloud sync on.
- Plan
- No plan gate. No file under website/lib names file-search except the hosted nav, and the local routes check nothing about a plan. The hosted page follows the hosted dashboard tiers.
Configure it
- Track the repository.
Settings, the Repos & accounts subtab: Connect from GitHub, or Add a repo manually by absolute path and press Add repo. The mirror initialises, and the index for that repository builds in the background as soon as the mirror is ready. A failed build on the GitHub path is logged with its reason under the [index] prefix; on the manual path it is silent.
- Open File search and read the stats row.
It reads Total: N files and a count per repository. If it reads No indexed repos yet. Click Re-index to build the index., no mirror was ready when the server started. On a reload the last stats paint first and refresh in place.
- Type, then narrow.
The search runs 120 milliseconds after you stop typing. All repos and All kinds narrow it; ?repo=<id> in the tab URL preselects a repository. Add a second word to require both.
- Press Re-index when the mirror has moved.
The button reads Indexing… while POST /api/index/refresh?repo=all rebuilds every ready mirror, then the stats and the current search run again. Within five minutes of a build at the same commit the rebuild returns the cached index unchanged.
| Setting | Where | A sensible choice | Why it matters |
|---|---|---|---|
mirrorRoot | repos.config.json | the default: the configured path, or <data dir>/mirrors when that path's parent does not exist | The index reads files from <mirrorRoot>/<repo id>, the worktree pinned to the remote branch, never from the checkout you edit. |
syncIntervalMs | repos.config.json | 30000 (the default) | How often the mirror resets to the remote branch while it is moving; a quiet branch is checked less often, up to every 5 minutes. It bounds how fresh a Re-index can be; it does not trigger one. |
limit | the query string of GET /api/index/search; the tab sends 200 | 200, as the tab does | Default 50, cap 500. total in the response is the full match count whatever the limit. |
repo, kind | the query string of GET /api/index/search, set by the two selects | empty, for All repos and All kinds | kind is one of code, doc, config, style, data, asset, other. A repo id that is not indexed matches nothing. |
repo | the query string of POST /api/index/refresh; the tab sends all | all | One id rebuilds one repository; an id that is not tracked returns 404 unknown repo. all rebuilds every mirror that is ready. |
Brain snapshot | Settings, What this machine publishes to your team | off unless your team uses the hosted dashboard | It is what fills /team/file-search. Off by default; it needs a bound device and cloud sync on. The local index is not published by it or by anything else. |
What you should see
The normal case
Configuration. Two or more repositories tracked, mirrors ready, no filters.
Expect. The stats row counts every repository. A query returns rows ranked with path matches first; a docs/ or .claude/brain/ path is a link.
Verify. GET /api/index/stats lists each repoId with count, indexedAt, sha and byKind. The sha equals the mirror's current commit when the index is fresh.
A fresh push, a stale index
Configuration. A file pushed to the remote after the last build.
Expect. The mirror picks it up on the next sync; the index does not. The sha in the stats lags the mirror's commit.
Verify. Press Re-index; the count moves and the new path appears. The refresh is a POST, so a GET on /api/index/refresh is not a route.
A repository you removed
Configuration. A repository removed with Remove in Settings after the server started.
Expect. Its rows still match until the server restarts. Re-index rebuilds the remaining repositories and leaves the stale entries in place.
Verify. Restart the app; the stats row no longer lists it and its rows are gone.
Data and cost
- What is captured
- Per file: repository id, path, size, modified time, kind and up to 160 characters of first line or first heading, read from the mirror and held in memory. The indexer writes nothing to disk. Nothing about who searched or what they typed is recorded.
- Who can see it
- Local only. The index is not part of any snapshot the desktop publishes. The hosted /team/file-search list is a different source: the file names in the Brain snapshot your desktop publishes (top-level .claude/brain/*.md, lessons, ai-calls metadata, engagements, labs, docs/roadmap.md and docs/ledger.d/*.md), team-scoped and narrowed by per-repo access.
- How long it is kept
- Local: until the next rebuild or restart; there is nothing on disk to keep. Hosted: the snapshot rows in brain_snapshots, of which the list reads the freshest per repository and file. No retention rule for that table was found under website/lib; see Delete below.
- What leaves the machine
- None from the index. No model call, no key, no network beyond the mirror's own git fetch. The one path a file name takes to repoops.ai is the Brain snapshot publish, which is off by default and covers the files above, not your repository tree.
- What it costs
- No metered cost. At build, one stat per file and one read of up to 64 KB per text file, 16 at a time, per repository. A search is one pass over the in-memory entries on the server, once per pause in typing.
When the result differs
| Symptom | Likely cause | Next action |
|---|---|---|
| The stats row reads No indexed repos yet. | No mirror was ready when the server started, or every index build failed. | Press Re-index. If it stays empty, check the repository's state in Settings and the [index] lines in the app log. |
| A file you pushed is missing. | The index was built before the mirror fetched it; the mirror advancing does not rebuild the index. | Press Re-index. Compare sha in GET /api/index/stats with the mirror's commit. |
| A file open in your editor is missing. | It is not on the remote branch the mirror tracks; the index never reads your working checkout. | Push it, wait one sync interval, press Re-index. |
| A row has a dash where the title should be. | The file is over 64 KB, empty, not readable as UTF-8, or of kind asset or data. | Match on the path instead; the Bytes column shows the size. |
| A row lacks one of your words. | Matches are counted per word per field, so a word found in both the path and the title can stand in for a word found in neither. | Add the missing word as a path fragment, or narrow with All repos or All kinds. |
| A path is not a link. | The dashboard serves .claude/brain/ and docs/ of each mirror and nothing else. | Open the path in your own checkout. The row is text on purpose rather than a link that fails. |
| Rows from a repository you removed. | The indexer never drops a repository's entries; Re-index rebuilds only the mirrors that remain. | Restart the app. |
| The hosted page reads No snapshot yet. | The Brain snapshot toggle is off, the device is not bound, or cloud sync is off. | Turn the toggle on in Settings under What this machine publishes to your team, then wait for the next publish. |
- Disable
- Not provided as a switch. The index builds on every start; lib/repo-indexer.mjs reads no flag. Remove a repository in Settings to keep it out of the next build, and restart to drop its rows now.
- Roll back
- Not applicable. The index writes nothing; the next rebuild replaces it whole.
- Revoke access
- Local: nothing to revoke; the API answers on the loopback address. Hosted: disconnect the device in the desktop app, which forgets the binding and revokes the device token; the team's per-repo access rules decide who sees the published list.
- Delete
- Local: restart the app; there is no file. Hosted: not provided. No route deletes a team's brain_snapshots rows; a new publish adds a fresher row and the list reads the freshest, so it supersedes rather than removes. The data sits in the brain_snapshots table read by website/lib/brain-search.ts.
Related tasks
Maintenance evidence
- Feature id
file-search(spine leaffile-search)- Owner
- Universal file indexer (the features.md row; docs/features/universal-file-indexer.md, the brain-vision agenda). Hosted list: Hosted UI Parity W5. Guide: LDG-0717.
- Supported product version
- RepoOps v0.3.1
- Last verified
- 2026-09-15, read against origin/main at 52366bb6d; labels read from the served tab source (public/file-search.html, public/settings.html) and the hosted page source. No live instance was run for this guide.
- Example fixtures
- No fixture file. lib/repo-indexer.test.mjs seeds a temp directory with a (marketing) folder and two markdown files through the non-git walk and exercises searchIndex on metacharacter queries and whole-word matches; lib/repo-indexer.retention.test.mjs pins flatten and both first-line return paths, with a heap check that runs only under --expose-gc.
- Source references
lib/repo-indexer.mjs,lib/routes/repo-index.mjs,lib/routes/repos.mjs,server.mjs,lib/mirror.mjs,lib/mirror-root.mjs,public/file-search.html,public/lib/pagination.js,public/lib/repoops-tab.js,public/settings.html,website/app/team/(home)/file-search/page.tsx,website/lib/brain-search.ts,lib/brain-publisher.mjs,lib/publish-catalog.mjs- Documentation review
- Independent review requested on the slice pull request; not yet recorded.
- Video review
- Story script written 2026-09-15; render and review pending in the same slice.
Last updated