
vex
io.github.tenatarikav1.27.2Updated Oct 3, 2026
Local code search for agents: symbols, usages, call graph, AST patterns, grep and semantic search
Overview
Local code search for AI assistants: symbol lookup, usages, call graphs, AST patterns, grep and semantic search over an indexed project.
- What it does
- Vex indexes a local codebase and exposes search-shaped commands to an assistant: exact symbol lookup, symbol body extraction, usages, callers and callees, multi-hop call graphs, implementations, tests-for, near-duplicate detection, semantic similarity, symbol-level diffs against a branch, and per-symbol history across commits. It combines structural FST lookup, BM25 and embedding search with scope and metadata filters, and returns compact one-line records to save context tokens. A bundle mode composes several lookups into one response.
- When to use it
- Useful when an assistant needs to navigate a large local repository without reading whole files: finding where a symbol is defined or used, judging the blast radius of a change, locating tests for a function, or finding semantically similar code. It is a static-analysis tool, so dynamic dispatch and reflection are invisible to it.
- Requirements
- Runs as a local process on the user's machine (desktop only). The MCP server package runs the vex CLI, so vex must also be installed and on PATH, or VEX_BIN must point to it. Source builds need network access at build time, a C/C++ toolchain, and on Linux libssl-dev and pkg-config. Semantic indexing downloads an embedding model on first use. No accounts, API keys or declared environment variables are required.
Installation
In SourceWeft
- Open vex in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
Vex
[License: MIT] [CI] [Rust] [Commands] [Languages] [Tests]
Fast hybrid structural + semantic code search. Vector + index.
Why Vex? · How It Compares · Installation · Quick Start · Commands · Configuration · How Search Works · Benchmarks · Supported Languages · Integration · Testing · Architecture
Pick the right tool: vex check for "does Foo exist?", vex search for "find me something about retries". search is a ranked blend — it surfaces neighbors (callers / imports) when no symbol literally matches, which is great for exploration and wrong for exact-name lookup. v1.15.0 prints a stderr hint when an identifier-shaped search returns 0 FST hits.
Why Vex?
- ~4-5ms search after indexing — FST-based O(query_len) lookup, not O(symbols); constant regardless of project size. Requires a pre-built index. Indexing is a one-time cost (hundreds of ms on typical projects) and builds more than a plain text index — FST + BM25 + call graph + type-hierarchy + trigram skip-index — so it trades a slower build for far cheaper, richer queries (see Benchmarks)
- 3-channel hybrid search — structural FST (names) + BM25 (rare body terms) + semantic HNSW (meaning), fused via Reciprocal Rank Fusion. Find symbols when you don't know the exact name AND when generic semantic-only search would be too noisy
- Persistent call graph —
vex callers/vex calleesread from a persistent index built at index time (~4ms), not a live tree-sitter scan (seconds):callersis a name-keyed FST,calleesis a dense CSR index (v9+). Module-scope expressions are reported via synthetic<module:path>callers (Phase 14.1); Python + Java function/method decorators (Phase 14.2), Kotlin annotations + C# method/constructor attributes (Phase 14.2.2), and TypeScript method decorators + Rust outer attributes (Phase 14.2.1) emit forward edges to their targets. Class-level decorators remain invisible — seedocs/LIMITATIONS.md - Pluggable embedder —
Embeddertrait + registry; swap MiniLM-L6-v2 for future code-specific models (BGE, CodeBERT) without touching call sites - Token-efficient — compact output saves typically 6x fewer tokens than grep on average lookups (up to 217x on minified JS/CSS);
vex showextracts just the symbol body instead of the whole file - 19 languages indexed via tree-sitter, with three coverage tiers: type-aware
--strict usageson 8 binder languages (Rust / TypeScript / Python / C# / C++ / Go / Java / Kotlin); indexed pattern prefilter on 15 T1+T2a languages; baseline structural + semantic search on all 19 (see Supported Languages for the matrix) - Single binary, zero config — no LSP servers, no databases, no Docker. Just
vex index && vex check Foo
What Vex isn't
vex is a static-analysis indexing tool, not a language server. Set expectations honestly:
- Not an LSP replacement. No go-to-definition into third-party packages, no rename refactoring, no type-checking, no hover docs. For those, keep your LSP.
vex searchis a ranked blend, not an exact-name lookup. Structural FST + BM25 + semantic fused via RRF return relevance-ordered results — when no symbol literally namedFoolives in the index (imported from a dependency, deleted, typo), BM25 may surface callers / imports as if they were the definition. For exact-symbol questions ("does it exist?", "show me the body", "who calls it?") usevex check Foo/vex show Foo/vex usages Foo --strict— they bypass the ranker. v1.15.0 prints a one-line stderr hint when an identifier-shaped query gets zero FST hits.- No dynamic-dispatch visibility. Decorator routing (
@router.get("/path")), string-resolved factories (uvicorn.run("main:app")), reflection (getattr(obj, name)()), and macro-expanded references are all invisible to every vex command.vex grep '\bname\b'is the textual escape hatch. vex callershas uneven coverage outside function scope. Module-level expressions likeapp = create_app()are reported via synthetic<module:path>callers (Phase 14.1). Python + Java function/method decorators (Phase 14.2), Kotlin annotations + C# method/constructor attributes (Phase 14.2.2), and TypeScript method decorators + Rust outer attributes on fns/methods (Phase 14.2.1) emit forward edges —vex callers GetMappinglists every Spring handler,vex callers HttpGetevery ASP.NET action,vex callers testevery#[tokio::test]. Class-level decorators (14.6) remain on the roadmap.vex usagesquality varies by language. 8 binder-supported languages get refactor-grade--strictrefs; the other 11 use an identifier scanner with a higher false-positive rate.
See docs/LIMITATIONS.md for the full coverage matrix, concrete repros, and recommended workarounds per query type. Read it before evaluating vex on a Python/FastAPI/Django codebase — the framework patterns are the most-flagged gaps.
How It Compares
Note: vex search speed assumes a pre-built index. Ripgrep and ast-grep require no upfront indexing and work immediately on any directory. The tradeoff is amortized: if you search the same codebase many times (typical in agent workflows), the one-time indexing cost pays for itself.
Best for: fast symbol search in AI agent workflows where token efficiency matters. Not a replacement for LSP-based tools (no refactoring, no go-to-definition in dependencies).
Installation
What cargo install vex-search (and any source build) needs:
- Network at build time: the build downloads a prebuilt ONNX Runtime. Prebuilts exist only for
aarch64-apple-darwin,x86_64/aarch64-unknown-linux-gnuandx86_64/aarch64-pc-windows-msvc; on any other target (Intel macOS, musl, …) pointORT_LIB_LOCATIONat a local ONNX Runtime build. - A C/C++ toolchain (Xcode Command Line Tools,
build-essential, or MSVC Build Tools) for the tree-sitter grammars. - Linux:
libssl-devandpkg-config(Fedora:openssl-devel). The HTTP stack links OpenSSL throughnative-tls. - The first
vex index --semanticdownloads the ~86 MB embedding model; structural search needs no download. vex-search-mcponly installs the MCP server. It runs thevexCLI, so installvex-searchtoo and keepvexonPATH, or setVEX_BINto its full path.
Linux
Pre-built vex ships in every GitHub Release for x86_64-unknown-linux-gnu:
Built on the current ubuntu-latest GitHub runner (glibc-linked). For older glibc distros, musl-based distros (Alpine, NixOS without nix-ld), or aarch64 Linux (Graviton, Pi 5, Ampere) — build from source via cargo build --release.
Windows
Pre-built vex.exe ships in every GitHub Release.
- Download
vex-x86_64-pc-windows-msvc.tar.gzfrom the latest release - Extract
vex.exesomewhere stable (e.g.C:\Users\<you>\bin\) —tar -xzf vex-x86_64-pc-windows-msvc.tar.gzfrom a recent PowerShell, or 7-Zip / WinRAR via right-click. Security note:vex.exeloads the bundledDirectML.dllfrom its own folder, so on a multi-user machine or shared drive prefer a directory other users can't write to (e.g.C:\Program Files\vex\— with the trade-off thatvex self-updatethen needs an elevated shell). See GPU_SUPPORT.md §6. - Add that folder to
PATH(System Properties → Environment Variables → editPath→ add the folder) - Open a fresh terminal and run
vex --version
To update, run vex self-update — it fetches the latest release, picks the right archive for your platform, verifies its signature, and replaces the binary in-place. On Windows it also installs/refreshes the bundled DirectML.dll sidecar (skipped when byte-identical; re-installed if an older self-update dropped it — updaters up to v1.16.0 extracted only the binary). Same command works on macOS and Linux too.
GPU acceleration is built into the prebuilt binaries — Windows ships with DirectML (any DX12 GPU, driver-only; the redist
DirectML.dllis bundled in the archive) and macOS arm64 with CoreML. NVIDIA CUDA is a source-build opt-in. Runvex gputo check, and see GPU Acceleration.
Quick Start
Commands
Per-query filters (every search-shaped command)
All search-shaped commands accept scope filters. Specific filters vary by command:
--include <glob>/--exclude <glob>(repeatable, gitignore syntax) — per-call path scoping that doesn't require re-indexing.--excludewins over--include. Example:vex search Foo --include 'src/**' --exclude '**/*.gen.*'.--exclude-tests(MCP:exclude_tests: true) drops test files (tests/,*_test.*,test_*.py,*.spec.ts,__tests__/,tests.rs, … — the same set asvex tests-for); it composes with--include/--exclude, is recorded in the--whytrace, and is applied by every command that takes the scope flags (search,usages,callers/callees,impact,grep,show,pattern,implementations/subtypes,similar/duplicates,modules,paths,reachable,diff,bundlepr-impact — there it filters the changed files and the transitive-caller/test rows;bundlesymbol/projectmodes ignore all scope filters).vex tests-forrejects--exclude-tests(exit 2): it lists test functions, so the flag would always return nothing. Path-based only: Rust unit tests inside a#[cfg(test)] mod testsblock of a non-test file are not excluded.--filter-path <substring>(alias--filter) — path-substring filter onsearch,show,usages,grep,similar,duplicates. Composes AND with the globs.
vex search / vex show additionally accept:
--visibility <public|private|protected|internal>— keep only symbols whose signature carries the explicit keyword. Defaults aren't inferred (bare Rustfn foo()does NOT match--visibility private).--async-only/--no-async— keep or exclude async / Kotlin-suspendsymbols.--static-only,--sealed-only— restrict to static class members or sealed (or Java-final) types.
Reasoning flags
vex search --whyprints a JSON trace to stderr (the result list stays on stdout):normalized_query, per-channel hit counts (FST / BM25 / semantic), fallbacks engaged (fuzzy), and the active filter snapshot.vex pattern --whyprints a JSONScanTraceto stderr after the result list:mode(indexed/live_scan),root_kind_inferred,candidate_files/total_files, andfallback_reasonwhen the indexed prefilter was skipped (no-index,no-skeleton-section,empty-section,grammar-drift,partial-section,index-open-error). MCP callers see the same JSON under_meta.why.vex similar --explain/vex duplicates --explainadd ajaccardoverlap score plus a truncated unified diff between the two bodies, so you can decide whether two semantically-clustered symbols are actually duplicates before acting.
Multi-repo workspaces (--workspace) — v1.22.0
Declare a set of sibling repos in a .vex-workspace.toml and run any command with --workspace to fan out across all of them, grouped by repo:
--workspaceis accepted byindex,update,search,grep,check,usages,impact,callers,callees,reachable,modules, andwatch. Each member keeps its own.vex.toml(excludes / embedder / sections / cache).- Reference and call-graph resolution is per-repo by default. The one exception is
vex usages <name> --strict --workspace, which resolves a reference in repo B to a symbol defined in repo A via a gtags-style name fallback (rendered as aname-resolvedsub-tier). Requires v7+ index — re-runvex indexafter upgrading. - A member missing a capability (
--stricton an old index, a call graph forreachable) is reported unavailable for that repo instead of aborting the whole fan-out.--workspaceconflicts with--why. Seedocs/MULTIREPO.mdand LIMITATIONS §7.
Configuration
Create a .vex.toml in your project root to customize vex behavior:
CLI flags always override config values. Use --no-semantic to explicitly disable semantic mode when the config enables it. The VEX_DEVICE and VEX_EMBEDDER environment variables act as global defaults across all projects (lowest precedence, below .vex.toml) — see GPU Acceleration.
Keeping config out of the repo
Don't want a .vex.toml inside the repository (can't .gitignore it, shared checkout, etc.)? vex never creates one on its own — only vex init writes it — and you have two ways to keep config external:
--config <path>/$VEX_CONFIG— point vex at a config file anywhere on disk. It replaces the in-repo lookup entirely, so the repo stays clean:--configbeats$VEX_CONFIG; a missing/invalid path is a hard error (vex won't silently fall back). Relative paths inside that file resolve against the file's own directory.- A parent directory — config lookup walks up from the project to the filesystem root, so a
.vex.tomlplaced in any ancestor (e.g.~/work/.vex.toml, or~/.vex.tomlfor a machine-wide default) is picked up for every repo beneath it, with none living in the repos themselves.
The index itself is never written into the repo — it lives in the cache dir (--cache-dir / $VEX_CACHE_DIR / platform cache), so a clean repo is just a matter of config placement.
Staleness Detection
Vex detects when the index is stale and warns before search:
How it works: on every search, vex compares the git HEAD stored at index time with the current HEAD (~0.1ms, single git rev-parse). If HEAD changed → stale. For non-git repos, falls back to mtime comparison — and since v1.11 (H11), when mtime fires, vex streams a xxh3_64 content hash of the file and compares it to the manifest. If the hash matches, the touch was cosmetic (git checkout, rustfmt no-op, rsync --times) and the file stays Fresh; only a real content change re-triggers indexing.
Auto-update: skip the warning and update inline:
GPU Acceleration
Semantic indexing (--semantic) can run the embedding model on a GPU — a large win on a full/cold index. On an RTX 3080 over a 28k-symbol C++ module, embedding the default MiniLM model was 51× faster on CUDA and 29× on DirectML vs CPU (full benchmark + design notes in docs/GPU_SUPPORT.md).
Two layers — the binary, and the device:
- Prebuilt binaries bake in a driver-only GPU EP: Windows → DirectML (any DX12 GPU — NVIDIA/AMD/Intel; the redist
DirectML.dllis bundled in the archive), macOS arm64 → CoreML. No SDK, no extra install. The Linux prebuilt is CPU-only. - CUDA is a source-build opt-in (fastest on NVIDIA — ~1.75× DirectML):
cargo install --git https://github.com/tenatarika/vex vex-search --features gpu-cuda. Needs the CUDA 12 runtime + cuDNN 9 onPATH(the NVIDIA driver alone is not enough — it ships onlynvcuda.dll, not the runtime/cuDNN). Source builds for the others:--features gpu-coreml/gpu-directml.
Selecting the device (vex index / vex update):
--gpu/--no-gpu— force GPU (Auto) or CPU.--device cpu|auto|cuda|directml|coreml— pick a specific execution provider.- Default is Auto: use the compiled-in EP when it initializes, else silently fall back to CPU. On a CUDA-enabled binary Auto prefers CUDA → DirectML → CoreML; on the standard Windows prebuilt only DirectML is compiled in, so Auto uses DirectML regardless of whether the PC could do CUDA.
- A tiny incremental
vex updatestays on CPU (the GPU warm-up isn't worth a handful of symbols); cold/large--semanticbuilds use the GPU.
Is the GPU actually being used? Run vex gpu — it reports the compiled EP and actively probes it, so a silent CPU fallback shows as FAILED with targeted setup remediation. vex gpu cuda probes a single EP; vex gpu --enable persists the working device to VEX_DEVICE (user env via setx on Windows; prints the export line to add on macOS/Linux). A stale VEX_DEVICE pinned to a GPU EP that a later (e.g. CPU-only) build lacks degrades to CPU rather than erroring.
Environment variables
Output Formats
Pin a different default in .vex.toml via format = "text" if you want the verbose multi-line view at the terminal.
JSON envelope (v1.11.0 — BREAKING for bare-array parsers)
Every --format json subcommand wraps its payload in the Phase 13
envelope. Single shape, easy to detect via protocol_version:
Pre-v1.11 only search and bundle returned this envelope; the other
subcommands (show, usages, pattern, grep, implementations,
callers, callees, paths, reachable, tests-for, check,
similar, duplicates, diff, outline, index, update,
status, eval) emitted bare arrays / objects. Migration: pre-1.11 jq '.[0].name'
or data[0]['name'] now needs jq '.results[0].name' /
data['results'][0]['name']. Detect the envelope via
response.get('protocol_version') == 'v1' to support both shapes
during a rollout window.
How Search Works
Structural Search (default)
Searches by symbol name using an inverted index with CamelCase splitting:
"PaymentService"— exact match"Payment"— prefix match, finds PaymentService, PaymentGateway"payment"— case-insensitive, also finds via CamelCase tokens
Semantic Search (--semantic)
Embeds your query with MiniLM-L6-v2 (384-dim vectors) and finds symbols with similar meaning:
"parse source code files"findsparse_file,extract_refs,parse_file_symbols"database storage"findspopulate_db,create_10k_db,add_root_persists_to_db"find implementations of an interface"findsfind_implementations,test_interface_extends
BM25 Channel (auto-on when index has BM25 data)
A classic Okapi BM25 (K1=1.2, B=0.75) over symbol body tokens — identifiers, signatures, docstrings. Closes the gap between "exact name" (structural) and "general meaning" (semantic): finds rare body terms like timeout, retry, singlestore, idempotency_key that aren't part of any symbol name. Since v1.11 (Phase 8.4) body tokens are also extracted from TOML / YAML / HTML / CSS values, so vex search "production endpoint" --semantic can hit a [server] table with endpoint = "https://...". Pass --no-bm25 to disable per-call.
Hybrid Search (3-way RRF)
When the index has all three channels (built with --semantic), vex search fuses structural + BM25 + semantic using Reciprocal Rank Fusion. Symbols hit by ≥2 channels rank as Hybrid; symbols unique to one keep their original match type. Cuts both structural-noise and semantic-blur in the same query.
Usages (FST)
References stored in an FST (Finite State Transducer) — zero-copy lookup from mmap with prefix search support.
Symbol clusters (vex modules)
vex index groups symbols into clusters of code that call or reference each other (deterministic Leiden-CPM over the call, reference and hierarchy edges; no randomness, so two indexes of the same tree agree). vex modules reads them back:
(Output above is from this repository at the time of writing; cluster ids are ordinals in the section, not ranks.)
- A cluster's
labelis the deepest path prefix holding at least 60 % of its members' files.cohesionisinternal / (internal + cut)edge weight; the hubs are the three members best connected inside the cluster. --include/--exclude/--exclude-testsfilter members and hubs (out-of-scope hubs are dropped); a cluster is shown when at least one member is in scope, and itssizeis the in-scope count (size_at_buildkeeps the build-time count in JSON).- Symbol mode reports a per-match
status:clustered,unclustered(isolated),not_eligible(headings, modules, markup/config languages) ornew_since_build. - Clusters are computed by
vex indexand carried, frozen, acrossvex update. After an update the JSON hasstale: trueandnew_since_build, and text output ends with a!line; runvex indexto recompute. This is separate from_meta.vex.dev/stale, which still means "index older than the working tree". --limitmust be at least 1; with a SYMBOL it caps the matched symbols (JSONsymbols_totalreports the uncapped count) and--min-sizeis ignored. Exit codes:0with results,1when empty (the reason is inresults.empty_reasonand on stderr),2for a corrupt cluster section. With--workspace, clusters are per repo and--limitapplies per repo.
Type-aware refs (--strict)
vex usages --strict <name> reads the v5 reference_edges section
written by an LSP-style scope binder. For the languages with a
binder (Rust, TypeScript, Python, C#, C++, Go, Java, Kotlin) every ref is resolved at
index time against an in-file scope chain plus an import/use graph,
then serialised against the global symbol the user actually meant —
not just any line that mentions the spelling.
What this changes for the user:
- Identifiers inside comments, doc-strings, string literals, and
regex bodies are dropped (this filter is on for everyone, not just
--strict). - A name shadowed by a
let/const/ fn param resolves to the inner scope, not the outer. - A
use ext::Foo;/import { Foo } from './ext'/from ext import Foomakes a ref toFooresolve cross-file to whatever defines it in the index. For C++, quoted#include "..."(v1.14+) walks the transitive include graph via BFS to resolveFooagainst symbols defined in any reachable header. System headers<vector>/<string>and macro includes (#include MY_HEADER) stay unresolved by design. - A name imported but never defined in the index stays
Unresolvedand produces no edge — better than a coincidental match.
Without --strict vex usages still works for every supported
language via the legacy refs FST; --strict simply trades recall
breadth for precision on the eight binder languages. v3 / v4 indexes
predating the binder bail with a "re-run vex index" message.
Structural Patterns (vex pattern)
Match code by shape rather than text. Works on every language vex
parses; an indexed prefilter (via the v6 pattern_skeletons section)
speeds up candidate selection on the 15 languages that emit skeletons
(every language except Bash, Lua, YAML, and TOML, which live-scan).
Syntax:
$NAME— capture a single identifier or balanced expression. Same name appearing twice enforces a back-reference:record($X, $X)matchesrecord(state, state)and rejectsrecord(state, other).$_— wildcard (matches without capturing).$$$— anonymous ellipsis (matches anything up to the next literal; spans newlines).$$$BODY/$$ARGS— named multi-line ellipsis. Functionally identical to$$$but captures the consumed text under the given name;$$$BODYreads naturally for block bodies,$$ARGSfor parameter lists. Back-reference equality also applies.&&(space-flanked) — AND composition. Both sub-patterns must match in the same file, and shared metavar names must capture the same text in both:struct $S && impl $Smatches files that have both shapes for the same$S.||(space-flanked) — OR composition (union, deduped by(path, line)).&&binds tighter than||.- Composition operators only fire at bracket / quote depth 0, so
record($X, $X)andf($X && $Y)stay single patterns.
Indexed prefilter: when a v6 index is present, the leading literal
keyword of the pattern (fn, struct, class, def, impl, …) is
mapped to a tree-sitter node kind, and vex pattern walks only the
files whose persisted skeletons contain that kind. Visibility / async
/ export modifiers in front of the keyword are stripped before the
match (pub async fn $F infers function_item correctly). Falls
back to live-scan on grammar drift, missing section, or a partial
section after vex update — --why reports the exact reason.
Examples:
Benchmarks
Compared against ast-index v3.31.0 (SQLite + FTS5) and ripgrep 15.1.0.
Methodology: indexing re-measured 2026-08-12 on Apple Silicon (macOS), vex v1.25.5, release build, cold cache, non-semantic index. Search figures are unchanged from the 2026-07-11 / v1.25.1 run (nothing in v1.25.2-v1.25.5 touches the search path) and are the average of 10 runs. Reproduce with ./benches/bench.sh (point VEX_BENCH_LARGE_PROJECTS at your own repos for larger corpora). Numbers are machine-specific — treat the ratios, not the absolutes, as the signal.
Indexing
Honest read: vex indexing is ~1.7-1.9x slower than ast-index — it builds far more at index time (FST + BM25 + persistent call graph + resolved reference edges + a v8 type-hierarchy section + a trigram skip-index + pattern skeletons + symbol clusters (v9)), where ast-index builds a SQLite + FTS5 store. That one-time cost buys the constant-time queries below; the resulting index is still ~1.4-2x smaller on disk (mmap + FST vs SQLite). These measurements are from v1.25.5 (before v9); v9 adds the cluster pass — re-measure pending. The gap was ~2.5-3x when last measured at v1.25.1; v1.25.5 is the main reason it has narrowed — a single-variable A/B against v1.25.4 put its cold-index gain at −34% and −31% on two corpora, from parsing each file once and sharing the tree across all extractors instead of re-parsing it per extractor. Projects indexed with --semantic are slower again (ONNX embedding generation) and produce a larger index.
Search: vex vs ast-index vs ripgrep
Medium project (ast-index repo, 31K lines Rust, avg 10 runs):
Key takeaway: vex search is constant ~4-5 ms (FST O(query_len)) regardless of project size — this is the win the slower index build pays for. The ripgrep comparison is not apples-to-apples: rg scans raw text with no index, so it scales with corpus size (single-digit ms on this 31K-line repo, 100 ms+ on large ones), while vex does a pre-built FST lookup. The durable advantage is amortized and qualitative: vex returns only symbol definitions (precise, token-efficient), while rg returns every text occurrence (noisy, expensive in LLM contexts).
Pattern Matching (vex only)
Medium project (ast-index repo, Rust):
ast-index and ripgrep do not support AST pattern matching.
Semantic Search
Illustrative (semantic capability, not a latency benchmark) — queries where structural search returns 0 results but semantic finds relevant symbols:
HNSW vs Brute-Force (semantic vector search)
The latency figures below were measured on an earlier build and not re-run in the v1.25.1 pass — read them as the scaling shape (HNSW stays flat, brute-force grows linearly), not current absolutes.
Semantic search embeds the query via ONNX (~55ms) then searches stored vectors. HNSW (usearch) replaces brute-force O(N) scan with O(log N) approximate nearest neighbor search:
HNSW stays constant ~3ms regardless of index size. Brute-force grows linearly. Total semantic search latency is dominated by ONNX embedding (~55ms), so end-to-end speedup is modest for small codebases but critical at scale.
LLM Token Efficiency
When an AI agent searches code, the output goes directly into the context window. Grep-based tools return every text occurrence — including comments, strings, variable usage, and matches in minified files — consuming tokens without adding signal.
vex returns only symbol definitions in a compact one-line format, drastically reducing token consumption:
Example — searching for a class name on a large project:
For an agent making 10-20 code lookups per task, vex saves 5,000-20,000 tokens per session compared to grep — reducing cost and leaving more context window for reasoning.
Supported Languages
19 languages indexed via tree-sitter. The capability columns:
- Binder — does
vex usages --strictresolve refs through an LSP-style scope chain (Phase 11.1)?cross-fileincludesuse/importresolution;in-fileresolves within a file but treats imports as unresolved. The remaining languages fall back to the line-based scanner used by plainvex usages. - Patterns — does
vex patternget the v6 indexed prefilter (Phase 11.4)?indexedmeans a persisted skeleton section narrows candidate files at query time;live-scanmeans tree-sitter walks every lang-matching file on each query. All 19 languages work withvex patternsyntax ($NAME,$$$BODY,&&/||); the prefilter just speeds up discovery for the 15 languages that emit a skeleton section (every language except Bash, Lua, YAML, and TOML).
See docs/SUPPORTED_LANGUAGES.md for grammar
versions, ABI level, and the runbook for adding a language or upgrading a
grammar. Adding a language to the indexed-Patterns tier is one
allowlist edit in src/pattern/skeleton/kinds.rs — the Phase 11.4
follow-up promotion (Go → Java → Kotlin → C# → C++ → Swift → PHP →
Ruby, plus SQL / Markdown / CSS / HTML) is complete; only Bash, Lua,
YAML, and TOML remain on live-scan.
Index Location
Each project gets its own index based on a hash of the canonical project root path (xxh3). Overrides:
--cache-dir <path>— point vex at a custom cache directory$VEX_CACHE_DIR— environment variable (lower precedence than--cache-dir)cache_dirin.vex.toml— configuration file (lowest precedence)
Known limitations
vex is a static-analysis tool — some real call sites and references are invisible by construction. The headline gaps:
vex callersoutside function scope — Module-level expressions are reported via synthetic<module:path>callers (Phase 14.1). Python + Java function/method decorators (Phase 14.2), Kotlin annotations + C# method/constructor attributes (Phase 14.2.2), and TypeScript method decorators + Rust outer attributes on fns/methods (Phase 14.2.1) emit forward edges. Remaining gap: class-level decorators (Phase 14.6); Rust#[derive(...)]is intentionally filtered.vex usagesquality depends on language. Rust / TypeScript / Python / C# / C++ get--strict(binder-resolved refs from the v5reference_edgessection, Phase 11.1). Other languages use a line-based identifier scan with a higher false-positive rate.- Dynamic dispatch is invisible. String-resolved factories
(
uvicorn.run("main:app")), task queues (celery_task.delay()), reflection (getattr(obj, name)()) — none of these produce edges. - Workaround:
vex grep '\bname\b'is the exhaustive textual fallback. Slower (~50 ms) but never misses a hit.
See docs/LIMITATIONS.md for the full coverage
matrix, repros, and recommendations per query type.
Troubleshooting
Surfacing internal warnings
Vex emits structured logs via the tracing crate at parse/store
boundaries — failed grammar loads, mmap reopens, manifest mismatches,
and so on. By default RUST_LOG is unset, so only the most critical
diagnostics make it to stderr.
When a search returns surprising results or an index command behaves oddly, raise the log level:
For what the search engine actually did (per-channel hit counts, fuzzy fallback engagement, applied filters), use the structured trace instead:
See docs/MCP-SCHEMA.md for the --why /
why: true JSON shape.
Integration
Claude Code (CLI Integration)
The recommended way to integrate vex with Claude Code is via CLAUDE.md rules (see below). Vex runs as a CLI tool — Claude Code calls it directly via Bash, no MCP server needed.
Setup:
Then add .vex.toml config for auto-update so Claude always searches a fresh index:
Multi-repo (v1.22.0): if Claude Code is working across several repos at once, drop a .vex-workspace.toml at the common parent and tell Claude to add --workspace to its vex calls — e.g. vex usages Config --strict --workspace to trace a symbol's references across every repo, or vex check Foo --workspace to see which repos define it. Results come back grouped by repo. See Multi-repo workspaces.
Claude Code (MCP Server)
Alternatively, vex includes an MCP server (vex-mcp) that exposes all commands as MCP tools. Note: Homebrew installs only vex (not vex-mcp). Since v1.11.2 a prebuilt vex-mcp binary ships in every release alongside vex for the three triples the build matrix covers: aarch64-apple-darwin (macOS Apple Silicon), x86_64-unknown-linux-gnu (Linux), and x86_64-pc-windows-msvc (Windows). Intel-Mac and other triples still require the source build below.
Easiest setup (v1.15.0+):
This runs claude mcp add --scope user --transport stdio vex --env VEX_ROOT=<root> -- <vex-mcp> for you (v1.27.1+). If the claude CLI is not on PATH, it prints that command instead and writes nothing. Releases before v1.27.1 wrote ~/.claude/claude_desktop_config.json, which Claude Code does not read; re-run the command after upgrading.
Manual setup:
VEX_DEVICE (v1.16.0) picks the GPU execution provider when the binary was built with gpu-cuda / gpu-directml / gpu-coreml — relevant when an MCP-driven index / update call rebuilds semantic embeddings on a large repo (51× CUDA / 29× DirectML over CPU on MiniLM-L6). auto is safe on CPU-only builds (degrades silently). Run vex gpu once to confirm the EP actually engages.
MCP Tools (28):
search— 3-way hybrid (structural + BM25 + semantic); acceptsfilter/include/exclude/kind/context_path/no_bm25/--why/ metadata filters / diff-scope (since/since_branched/changed_only)find_symbol— exact name lookupfind_similar— semantic search by free-form descriptionsimilar— nearest neighbors of an existing symbol (explainadds Jaccard + diff); diff-scopeduplicates— near-duplicate symbol pairs (explainshows what differs); diff-scopeshow— extract symbol body from source; Phase 13.3 truncation flags (signature_only/head/no_body/collapsed, mutually exclusive)outline— file structureusages— find all references to a symbol;filter_path/strict/whyimpact— delete-safety blast radius (verdict + per-channel evidence);depth/exclude_docsgrep— regex content searchpattern— AST pattern matching with metavar back-references; diff-scope;--whyimplementations— find types extending a base class/trait/interface (incl. generics); diff-scopesubtypes— transitive-down closure over extends/implements edges (direct children, grandchildren, …), depth-labelled; index-only (no live-walk fallback);depth/ diff-scopemodules— de-facto modules: clusters of symbols that call/reference each other (v9 index, computed on fullvex index); list clusters (label, size, cohesion, hubs) or passsymbolfor its cluster;limit/min_size/members/sort/ scope /workspace; empty result +empty_reasonon older indexes or--no-clusterscallers/callees— direct callgraph navigation (fast path via persistent index); diff-scopepaths— enumerate caller chains between two functionsreachable— transitive callers of a targettests_for— test functions that transitively cover a target (framework-labelled)diff— symbol-level diff between a git revision and the working treecheck— fast symbol existence checkbundle— unified multi-source bundle (mode: symbol | pr-impact | project), Phase 13 envelopeeval— ranking-evaluation harness (bench/min_ndcg), MCP defaultsjson: trueso agents get a structuredEvalReportcapabilities— machine-readable capability matrix (protocol_version,signals,bundle_modes,history_diff(v1.16.0),symbol_clusters, etc.)index/update— build/rebuild index; v1.16.0 addsgpu: bool/device: cpu|auto|cuda|directml|coremlargs (GPU-enabled builds only) so an agent can opt into GPU semantic embedding per-call without touching env or configstatus— index statistics (now includesgpu_support/default_device(v1.16.0))history— historical versions of a symbol across commits (MCP tool since v1.20.0, D5);depth/limit/since/until/author/kind/diff/exact_presence
Note:
vex historyandvex tests-forwere promoted to first-class MCP tools in v1.20.0 (D5); earlier docs that calledhistory"CLI-only" are stale. Both emit the same--format jsonenvelope as every other vex command.
Multi-repo (v1.22.0): eleven tools —
search,grep,check,usages,impact,callers,callees,reachable,modules,index,update— take aworkspace: booleanarg that fans the call across every.vex-workspace.tomlmember, returning the grouped{workspace, repos:[...]}payload understructuredContent.results. Pointproject_rootat or above the.vex-workspace.toml.find_symbolis excluded (usecheck/search);whyis ignored in workspace mode. Seedocs/MULTIREPO-PHASE8-mcp.md.
MCP ↔ CLI parity (v1.10): the schemas now mirror the CLI surface for every path-aware tool. Glob filters (include / exclude), substring filter, kind boost, context_path proximity hint, no_bm25, Phase 13.3 truncation, diff-scope, and no_stale_check are exposed everywhere the CLI accepts them — agents no longer need to drop to bash for "Rust files under crates/api/ since main"-style scoping.
The schemas follow a canonical vocabulary (query / symbol / symbols / path / pattern / filter / include / exclude); pre-v1.7 aliases (name, file, names, etc.) still work and emit _meta.deprecated_args: [...] in the JSON-RPC response. Malformed JSON-RPC input now returns the spec-compliant -32700 Parse error response (v1.9.2 fix) with a 512-codepoint echo of the offending line in the data field; broken-pipe / EOF on stdin cleanly shuts down the server instead of dropping in-flight tool calls. See docs/MCP-SCHEMA.md.
For other MCP-compatible clients (Cursor, Codex CLI, Windsurf, Cline, Continue.dev, Zed), see Other MCP Clients below — same vex-mcp binary, different config files.
Other MCP Clients
The same vex-mcp binary works with any MCP-compatible client. The binary install is identical to the Claude Code section above; only the per-client config file location and format differ.
One-line setup (v1.15.0+):
For file-based agents, vex mcp install reads your existing agent config, merges a single vex server entry without disturbing siblings, and writes back atomically. For Claude Code it runs claude mcp add instead of editing a file. Idempotent — re-running on a matching entry is a no-op skip (--force overrides). vex mcp uninstall --agent <X> removes the entry; vex mcp list enumerates current entries per agent. The config files documented below for the other agents are exactly what vex mcp install writes — keep integrations/ handy for manual edits, agents the auto-installer doesn't know yet, or anything more exotic than the default shape.
Copy-pasteable snippets for the most common ones live under integrations/:
Per-agent caveats (auto-approve flags, timeout overrides, agent-mode requirements) are documented in integrations/README.md.
MCP Registry (from v1.27.2): vex is listed in the official MCP Registry as io.github.tenatarika/vex. Each release attaches one MCP Bundle per platform (vex-mcp-<target>.mcpb, macOS arm64 / Linux x86_64 / Windows x86_64) that holds both vex-mcp and vex; an MCPB-capable client asks for the project root once and needs nothing else on PATH. Bundle installs are updated by the client, not by vex self-update (which refuses to run inside a bundle).
Agent Recipes & Workflows
Once vex-mcp is wired into your agent, the next question is what to ask the agent so it picks the right tools in the right order. docs/COOKBOOK.md is a recipe collection for the common chains — code archaeology, cross-file refactor with usages --strict verification, PR-impact analysis via bundle(mode="pr-impact"), dead-code & duplicate cleanup, and multi-repo orchestration. Each recipe shows the tool sequence, the why of the ordering, and a phrase that reliably triggers the chain in agent prompts.
Documentation & Integration:
- Full vex documentation and API reference: https://context7.com/tenatarika/vex (on Context7)
- Agent skill reference:
.claude/skills/vex/SKILL.md(included in the project)
Shell Integration
CLAUDE.md Integration
Add this to your project's CLAUDE.md to make Claude Code use vex instead of grep:
Testing
Unit & Integration Tests
(nextest ≥ 0.9.145 is recommended — older versions report spurious LEAKs on macOS; update with cargo nextest self update.)
Test coverage includes:
- Per-language grammar regression (NEW):
tests/<lang>_query_test.rsfor all 19 supported languages — catches ABI mismatches and AST node renames when a tree-sitter grammar crate is upgraded - Binary format: roundtrip, corrupted/truncated/wrong-version rejection, out-of-bounds access, string pool dedup, empty index
- Adversarial format: 20 crafted index tests — overflow offsets, bad magic/version, alignment attacks, truncated records
- Vectors: write/read roundtrip for 384-dim f32 embeddings
- FST: refs FST roundtrip, prefix search, symbol FST exact/prefix/fuzzy search
- Search: structural, fuzzy (Levenshtein), RRF fusion, reranking with kind/path/proximity boosts
- Reranking stress: NaN/Infinity/zero scores, 10K results, edge context paths
- Property-based (proptest): rerank preserves length, sorted output, no NaN/negative scores, fusion commutativity
- Incremental update: unchanged reuse, deleted removal, file rename, symbol move between files, empty file
- Concurrency: parallel index/update (lock serialization), concurrent readers, read during reindex
- Multi-language: Rust, Python, Go, Kotlin, TypeScript, C++, cross-language same-name, wrong extension, 1K-symbol file, deep nesting, error recovery
- Unicode: BOM, mixed CRLF, unicode identifiers, null bytes, empty/whitespace files
- Path edges: spaces in paths, deep nesting (20 levels), symlinks, absolute vs relative, Windows backslashes
- Callgraph: callers/callees for Rust, Python, Go, TypeScript, Java
- Persistent call graph (v1.5): format v4 roundtrip, callers/callees FST lookup, dedup, same-name-across-files isolation, same-name-within-file disambiguation, incremental update preserves edges, fallback to live scan for v3
- Similar/duplicates (v1.5): self-exclusion, threshold filtering, canonical pair dedup, body-length filter, empty-index handling
- Pluggable embedder (v1.5): registry lookup, mismatch detection (incl. back-compat for pre-9.1 manifests), config + CLI priority, writer variable
vector_dim - BM25 channel (v1.5): writer/reader roundtrip, pipeline emission, IDF discrimination, short-doc preference, 3-way RRF with Hybrid labeling, MatchType tagging, unicode tokens
- Staleness: git HEAD comparison, dirty file detection, mtime fallback
Fuzz Testing
Fuzz tests exercise every parser that consumes untrusted input — the binary index format, sidecar files, the user-facing pattern grammar, and the JSON manifest — using cargo-fuzz (libFuzzer + AddressSanitizer):
Eighteen fuzz targets cover the reader's unsafe paths plus every text /
sidecar parser that takes adversarial input:
Most recent system-wide audit (Q4-A/B closure, 2026-06-17): ~76M
total executions across all 11 targets that existed at the time, 0 crashes / panics /
AddressSanitizer hits / leaks (61s per target, libFuzzer +
ASan + nightly). One latent FST panic was caught en route in a
dead-code path on adversarial ref_edge bytes and fixed before the
clean run — find_ref_edges_by_symbol and RefReader::find_by_prefix
now wrap the inner FST walk in catch_unwind so a corrupt sidecar
returns Err instead of taking down the process. Earlier baselines:
v1.14.1 (2026-06-05) ran 5.8M iterations across 9 targets clean;
v1.15.2 release-gate (2026-06-08) ran ~853k focused executions on
the four highest-signal targets clean.
Fuzzing has found and fixed eight real defects across the project life:
- v1.x: out-of-bounds read on crafted
symbol_count, misaligned pointer dereference on oddsymbols_offset, unchecked section offsets exceeding file size (binary reader hardening). - v1.12.0:
SymbolBloom::loadaccepted a sidecar withn_bits = 0k_num = 0whose consistency guard passed but later panicked insidebloomfilter::Bloom::checkonhash % 0. Fix: reject degenerate sizes during load.
- v1.12.0:
SymbolBloom::loadacceptedk_numup to ~2.1B, which made everymay_containcall loop for 110+ seconds (DoS, not a panic). Fix: capk_num <= MAX_K_NUM = 64at load time. - Phase 11.1.10 / Q4-A (2026-06-17): FST walk inside
find_ref_edges_by_symbolpanicked on a crafted refs FST, bypassing the productioncatch_unwind(the libfuzzer-sys panic hook fires before user code can intercept). Fix: wrap the inner walk in its owncatch_unwindand surfaceErr. Defense-in-depth hardening was also applied toManifest::load: a 128 MiB pre-read size cap blocks hostile JSON before serde can allocate multi-GB heap (Q4-B audit follow-up; defense-in-depth, threat model is user-owned files). - v1.23.0: a 451-byte malformed Kotlin input drove tree-sitter's GLR
error recovery into super-linear time and memory (334 s, >2 GB; DoS, not
a crash; found by
fuzz_kotlin_binder). Fix: every production parse goes throughparser_pool::parse_text, which caps progress-callback invocations (scaled by input size) for all languages. - v1.23.0:
tree_sitter::Node::utf8_text()panicked on malformed input where tree-sitter emitted a node past EOF (found byfuzz_kotlin_binder). Fix: the bounds-checkedNodeTextExt(node_text/node_text_opt) replaces rawutf8_textin the extractor, binders and pattern prefilter.
The v1.13.0 / v1.14.1 additions found no defects in fresh code — the
review-driven MAX_COUNT guards on hash_index::save / load were
added as defence-in-depth before the fuzzer ran (rust-reviewer +
code-reviewer flagged the truncating as u32 cast on save), and the
sustained 3M / 5.8M iteration runs confirmed they hold.
Architecture
- No SQLite — custom binary format v6, zero-copy mmap reads; readers accept v3+ for backwards compatibility
- Symbol FST — persistent inverted index, O(query_len) lookup
- Refs FST + ref_edges — symbol references as FST + cross-file edges resolved at write time (Pass-2 in
store::writer); enables refactor-gradeusages --strict - Persistent call graph —
CallEdgerecords + a name-keyed callers FST + a dense callees CSR index (v9+; FST on older indexes), built at index time, ~4ms lookup vs seconds of live tree-sitter scan - BM25 channel — Okapi BM25 over
body_tokens, auto-on when section present - HNSW — approximate nearest neighbor via usearch, O(log N) semantic search; hash-keyed entries for content-stable IDs across re-indexing
- Pluggable embedder —
Embeddertrait + registry, identity recorded in manifest with mismatch detection at search - History index — symbol presence per commit + MinHash-based rename chain tracking (closes LIMITATIONS §4c #2 for 1:1 renames)
- Parallel parsing — rayon with 500-file chunks; blob-SHA parse cache (shared across projects in the user cache root) skips re-parse of unchanged files across re-indexes
- Incremental updates — content hashing via xxh3;
vex updatere-parses only changed files (unchanged symbols + call edges reconstructed from existing index) - Watch mode —
notifycrate with 500ms debouncing - N-way RRF fusion —
fuse_manymerges structural + BM25 + semantic ranked lists, marks cross-channel hits asHybrid - Ranking eval harness —
vex evalover a bundled golden set; CI regression gate on mean nDCG@10 + per-query-type floors + per-channel attribution (Phase 13.12 / 13.12.1)
License
MIT
Source: README.md at commit 35bc9c4
Tools
0Version history
1- v1.27.2LatestOct 3, 2026

