How Canopy Works
The big picture
Section titled “The big picture”Your repo (files on disk) │ ▼ canopy index┌─────────────────────────────────────┐│ Indexing pipeline ││ ││ 1. File scanner ││ Walk the tree; honor .gitignore ││ and .canopyignore ││ ││ 2. AST symbols (tree-sitter) ││ Every declaration across 55+ ││ languages and formats ││ ││ 3. Import graph ││ Imports resolved to target ││ files — a directed graph ││ ││ 4. Tantivy full-text index ││ camelCase-aware chunk search ││ ││ 5. Vector embeddings ││ Semantic search over chunks ││ ││ 6. Git ingest (auto when .git/) ││ History for ownership answers ││ ││ 7. Tier-A file summaries ││ Optional: --with-summaries │└─────────────────────────────────────┘ │ ▼ stored locally in ~/.canopy/ │ ▼ canopy serve (MCP stdio, --watch)┌─────────────────────────────────────┐│ One tool: canopy_answer ││ ││ question → deterministic router ││ → one recipe ││ → whole chain server-side ││ → one compact answer │└─────────────────────────────────────┘ │ ▼ one tool call AI agent (Claude Code, Cursor, …)Two phases: build the index once, then answer questions against it. The per-question cost is a lookup, not a re-scan of the source tree.
Phase 1: The index
Section titled “Phase 1: The index”canopy index is incremental by default — first run processes everything, later runs
re-process only changed files. canopy index --full rebuilds from scratch.
(canopy setup runs the first index for you.)
The index has five layers:
| Layer | What it holds | Built |
|---|---|---|
| AST symbols | Every declaration — functions, classes, structs, types, exports — with file, line span, and signature, extracted via tree-sitter across 55+ languages and formats | Always |
| Import graph | Import statements resolved to their target files, stored as directed edges | Always |
| Full-text (Tantivy) | Code chunks with camelCase-aware tokenization: payment matches processPayment | By default (--no-search opts out) |
| Vector embeddings | Semantic search over chunks, powered by a local ONNX model or Ollama | By default (--no-embeddings opts out) |
| Git data | Commit history for ownership and churn answers | Auto when .git/ is present (--with-git forces, --no-git skips) |
--with-summaries adds an optional sixth layer: Tier-A file summaries, embedded
alongside the chunks. Summaries are heuristic (offline) by default; cloud backends are
opt-in.
Everything lands under ~/.canopy/ — a SQLite database for symbols, graph, and git
data, plus the Tantivy and vector indexes. Nothing is written into your repo, and
nothing leaves the machine.
Phase 2: The answer
Section titled “Phase 2: The answer”canopy serve starts the MCP server — JSON-RPC 2.0 over stdio, launched as a subprocess
by your agent. With --watch, it re-indexes changed files as you edit, so answers track
the working tree. The server exposes exactly one tool: canopy_answer.
Each call runs the same path:
- Route. A deterministic classifier maps the question to one recipe — no LLM, no network, 360/360 on its labelled regression corpus. Phrasing picks the recipe: the router keys on the entity type you name and on backticked paths and symbols.
- Execute. The recipe runs the whole retrieval chain server-side: symbol lookup, graph traversal, full-text or semantic retrieval, ranking, formatting. What used to be a multi-tool orchestration problem for the agent is a single internal pipeline.
- Answer. One compact, structured answer comes back — or an honest “No strong match in this repository for …” when nothing clears the relevance floor.
The agent makes one call and relays the result. That single-call design is the measured lift: +76% correctness at −69% tokens versus the shell grep/read loop, while a control agent orchestrating the old multi-tool catalog spent 2.3×–5.8× more than shell.
The same path is available without an agent: canopy answer "<question>" from the
terminal, repo auto-detected from the current directory.
Why local-first matters
Section titled “Why local-first matters”- Local index, local queries — no latency, no authentication, no network dependency for answers.
- One static binary — no Node.js, no Python, no daemon to manage.
- Incremental index — re-indexing fits inside a normal development loop.
Each machine has its own index; Team tier adds a CI index cache for sharing built indexes across CI runs.