One command turns your repository into a directed knowledge graph your AI coding assistant reads instead of blindly grepping. A single static Go binary — zero API keys, $0, fully offline, byte-deterministic. Parses Go, Python, JavaScript, TypeScript, Rust, Java, and PHP. Ships an LLM-free
query, an MCP server, Claude Code integration, an interactive offline graph viewer, and multi-repo workspace federation.
Languages: English · Русский · Français · Deutsch · Українська · Slovenščina · Italiano · Español · 中文 · 日本語 · 한국어 · العربية · Português · Türkçe · Bahasa Indonesia · Tiếng Việt · हिन्दी · 繁體中文 · Polski · ไทย · עברית · বাংলা · اردو
🌐 Website: https://amazopic.github.io/graffiti/
$ graffiti .
✓ Done. 0 API calls, $0. 214 files → 1,883 nodes, 4,102 edges, 12 communities.
The 3 most interesting questions your map can answer:
1) Which module is the load-bearing wall?
2) What does the auth flow touch?
3) Where are the cross-package call hotspots?
An AI coding assistant is only as good as what it can see. Drop it into a large repo and it does what you'd do with no map: it greps, opens a few files, guesses. It never sees the shape of the code — which function calls which, where a type is defined, which module is the load-bearing wall.
graffiti is the map that should have been there. One command parses the repo with tree-sitter, resolves the edges, clusters the modules, and writes a graph — as JSON for the machine, as Markdown for you, and as a single offline HTML you can actually look at. No keys. No cloud. No cost.
curl -fsSL https://raw.githubusercontent.com/amazopic/graffiti/main/scripts/install.sh | shPin a version or directory:
GRAFFITI_VERSION=v0.1.0 INSTALL_DIR="$HOME/.local/bin" \
sh -c "$(curl -fsSL https://raw.githubusercontent.com/amazopic/graffiti/main/scripts/install.sh)"The installer picks the right static binary for your OS/arch, verifies its SHA256
against the release manifest, and installs it. Verify with graffiti version.
Or build from source (below).
No terminal needed — let Claude Code do the whole thing. Paste this one prompt
into a Claude Code session and answer y at each step. It fetches the right binary,
builds the map for your repo, wires up the integration, and opens the graph:
Install graffiti by amazopic for me. Download the right static binary for my OS/arch from the latest release at github.com/amazopic/graffiti (or build it from source with `make build` if Go is available), put it on my PATH as `graffiti`, and verify with `graffiti version`. Then run `graffiti .` at my repo root to build the map, run `graffiti init --hook` to wire graffiti into Claude Code, and finally open `.graffiti/map.html` so I can see the graph. Ask before each step.
# 1 — install (or build from source with `make build`)
curl -fsSL https://raw.githubusercontent.com/amazopic/graffiti/main/scripts/install.sh | sh
# 2 — map your repo (writes .graffiti/map.json, MAP.md, map.html)
cd your-repo
graffiti .
# 3 — look at the graph
open .graffiti/map.html # macOS — use `xdg-open` on Linux, `start` on Windows
# 4 — ask it questions: no LLM, no API key
graffiti query "where is the user authenticated"Then wire it into your AI assistant once:
graffiti init --hook # Claude Code: skill + CLAUDE.md + grep→query nudge
graffiti serve # or expose the map to any MCP client over stdioMore example questions — query returns a scoped subgraph within a soft
~2,000-token budget, so context stays small and cheap (quote the question):
graffiti query "login handler"
graffiti query "what does the checkout flow touch"
graffiti query "where is the cart fetched" ../shop # target another pathmake build # builds ./graffiti (CGO-free, ~10MB, 7 language grammars)
make test # runs the full test suite with the required build tags
make xcompile # cross-compiles static binaries for all targets into dist/The grammar_subset build tags ship only the grammars graffiti supports (Go,
Python, JS, TS, Rust, Java, PHP, plus go.mod) via the pure-Go runtime
github.com/odvcencio/gotreesitter (no CGO, no WASM). They keep the binary at
~10 MB; without them the code still compiles but links the full grammar set
(~31 MB). Always pass them — the Makefile does this for you.
| Language | Extracted |
|---|---|
| Go | files, functions, methods (by receiver), types, imports, resolved calls |
| Python, JavaScript, TypeScript, Rust, Java, PHP | files, functions, classes/structs/interfaces/enums/traits, methods (Class.method), imports, intra-repo calls |
| Markdown | doc nodes |
Non-Go extraction is intentionally honest: it captures the common, high-value structure and under-extracts exotic constructs (decorators, generics, nested definitions, dynamic dispatch) rather than emitting guesses.
graffiti . # build the map for the current repo
graffiti build <path> # build the map for <path>
graffiti <path> # shorthand for `build <path>` when <path> is a directory
graffiti update [path] # rebuild the map (full rebuild for now)
graffiti query "<q>" [path] # LLM-free scoped subgraph retrieval (soft token budget)
graffiti serve [path] # MCP server over stdio (JSON-RPC 2.0)
graffiti init [--user] [--hook] # install Claude Code integration
graffiti version # print the versionRun graffiti with no arguments for the full command list.
graffiti . writes everything into <repo>/.graffiti/:
map.json— the graph itself: nodes, edges, communities, schema-checked againstschema/map.schema.json. This is what your AI reads and whatqueryand the MCP server traverse.MAP.md— a human-readable digest: top modules, the most-connected nodes, and the three most interesting questions your map can answer.map.html— a single self-contained, offline, interactive force-directed graph. No CDN, no server, no network — just open the file.
map.html has a 2D/3D toggle (hover lifts a node and its neighbours), node
search, click-to-copy file:line, sector zones, client / tests /
external category toggles, and a resizable project → directory → file tree
with show/hide checkboxes. It's CSP-safe and works entirely offline.
A per-file content-hash cache lives under <repo>/.graffiti/cache/, so re-runs
only re-parse what changed.
graffiti init # install the skill + CLAUDE.md block (project)
graffiti init --hook # also install the PreToolUse nudge (grep → graffiti query)
graffiti init --user # install into ~/.claude instead of the repograffiti init writes:
.claude/skills/graffiti/SKILL.md— a short skill so Claude Code knows to build/read/query the map.- a
CLAUDE.mdblock (between<!-- graffiti:start -->/<!-- graffiti:end -->) telling the assistant to prefergraffiti queryover grep when a map exists. - with
--hook, a.claude/settings.jsonPreToolUse entry runninggraffiti hook, which adds a one-line nudge beforeGrep/Globwhen.graffiti/map.jsonis present. The hook never blocks a tool.
It is idempotent — re-run any time; existing CLAUDE.md / settings.json content is preserved.
graffiti query "login handler" # scoped subgraph for the current repo
graffiti query "where is cart fetched" ../shopquery returns a relevant slice of the graph within a soft ~2000-token node
budget — no model, no embeddings. Quote the question.
graffiti serve # MCP over stdio (JSON-RPC 2.0)Point any MCP-capable client at it and your assistant traverses the graph through tools instead of grepping.
Lay separate repos side by side and query across them — without merging:
graffiti link ../frontend ../backend # federate (builds members if needed)
graffiti query --workspace "where is the cart fetched and served"
graffiti serve --workspace # MCP over the federation
graffiti update --workspace # rebuild changed members + recompute links
graffiti workspace render # → .graffiti-workspace/workspace.htmlgraffiti link writes a committable registry (.graffiti-workspace/workspace.json)
and a derived, gitignorable cache (.graffiti-workspace/overlay.json). Each repo's
own .graffiti/map.json is unchanged and still works standalone — the workspace is a
thin computed overlay, never a merged blob.
Cross-project links: assert them explicitly in .graffiti-workspace/links,
one per line — frontend::main-go:fetchcart -> backend::main-go:getcart calls
(# comments allowed; endpoints are alias::nodeid). graffiti links check validates
both endpoints resolve; graffiti federate --explain lists every link. Federated query
prefixes each node with its member alias and traverses cross-links. graffiti workspace render writes a workspace.html — the same force-graph viewer with the projects as
the top level of the tree and cross-project links drawn.
Add .graffiti-workspace/overlay.json to .gitignore (it is derived and recomputable).
A microservice system is many independent repos that form one product. graffiti
maps each, then discovers the edges between them — HTTP, gRPC, queues — from
each service's contract surface (what it provides and consumes). No
hand-wiring: each service publishes its own map; the orchestrator federates the
published artifacts and matches consumers to providers.
# in each service's CI (or locally) — publish its map into a shared store:
graffiti publish --to ../system-store --as carts
# then, in CI or on demand, over the whole system:
graffiti system build # federate + auto-discover cross-service links
graffiti system render # → .graffiti-system/system.html (services as lanes)
graffiti system impact carts::"GET /carts/{}" # who breaks if this changes?
graffiti system audit # dangling consumers · orphan providers · ambiguous (CI gate)
graffiti system query "where is the cart fetched and served"Each map carries a contract surface extracted from openapi.json, .proto,
framework routes, queue calls, or an explicit graffiti.contract.json. Cross-service
links are scored by confidence; ambiguous and dangling (dead-endpoint)
consumers are reported, never silently dropped. The system store is just a directory
or git repo — $0, offline, recomputable. See the
design doc.
Say your services live in one parent folder, each in its own directory:
myproject/ ← parent folder = the shared "system store"
├── orders/ ← a service (Go)
├── web/ ← a service (React/TS)
└── payments/ ← a service (Python)
1. Build and publish each service into a store at the parent folder (--to .).
publish reuses an existing map, so build first to pick up code changes:
cd myproject
for d in */; do
d=${d%/}
graffiti build "$d" && graffiti publish "$d" --to .
doneThe service name defaults to its folder name; override with --as <name>.
⚠️ On re-publish:publishdoes not rebuild an existing map. After changing code, always rungraffiti build <service>first (the loop above does) and thenpublish— otherwise you publish a stale map.
2. Build the system graph — federate the maps and auto-discover the links:
graffiti system build
# ✓ System "myproject": 3 services → 7 cross-service links (0 ambiguous, 0 dangling, 2 orphan). 0 API calls, $0.3. Use it:
graffiti system render # → .graffiti-system/system.html (services as the top tree level)
graffiti system impact orders # who breaks if orders changes (direct + transitive)
graffiti system audit # dangling consumers · orphan providers · ambiguous (non-zero exit → CI gate)
graffiti system status # which services drifted since the last build
graffiti system query "where is the order created" # LLM-free retrieval across the whole system
graffiti system list # registered servicesWhat lands in the parent folder:
myproject/.graffiti-system/
├── system.json # the registry of services (commit this)
├── overlay.json # discovered links (derived — safe to .gitignore)
├── system.html # the visual system map
└── services/<name>/map.json # each service's published map
Improve link accuracy. Auto-detection covers Go (net/http, gin/chi/echo), Flask, FastAPI, Django/DRF, Spring, NestJS, ASP.NET, Ktor, frontend clients (React/Vue/Angular/Svelte), gRPC and Kafka/NATS. Where that is not enough, drop one of these into a service root (highest confidence first):
| File | Gives |
|---|---|
graffiti.contract.json |
explicit provides / consumes — any stack, highest confidence |
openapi.json / swagger.json |
HTTP routes as provides |
*.proto |
gRPC methods as provides |
Minimal graffiti.contract.json:
{
"provides": [{ "kind": "http", "name": "GET /orders/{id}" }],
"consumes": [{ "kind": "rpc", "name": "Payments.Charge" }]
}Gate CI on dead endpoints — audit exits non-zero when a consumer points at
an endpoint nothing provides:
graffiti system build && graffiti system audittree-sitter parsing (pure-Go, no CGO) → edge resolution → clustering into communities → lightweight analysis → deterministic serialization. No model, no embeddings, no network — just static analysis. That's why it's free, private, and reproducible.
- 0 API calls, $0, fully offline. Nothing about your code leaves your machine.
- Deterministic: same repo → byte-identical
map.jsonmodulo the singlegenerated_attimestamp and therootbasename. Commit it; diff it. - Single static binary, no runtime dependencies, no C toolchain.
Source-Available — read and run graffiti freely on your own repositories, but any reuse, redistribution, fork, or inclusion in another project requires prior written permission from the author. See LICENSE.
Yevgeniy Achin · amazopic@gmail.com · github.com/amazopic