ast-index, a Rust indexer that hands agents a symbol table instead of a file dump
Cli allows AST search, ~40-50% economy tokens. Claude/Codex/Cursor searches (suitable for any AI agent that can use Bash)
At a glance
- What is it?
- ast-index 3.56.0 stores symbols, references, and dependencies in a local SQLite file. Here is the index lifecycle, the monorepo rules, and what the benchmarks actually measure.
- Who is it for?
- ast-index is worth installing on a repository large enough that grep has become the bottleneck, or on any codebase where an agent keeps burning context re-reading files it has already seen. It is not worth the setup on a small project, where the extra build step buys nothing, and the MCP server is deliberately excluded from a default build so you have to ask for it.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.
Editorial analysis
`rebuild` once, then `update` after every edit
The index is a local SQLite database per project, and its lifecycle has exactly two verbs. `ast-index rebuild` builds it from scratch, and `ast-index update` brings it forward after edits or branch switches. The update path is the interesting one, because an agent is usually editing while it queries. Hooks can queue a trailing-debounced refresh, and the documented guarantee is that edits arriving during an update are not lost:
ast-index update --background --debounce-ms 500The read side closes the remaining race. Commands that read the index wait, with a bounded timeout, for a generation that is already queued, so a search issued moments after an edit sees the newer state rather than a stale snapshot. That bounded timeout is the detail to remember: it is a wait, not a guarantee, so a very long update will eventually let a read through with the previous generation rather than blocking indefinitely.
Monorepo reads stop at the nearest marker unless you opt in
Read commands stop at the nearest VCS marker. In a repository with git submodules, subtrees, or nested `Cargo.toml` or `settings.gradle` files, that means a query from a subproject will not reuse a parent index even when one exists. Two ways to say so explicitly:
# later, from any subproject — reuse the root index
AST_INDEX_WALK_UP=1 ast-index search ViewModel
# or per-call:
ast-index --walk-up search ViewModelThe flag is opt-in by design, and the reasoning is worth taking seriously. Silently preferring a far-away parent database could surface a stale or misconfigured index left behind by an earlier accidental `rebuild` higher up the tree. With the flag you are making a statement that the parent index is the one you trust. Set it per shell session or in your agent configuration rather than aliasing the command, so the trust decision stays visible in the invocation.
Worktrees get separate indexes; subtrees are the opt-in join
Two different multi-root situations get two different answers. Independent git worktrees receive independent indexes, because their canonical root paths differ, and the guidance is to rebuild once inside each rather than trying to attach worktrees to one another. Source trees that intentionally form one workspace are handled by attaching a named subtree to a primary index:
cd /path/to/application
ast-index rebuild
ast-index subtree add shared ../shared-library
ast-index update # or: ast-index rebuild
ast-index subtree listAfter that, `--subtree shared` queries only the attachment and `--local` queries only the primary project. The older root-level commands remain as compatibility aliases, and the project says new automation should use `subtree add`, `subtree remove`, and `subtree list` instead. The order of operations matters: the primary index is created first, then the subtree is attached, and only then are the attached files indexed, so an `update` in between is what folds the new files into the same database.
Git history and the symbol graph are opt-in, not part of `rebuild`
Two of the more interesting commands collect data that the index does not hold during a normal build, and both say so. `ast-index hotspots --collect` ranks files by churn, fixes, and authors, which needs Git history rather than a parse tree. `ast-index graph build` produces a symbol-to-symbol dependency graph. Neither runs on `rebuild` or `update`, and `ast-index graph` reads whatever the build step last produced. The consequence for an agent workflow is that a fresh clone will answer structural questions immediately but return nothing for churn analysis until someone has paid for the collection pass, which is the right default for a tool that wants to be cheap on a first run. Both collection steps are also the slowest things the binary does, since one walks history and the other resolves symbol edges, so they belong in a setup script rather than in a per-prompt hook.
Homebrew, Cargo, and Winget carry different costs
There are four documented ways in and they are not equivalent. Homebrew is the macOS and Linux path:
brew tap defendend/ast-index
brew install ast-indexCargo installs from crates.io with `cargo install ast-index --locked`, and a second Cargo invocation installs straight from the unreleased default branch. Both of those build from source and need a Rust toolchain. Windows uses Winget with `winget install --id defendend.ast-index`. The README is explicit that prebuilt release binaries come from Homebrew, npm, or Winget, so the Cargo route is the one to choose only when you want to build it. Building from a clone is the other escape hatch, and it produces a binary of roughly 50 MB at `target/release/ast-index`, which is the size to budget for if you plan to ship it in a container image.
A conflicted tap is a documented failure with a documented fix
Homebrew taps are git repositories, so a tap that has drifted produces a merge conflict rather than a package error. The symptom the project names is `brew install ast-index` failing with `<<<<<<< HEAD` markers, and the fix is to reset the tap to its remote state:
cd /opt/homebrew/Library/Taps/defendend/homebrew-ast-index
git fetch origin
git reset --hard origin/main
brew install ast-indexTwo things follow. First, the tap path is hardcoded to the Apple Silicon Homebrew prefix, which is the path an Intel Mac or a Linux install will not have, so the recipe needs adjusting rather than pasting. Second, this is a symptom of a locally modified tap, so if you edited formulae in there to pin something, the reset throws that away. There is also a migration path from an older tool with the same author: uninstall `kotlin-index`, untap it, tap `defendend/ast-index`, and install.
The MCP crate is deliberately outside a default `cargo build`
`Cargo.toml` declares a workspace with two members, the root package and `crates/ast-index-mcp`, but sets `default-members` to the root alone. The comment gives the reason: existing binary distribution workflows run a plain `cargo build --release` and produce a single `ast-index` binary, and that should keep working. The MCP server is therefore opt-in and needs `cargo build -p ast-index-mcp`. The same care appears in the publish settings, where the `include` list limits the crate to `src`, the vendored `tree-sitter-bsl` sources, the build script, the manifests, the lockfile, and the licence. That vendored grammar is the reason a 1C:Enterprise language can be indexed at all, and it is shipped inside the crate rather than fetched at build time.
The speedup table compares against ripgrep internals the tool links itself
The performance section reports benchmarks on a large Android project of roughly 29,000 files and 300,000 symbols, with ast-index against grep: imports 0.3ms versus 90ms, dependents 2ms versus 100ms, deps 3ms versus 90ms, class 1ms versus 90ms, search 11ms versus 280ms, and usages 8ms versus 90ms. Read the dependency list before you read those ratios. `Cargo.toml` pulls in `ignore`, `grep-searcher`, `grep-regex`, and `grep-matcher` under a comment about fast file search, so the binary and the baseline share the same scanning machinery. That makes the comparison narrower than it looks and also explains why `search`, the command closest to grep's job, shows the smallest margin at 14x while structural lookups show two orders of magnitude. The separate claim that agents save around 40 to 50 percent of tokens on large repositories is a token figure rather than a latency figure, and the repository ships its own benchmark script if you want to reproduce any of it.
Editorial conclusion
ast-index is worth installing on a repository large enough that grep has become the bottleneck, or on any codebase where an agent keeps burning context re-reading files it has already seen. It is not worth the setup on a small project, where the extra build step buys nothing, and the MCP server is deliberately excluded from a default build so you have to ask for it. Before you rely on the numbers, run the benchmark script in the repository against your own tree, read the dated comparison the project links, and set the walk-up flag explicitly in any monorepo rather than hoping the nearest marker is the right index.
Frequently asked questions
How do I build an ast-index index for a project?
Run ast-index rebuild once inside the project, then ast-index update after edits or branch switches. For hook-driven refreshes there is ast-index update --background --debounce-ms 500, and read commands wait with a bounded timeout for a queued generation so they do not return stale results.
Why does ast-index not find symbols in my monorepo subproject?
Read commands stop at the nearest VCS marker, so a nested Cargo.toml, settings.gradle, submodule, or subtree hides a parent index. Pass --walk-up on the call or set AST_INDEX_WALK_UP=1 to prefer an existing parent database. The behaviour is opt-in because a stale parent index would otherwise surface silently.
Does ast-index need an MCP server to work with Claude Code?
No. It ships a Claude Code plugin that you install from a marketplace or with ast-index install-claude-plugin, and its /initialize command writes .claude/settings.json plus .claude/rules/ast-index.md. The MCP crate is a separate workspace member that a default cargo build skips, so it has to be requested explicitly.
Which languages can ast-index parse?
The list is long and includes Kotlin, Java, Swift, Objective-C, TypeScript, JavaScript, Vue, Svelte, Rust, Zig, C#, Python, Go, C, C++, Scala, PHP, Ruby, Perl, Dart, Protocol Buffers, WSDL, XSD, BSL for 1C:Enterprise, Lua, Bash, Elixir, SQL, R, Matlab, Groovy, Common Lisp, and GDScript. Project type is auto-detected.
How are ast-index updates handled while an agent is editing files?
Hooks can queue a trailing-debounced refresh, and edits that arrive while an update is running are not lost. Commands that read the index wait for an already queued generation, with a bounded timeout, so they do not observe stale results.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/defendend-claude-ast-index-search)