Graft: a repo graph made of markdown files
Turbocharge Claude Code, Cursor, Codex, Gemini & every coding agent: faster, cheaper, with contextual understanding specific to your codebase.
At a glance
- What is it?
- Graft writes a codebase's structure into a folder of linked markdown files and wires it into your coding agent. The structural half is free tree-sitter work, the explanatory half runs on your own key, and the wiring it commits into your repo is the part you actually review.
- Who is it for?
- Graft suits teams already paying an agent to re-read the same repository every session, and teams running several agents against one codebase where the wiring under `.claude/` is worth committing. It does not suit anyone who needs semantic search over code, a repo whose main language is not among the README's supported languages, or a team unwilling to have an init step write files into the repository.
- 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 TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- trailhq/Graft11,271 ★ · +4,323
| Repository | 2026-09-10 | 2026-11-14 |
|---|---|---|
| trailhq/Graft | 6.9k | 11.3k |
4,323 Stars in the last 65 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The graph is linked markdown in graft/, kept out of git
Graft's storage format is a folder of linked markdown files, one node per system, API or concept, written to `graft/` at the repository root. No embeddings, no similarity search, no index to keep warm, and `gray-matter` in the dependency list is what lets a node carry frontmatter. An agent reads the result the way it reads any other file: open a node, follow links to its neighbours, grep for a name.
`graft build` writes that folder and adds it to `.gitignore` on the fly. What travels between machines is the wiring, not the graph:
git add .claude && git commit -m "wire in graft"Every teammate regenerates their own copy after cloning, and two checkouts can hold different node text for the same system without either being wrong. The price is that no reviewed artifact describes your architecture. Reviewers see a small set of files under `.claude/`. The prose describing your systems gets reviewed by whoever happened to run the build.
Structure is tree-sitter, explanation is your key
Two passes sit behind the same command and they cost different things. The structural pass uses tree-sitter and never calls a model: `build`, `check`, `ask`, `grep`, `map` and traversal all run on it, which is why a query can rebuild against the working tree for $0 in roughly 3ms when nothing has moved. The deep pass writes the English explanations, one per concept node and per symbol, and it needs a provider key:
GRAFT_PROVIDER=anthropic
GRAFT_API_KEY=sk-ant-...
GRAFT_MODEL=claude-sonnet-5Without a key you lose `build --deep` and keep structure and links. That gap is larger than it first sounds, because the whole argument for the tool is that an agent can skip exploration once a node explains in English what a part of the system does. A structural-only graph hands over the map without the tour, and the deep pass, being the half that calls a provider under your own key, is the half your invoice pays for.
The stated 3ms figure covers the case where nothing moved. Uncommitted edits are reflected on every query, and `graft check` is described as a local freshness signal rather than a rebuild you schedule.
graft init edits your repo and waits for the next session
npm install -g @nanonets/graft # install the CLI, once
graft init # build the graph + wire it into Claude Code`graft init` asks which coding agents to wire up, builds `graft/` from your code, and drops a statusline and hooks into `.claude/`. The README now presents the project as Trail-code, installed with `npm install -g @trailhq/trail-code && trail init`, and says every `graft` command still works. From the next session on, matching nodes are pulled into each prompt and the graph is rebuilt in the background after every turn.
Two consequences. The tool writes into files that belong to your editor configuration, so `graft init --dry-run` prints every file it would touch before you agree, and `graft init --agents claude` skips the prompt when you want Claude Code alone. And none of it applies to the session you ran it in, because the statusline, the hooks and the prompt injection all start at the next session boundary. A global install is optional too, since `npx @nanonets/graft init` does the same work:
"engines": {
"node": ">=20"
}Read TELEMETRY.md before putting this in a company repository. The package runs `node scripts/postinstall.mjs` on install and `node scripts/stamp-telemetry-key.mjs` inside `prepare`, both of which execute on your machine at install time.
GRAFT_PROVIDER picks a wire format, and an unset key falls back to OpenRouter
The provider switch is about protocol rather than vendor. `GRAFT_PROVIDER` accepts `openai` for any OpenAI-compatible endpoint, `anthropic`, `litellm` for a proxy, or `orcarouter`, and the vendor is chosen separately through `GRAFT_BASE_URL`:
GRAFT_BASE_URL=https://openrouter.ai/api/v1The commented examples in `.env.example` name OpenRouter, Fireworks, Groq, a LiteLLM proxy on port 4000 and Ollama on port 11434. That is the point of splitting wire format from company: moving between a hosted model and a local one is a base URL and a model id.
The trap is the fallback. If `GRAFT_API_KEY` is unset, the deprecated OpenRouter variables are used instead:
OPENROUTER_API_KEY=sk-or-...
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
GRAFT_OPENROUTER_MODEL=openai/gpt-4o-miniA CI job that forgets the new variable does not fail loudly. It finds the old credentials in the environment and runs the deep pass on the deprecated path with whatever model the legacy names point at. Set both or neither, and check which one your pipeline exports before blaming the model for the output.
Every cold install compiles nine tree-sitter grammars
The container build carries a scar from a real failure. `npm ci` runs this package's `prepare` script, which is the build, so copying only `package.json` and `package-lock.json` into the image fails with `The specified path does not exist: 'tsconfig.json'`. The fix, stated in the Dockerfile comments, is to drop the package's own prepare step for the install layer while keeping every dependency's install scripts, because those are what compile the grammars.
Those grammars build from source. Since the repo pinned node-gyp 12, `npm ci` compiles tree-sitter's parsers and dies on `find Python ... could not be run` unless a toolchain is installed:
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*The count is nine, and the comments quote minutes of rebuild on a one-line source edit. The stage order exists to prevent that cost, since unchanged dependencies mean a cached install and a deploy that is only a `tsc`. The runtime image cannot reinstall at all: `npm ci --omit=dev` would re-run `prepare` without `tsc` present, and `--ignore-scripts` would skip the native builds tree-sitter needs, so compiled `node_modules` is carried over from the build stage and pruned in place. Expect a cold build of this image to take minutes, and check that your image copies manifests before sources.
The benchmark runs three variants of one agent, 162 times
The project publishes its own measurement. The harness runs three variants of the same Claude Sonnet 5 agent with the same file tools: cold, which explores from zero. Graft, where a `graft ask --source` bundle is pushed up front. And pull, where `graft_find_code` and `graft_file_api` tools are exposed and nothing is injected, so context is only paid for when asked. Correctness is scored by an Opus 4.8 judge with a required-keyword floor so a fast wrong answer cannot win, and cost is treated as cache-aware, with reads at about 0.1 times and writes at 1.25 times.
The reported table covers 162 runs: tool calls down 46%, uncached input tokens down 42%, time down 60%, with the same share of correct answers. Separately, on SWE-bench Verified (50 real issues) the agent solved 33 (66%) with the graft map against 27 (54%) without it.
Both serving modes exist because the trade-off is real. A bundle pushes context into every prompt whether or not it is needed. Pull mode spends tool calls to fetch it on demand, and the saving depends on the graph holding the answer before the agent asks.
No similarity search means recall is bounded by your words
The alternative design here is an embedding index with similarity search over the same code, and the project rejects it in one line: no embeddings, no similarity search, no index to keep warm. The difference in approach is real. A vector store needs a model to embed every symbol, somewhere to hold the vectors, and a ranking that returns chunks nobody wrote. Linked markdown over tree-sitter structure gives an agent file paths, import edges and prose a person wrote, and the structural half costs nothing to produce.
What you give up is fuzzy recall. `graft grep` is a text search across files, so a question phrased entirely in concepts that never appear as identifiers in your code has nothing to match. Nothing ranks results for you either. The agent opens a node and follows links, which is exactly the tool-call cost the benchmark claims to reduce, and that reduction depends on the graph holding the answer before anyone asks for it.
The second boundary is language coverage. The README has a Supported languages section, and a structural pass is only as good as the grammars behind it, so check that your main language is covered before pointing this at a polyglot monorepo.
Version 0.21.1 with no GitHub releases behind it
The package declares the name `@nanonets/graft`, version 0.28.0, MIT, and a `graft` binary pointing at `dist/bin/graft.js`. The published tarball carries `dist`, `scripts`, `README.md` and `LICENSE`, and not `docs/`, `src/` or `test/`.
There are no GitHub releases to diff, and the last push was on 2026-11-02. `CHANGELOG.md` at the repository root is the only version history in the tree, so an upgrade is an npm version bump plus a read of that file rather than a release page. A 0.x number also keeps breaking changes in scope under npm's own rules, so pin the exact version if your setup depends on hook or statusline behaviour.
Self-hosting is a separate path from the CLI. The `Dockerfile` describes the graft GitHub App rather than the local tool, and it needs git on the PATH because it fetches pull request refs. Whatever it pulls with those refs needs the same git access in the VM, Fly, Cloud Run or App Runner target you point the image at.
Editorial conclusion
Graft suits teams already paying an agent to re-read the same repository every session, and teams running several agents against one codebase where the wiring under `.claude/` is worth committing. It does not suit anyone who needs semantic search over code, a repo whose main language is not among the README's supported languages, or a team unwilling to have an init step write files into the repository. Verify three things first: that `graft build --deep` works on a language you actually ship, that your CI holds `GRAFT_API_KEY` instead of silently falling back to the deprecated `OPENROUTER_API_KEY` path, and that a cold image survives nine tree-sitter grammars compiling from source.
Frequently asked questions
Does Graft need an API key to build its graph?
Only the deep pass does. The structural build, plus `check`, `ask`, `grep`, `map` and traversal, is deterministic tree-sitter and needs no key, while `build --deep` calls a provider for concept nodes and per-symbol summaries under your own key.
Where does the Graft graph live, and is it committed to git?
`graft build` writes `graft/` and adds it to `.gitignore` automatically. What gets committed is the wiring `graft init` drops into `.claude/`, and each teammate runs `graft build` to generate their own graph locally.
Which languages does Graft parse structurally?
The structural pass is built on tree-sitter, and the README has a Supported languages section to check against your main language.
What does graft init change inside my repository?
It asks which coding agents to wire up, builds `graft/`, and drops a statusline and hooks into `.claude/`. `graft init --dry-run` prints every file it would touch first, and the wiring takes effect from the next agent session.
How is Graft updated, and what runtime does it need?
It ships on npm as `@nanonets/graft`, currently at version 0.28.0, and declares Node ^20.16.0 || >=22.3.0 in its engines field. The repository has no GitHub releases, and `CHANGELOG.md` at the root is the version history.
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/trailhq-graft)Star history chart for your README
[](https://hysenlabs.com/projects/trailhq-graft)Was this useful?