semantic code intelligence for coding agents
Know what breaks before your coding agent changes it.
Kivgraph gives Claude Code, Codex and other coding agents exact symbol references, dependency paths and cross-repository change impact — from a graph on your own machine, over MCP.
Supported MCP clients: Claude CodeCodexOpenCodeClaude DesktopOh My Pi
$ curl -fsSL https://github.com/Luqueee/kivgraph/releases/latest/download/install.sh | bash- local only
- MCP over stdio
- no model, no API key
- Apache-2.0
ask without a name
The one tool that does not need you to know what it is called.
which code refuses to publish a generation when the disk is nearly full
- internal/storage/generation/store.go
- internal/storage/generation/types.go
Not one identifier in that question. 4 of its words match nothing in the graph — which, the, disk, nearly — and the rest carried it. The rows are text that looked alike, not edges a checker resolved, so they are where to start reading rather than the answer.
what your agent can ask
One symbol, three questions, and the answer your agent gets back. Every file count, hop, edge kind, line range and token figure below is from one run of the published benchmark, and each panel names the question it answers.
The corpus behind it is 37 private repositories, so the repository, file and symbol names are substituted. The structure is not: same repositories, same row in each, same language, same numbers. Every figure here can be checked against results-all.json in the repository.
less context, same exact answer
The alternative to a code graph is not being wrong. It is spending the context. So the benchmark's sixth arm is grep and reading the files — what an agent already does — and both arms are scored against a ground truth written by hand, question by question.
- 35,961
- tokens — Kivgraph graph queries
- 267,980
- tokens — grep + file reading
- 28 / 29
- exact answers, both arms
context saved
~87% fewer tokens
Twenty-nine questions over a 37-repository corpus, one pass. Both arms answered 28 of them exactly; the answers were scored against a hand-written ground truth, not against each other. See the method.
- questions
- 29 across six sets
- corpus
- 37 git repositories
- version
- kivgraph 0.5.0
- commit
- 954b9eb
- tokenizer
- o200k_base
- Precision was 1.00 on both arms. Kivgraph missed one TypeScript test file; grep missed one cross-repository consumer that never spells the symbol.
- The median question cost 5.95x less through the graph, with the widest margin at 86.8x. grep was cheaper on five of the twenty-nine, between 0.26x and 0.53x, and every one of those five was a question both arms answered fully.
- One corpus, one machine, one tokenizer. o200k_base is a proxy for the tokenizer an agent actually pays, so the ratio between the rows is the claim and the absolute values are not.
The harness, the hand-written ground truth and the verbatim response of every call are published beside the numbers inbenchmarks/graph-tools-comparison/, so any figure here can be checked against the bytes it came from — including the five where we lose. The five-way comparison against four other code graphs is a separate pass and is reported separately: explore the benchmark.
grep finds text, Kivgraph resolves symbols
Kivgraph does not replace grep. It answers the structural questions grep cannot answer reliably — and it says so on the two rows below where a text search is simply the better tool.
The difference is not thoroughness, it is evidence. An edge in this graph comes from the language's own checker, so an empty list of references is a claim: nobody calls it. A search that returns nothing has not made that claim, and cannot.
| question | grep | kivgraph |
|---|---|---|
| Find a string anywhereNot what the graph is for; it indexes declarations, not lines. | yes | no |
| Answer a rare name in a small treeT1_go_trivial — two occurrences in the corpus, and grep at 0.53x. | yes, cheaper | yes, dearer |
| Tell two homonyms apartH1–H5 — a method, an interface, a type, an alias and a Rust trait impl, each with a namesake elsewhere. | no | yes |
| Follow callers without opening the filesR1_ts_xrepo — grep needed eight calls and 10,054 tokens; one query needed two calls and 332. | manually | yes |
| Reach transitive impactI1–I3 — two and three hops. Inside one package grep is still cheaper; across packages it stops being. | one hop at a time | yes, bounded by depth |
| Find consumers in another repositoryX1_ts_shared_enum — grep found two of three consumers and missed the re-export. | only if they spell the name | yes |
| Prove that nothing calls itA1–A3 — grep is three to four times cheaper on these and still cannot distinguish `nobody calls it` from `my pattern missed it`. | no | yes |
a name is not a symbol
withBackoff is declared seven times in the benchmark corpus, in five repositories and two languages — two of them the same file path in two different repositories. The graph holds twenty-two symbols with that name — the other fifteen are TypeScript barrel re-exports — and the identifier appears in twenty-two files. Identical text, seven different symbols.
That corpus is private, so the repository, file and symbol names below are substituted; the seven declarations, the five repositories, the language split and the line numbers are the captured ones. The 129-token cost of the refusal was measured against the real identifiers — a token count is a function of the strings it encodes. The two transcripts on the right are literal: they come from Kivgraph's own repository.
one name, as a text search sees it
- withBackoffconfig-libsrc/retry.ts:41ts
- withBackoffmedia-serviceinternal/secrets/provider.go:240go
- withBackoffdata-serviceinternal/secrets/provider.go:143go
- withBackoffclient-sdksrc/managers/command.ts:19ts
- withBackoffdata-serviceinternal/storage/retry.go:49go
- withBackoffplatform-libsrc/util/retry.ts:135ts
- withBackoffclient-sdksrc/types/result.ts:91ts
seven declarations, one spelling — a search cannot rank them, and choosing the likeliest is how a graph invents an edge
tools/call find_references{"name":"withBackoff"} AMBIGUOUS_SYMBOL: name "withBackoff" declares 7symbols; repeat with the repository and path ofthe one you mean: config-lib:src/retry.ts:41, ... # 129 tokens, and the answer is the next calltools/call find_cross_repo_consumers{"repository":"kivgraph","path":"internal/facts/facts.go", "qualified_name":"MergeAll"} # total 0 returned 0 consumers nullguidance: no repository in the published graph consumes this symbol. Check graph_status if a consumer is registered but was not indexed, and find_references for uses inside its own repository- Edges come from go/types, the TypeScript checker, rust-analyzer and the Dart analysis server. Never from matching a name, a path, an alias or a single plausible candidate.
- Asked by a name that means seven things, the server refuses and names the seven. It costs 129 tokens and the next call is a copy of one row.
- Two homonymous symbols are two symbols, with different kinds, different packages and different ranges.
- A reference that cannot be resolved is published as UNRESOLVED with its reason, repository and language rather than dropped, and CANDIDATE is a different result from EXACT.
your repositories are not isolated
An agent reads one repository at a time. The change it is about to make does not stop there. Kivgraph registers every repository of a workspace into one graph, keeps each one's identity inside the keys it publishes, and answers who consumes a symbol from outside the repository that declares it.
This is the question with no cheap substitute. A text search finds a consumer only if that consumer spells the name, and a language server stops at the edge of its own workspace.
platform-lib · @workspace/platform
declares
StatusCode
src/errors/status.ts:29
client-sdk · @workspace/sdk
consumes
export *
src/index.ts:1
- the edge
- REEXPORTS, EXACT_TYPECHECKED, resolved by the TypeScript checker of the providing package — not by the two packages sharing a name.
- what grep saw
- Nothing. The consuming file never spells the symbol, so no text search reaches it. Two of the three consumers were found; this was the third.
- what a language server sees
- Its own workspace. A cross-repository consumer is outside it, so the question cannot be asked there at all.
- kept apart on purpose
- The same answer carried 22 package-level rows. They are reported separately and never counted as uses: a package depending on a package is not a symbol using a symbol.
works where you already code
One MCP server over stdio, and the same structural context in every client that speaks the protocol. kivgraph mcp install detects the agents on the machine, lets you pick, and writes the entry — with a backup beside the file it edits and --dry-run to see the change first.
$ kivgraph mcp install --scope userThe scope is user or project. Nothing is installed inside a repository you registered, and the command neither initialises Kivgraph nor indexes anything.
- Claude Code
--target claude-code - Codex
--target codex - OpenCode
--target opencode - Claude Desktop
--target claude-desktopuser scope only, and the one target with no local skill install
- Oh My Pi
--target oh-my-pi
Eleven tools are registered once a generation is published. Before that the server registers only index_project and says so, rather than offering ten tools that would have nothing to answer from.
start with your workspace
The installer puts the bundle in ~/.local/opt/kivgraph with launchers in ~/.local/bin, verifies the release checksum and then the bundle's own, and stops there: it configures no client, writes no configuration and indexes nothing. These four commands are the rest.
Register, check, index, connect. Nothing answers until a generation exists, so that order is the whole setup.
register a repository
$ kivgraph init \ --repository project=/absolute/path/to/project \ --languages go,typescript,rust--repository NAME=PATH may be repeated. The name is compared exactly, and it travels inside the stable keys of everything the repository declares.
check the machine
$ kivgraph doctordoctor reports the configuration, the toolchains it found and the state of the published graph. It names the language version ceiling this binary type-checks Go with, which is not the go on your PATH.
index and publish
$ kivgraph index --fullThe pass analyses every registered repository, validates the canonical graph and publishes it as a new generation. Publication is atomic: a candidate that fails integrity or validation never becomes CURRENT, and the previous generation keeps serving.
register the client
$ kivgraph mcp install --scope userDetects the coding agents on this machine and writes the entry after you select them. Supported targets: claude-code, claude-desktop, codex, opencode and oh-my-pi.
or wire a client by hand
{
"mcpServers": {
"kivgraph": {
"command": "/home/user/.local/bin/kivgraph",
"args": [
"serve",
"--config",
"/home/user/.config/kivgraph/config.yaml"
]
}
}
}serve is stdio and opens no port. It writes MCP framing to stdout and logs to stderr, and it follows the published generation: an index --full in another terminal cannot leave it answering from a graph that no longer exists on disk.
before you start
- Runtime for the installed bundle: Bash, Node.js 22 or later,
curl,tar, andsha256sumorshasum. Published platforms arelinux/amd64anddarwin/arm64; the installer refuses an Intel Mac by name rather than installing a bundle that cannot run. - Per language, and only for the languages you register: indexing Rust needs
cargoon thePATH— the bundledrust-analyzercannot load a Cargo workspace without it — indexing Python needs Python 3.10 or later, and indexing Dart needs the Dart or Flutter SDK. index_projectis the only tool that writes. Withoutconfirmed=trueit refuses, and it never writes inside the source projects.
tools/call index_project # confirmed unset PERMISSION_REQUIRED: user approval is required; confirm theoperation before setting confirmed=truelanguages
One authority per language, and it is the language's own. An edge is never created from a name — but the five are not equal, and the difference is printed on each row rather than left for you to discover after indexing.
Go
type-checked
go/types, linked into the binary
Packages are loaded through go/packages with the build tags you configure, and resolution comes from a full type universe. The unit of analysis is the module.
The binary type-checks only up to the language version of the toolchain that built it. A module above that ceiling is rejected by name.
TypeScript
type-checked
the @kivgraph/ts-worker tsserver process
Each package.json project is analysed by the TypeScript checker itself. A cross-package symbol is placed by the provider's declaration map, or by asking the provider's own checker what its module exports.
Rust
type-checked
rust-analyzer scip, bundled and pinned
The authority is rust-analyzer, invoked once per Cargo workspace. Tree-sitter contributes the syntactic class of a use and the declared visibility, never identity.
core, std and alloc are not indexed, so derive, operator overloading, ? and standard-library calls produce no edges.
Dart
analyzer-resolved
the Dart or Flutter Analysis Server
Dart and Flutter source is indexed through the SDK analysis server. Local declarations, navigation references and calls are published with analyzer evidence, while external packages are reported as unresolved unless their source is indexed.
Tests and generated files are excluded by default and can be enabled in configuration.
Python
exact only with a configured analyzer
the configured semantic worker or bundled AST worker
A configured semantic provider — a Pyright-compatible language server — can publish exact declarations and references. Without one, the bundled worker covers Python and stub files without guessing dynamic names, and everything it infers stays CANDIDATE rather than becoming EXACT.
Dynamic imports, monkey patching and unresolved runtime attributes remain explicit candidates or unresolved facts.
see what the agent sees
Every answer on this page comes out of one immutable snapshot, published atomically and never queried from the database behind it. That snapshot is inspectable two ways: as the numbers below, and as a read-only 3D view of the same structure. It is a debugging surface, not the product — but it is the fastest way to see whether the graph your agent is answering from is the graph you meant to build.
kivgraph ui serves that view. It binds 0.0.0.0:7777 by default and carries no authentication, so on a shared network restrict it with --addr 127.0.0.1:7777; what it exposes is repository paths, file paths, symbol names and signatures. Its palette is the one the transcripts on this page borrow: one colour per node kind, plus one for an exact, type-checked edge.
- repository
- package
- file
- symbol
- exact dependency
graph_status
- statusready
- snapshot_id30
- snapshot_built_at2026-08-15T11:14:10Z
- schema_version2
- resolver_version0.5.0
- repositories2
- packages57
- files311
- symbols10957
- edges40125
- package_edges123
- unresolved1642
edges_by_kind
- CALLS_DIRECT6198
- EXPORTS312
- PASSES_AS_CALLBACK75
- ASSIGNS_FUNCTION22
- IMPLEMENTS9
- EMBEDS8
- EXTENDS4
list_repositories
| name | languages | indexed_branch | indexed_commit | state |
|---|---|---|---|---|
| kivgraph | go, typescript | main | d67bc0e | moved false |
| mole | go, typescript | main | 4cc05cd | indexed_dirty true |
The 1642 unresolved references are published with their reason, repository and language rather than dropped, and they are counted apart from the 40125 edges. graph_status also reports whether a repository has moved since it was indexed, which is the answer when a result looks stale.
frequently asked questions
Does Kivgraph replace grep?
No, and the benchmark says where it should not. grep is cheaper for a rare name in one small repository: over the 29 published questions it cost fewer tokens on five of them, between 0.26x and 0.53x of ours, and both arms were fully correct on all five. Kivgraph is for type-checked references, transitive impact, consumers in another repository, and empty answers you can rely on.
Does my code leave my machine?
No. The server is a local process that speaks MCP over stdio, and no query path makes a network call. One thing to know: kivgraph ui, the optional viewer, opens a local HTTP listener that binds 0.0.0.0:7777 with no authentication and exposes repository paths, file paths, symbol names and signatures. On a shared network, start it as kivgraph ui --addr 127.0.0.1:7777.
Does Kivgraph need an LLM, an API key or an embedding step?
None of the three. The edges are resolved by go/types, the TypeScript checker, rust-analyzer and the Dart analysis server, all running locally. There is no model in the indexing path and no key to configure.
How is this different from a vector database over my code?
A vector index answers what looks similar; this answers what is connected, and each edge records the analyzer that resolved it. The difference shows up in an empty answer: a similarity search always returns its nearest neighbours, whereas an empty reference list here is a claim that nothing calls the symbol.
How does cross-repository analysis work?
Every registered repository is indexed into one graph, and each keeps its identity inside the keys it publishes, so a symbol used from outside the repository that declares it is a cross-repository consumer with its own evidence. In the published benchmark this found three of three consumers of one symbol where grep found two: the third re-exports it and never spells its name, so no text search reaches it.
Which languages are supported, and are they equal?
Go, TypeScript, Rust, Python and Dart, and no, they are not equal. Go, TypeScript and Rust edges are type-checked. Dart edges are resolved by the Dart Analysis Server. Python publishes exact semantic facts only when a configured Pyright-compatible analyzer provides them; with the bundled AST fallback its inferred references stay CANDIDATE and never become EXACT.
What happens when Kivgraph finds no references?
It says so, and that is the answer rather than a failure. A reference that could not be resolved is published as UNRESOLVED with its reason, repository and language instead of being dropped, and CANDIDATE is reported as a different result from EXACT — so an empty list means nobody calls the symbol, not that nothing was found.
How much does indexing cost, and how often must I re-index?
A full rebuild is the only path; there is no incremental mode. Measured cold on the benchmark corpus of 37 repositories and 96,482 published symbols, one pass took 37.6 seconds and left 1,423 MB of on-disk state — the slowest and heaviest of the five tools compared. Re-index when a repository has moved; graph_status reports whether one has.
What can it answer before I have indexed anything?
Nothing, deliberately. With no published generation the server registers only index_project and reports that it has no graph to answer from, rather than offering ten tools whose answers would be empty for the wrong reason.
More answers in the Kivgraph FAQ, the limits and on GitHub.
Give your coding agent structural context.
One binary, one index, one MCP entry. Nothing leaves the machine, and every answer carries the evidence it was resolved from.
$ curl -fsSL https://github.com/Luqueee/kivgraph/releases/latest/download/install.sh | bash