TL;DR

Graft is an MIT-licensed CLI from NanoNets that builds a persistent, tree-sitter-derived map of your codebase and wires it into coding agents — so Claude Code, Cursor, Codex and friends stop re-discovering your repo from scratch on every single task. It has 4,843 stars and 423 forks since the repo went public on July 3, 2026, and ships as @nanonets/graft on npm (v0.13.0, ~7,500 weekly downloads).

The interesting design choice isn’t the graph — plenty of tools build those. It’s the delivery mechanism: hooks, not MCP tools. The team’s own framing on Hacker News was blunt: “Claude Code kept ignoring our MCP tools, so we used hooks instead.”

Key facts:

  • 4,843 stars / 423 forks, MIT, TypeScript, created July 3, 2026 — under two months old
  • Two-tier graph: deterministic tree-sitter (no model, no API key, $0) + an optional LLM enrichment pass (--deep) under your provider key
  • 22 languages — 5 at full fidelity (TS/JS, Python, Go, Java, R), 16 at “broad” symbol level, plus opt-in LSP-grade edges
  • Published benchmark: 162 controlled runs showing 42% fewer tokens, 46% fewer tool calls, 60% less wall-clock
  • SWE-bench Verified: 33/50 vs a cold Claude Code baseline’s 27/50 (+12 points) on the same model
  • The graph is gitignored — a regenerable local cache like node_modules, not a committed artifact
  • The benchmarks got a rough ride on HN — and the maintainer’s answers made it worse before they made it better

Quick reference

Repogithub.com/NanoNets/Graft
Package@nanonets/graft — v0.13.0
LicenseMIT
LanguageTypeScript (strict)
Installnpm install -g @nanonets/graft && graft init
RequiresNode.js; an LLM provider key only for --deep
Agents wiredClaude Code, Codex, Cursor, Gemini CLI, Copilot, Kiro, Windsurf, Grok, AdaL
Open issues76

The problem it targets

The pitch is one observation most people running coding agents daily have already felt:

Humans onboard to a codebase once. Agents onboard every single time.

Every task, your agent starts blind. It greps a term, opens a file, follows an import, backs out, tries again — rebuilding a mental model of a repo it mapped an hour ago and then threw away when the session ended. That rediscovery is where a large share of a run’s tool calls, tokens and latency actually go, and none of it is the work you asked for.

Graft’s answer: build the understanding once, write it into the repo as linked markdown files, and make the agent read those instead of wandering.


How the graph actually works

Graft is two graphs stacked, and the distinction matters for cost:

Tier 1 — structural, deterministic, free. graft build runs tree-sitter over your source and emits graft/.graph/wiring.json: every function, class, type and call edge, plus a per-file card mirroring your source tree. No model call, no API key, no network. This is the layer that powers ask, grep, callers, skeleton and map.

Tier 2 — semantic, LLM-written, opt-in. graft build --deep adds the part that makes it more than an index. Two passes: summarize each file, then group those summaries into a curated set of concept nodes with typed links. Graft deliberately does not emit one node per file — it picks a level of detail so a big repo collapses into a few dozen readable nodes.

Each node is a single markdown file holding four things:

PartWhat it holds
SummaryPlain-English explanation of what the code does, regenerated when the source changes
CruxThe handful of lines that carry the actual logic — the guard, the skip condition, the state change — lifted inline from source
SourcesThe exact files the node came from, tracked by content hash, so staleness is detectable
LinksTyped [[wikilinks]]depends_on, part_of, uses, implements, produces

The crux is stored as the code text, not a line range. That’s a genuinely smart call: line numbers drift the moment anything above them shifts, but the lines that matter don’t.

Everything is cached by content hash. On Graft’s own 124-file repo: 0.74s cold, 0.18s after one edited file, 0.18s with nothing changed. That cheapness is what makes the design work — every query stats the tree against the last build’s fingerprint (~3ms) and rebuilds only what moved, so answers describe the working tree right now, uncommitted edits included.


The hooks decision

This is the part worth stealing even if you never install Graft.

MCP servers are the standard way to hand an agent new retrieval tools. The problem is behavioural: Claude Code is trained to reach for Grep, and a custom MCP tool sitting next to it frequently just… doesn’t get called. The Graft team hit this hard enough to write a separate Show HN about it.

So graft init registers the MCP server and installs hooks. In Claude Code you get:

  • A live statusline — graph size, % enriched, ⚠ N stale when the code has moved ahead
  • Auto-sync on session start, user prompt, post-tool-use and stop — structural, $0, never calls the LLM on its own
  • Context on tap — matching nodes pulled into each prompt; editing a file surfaces its blast radius

The MCP surface is still there for agents that use it properly — six tools: graft_find_code, graft_file_api, graft_trace_calls, graft_find_all, graft_repo_map, graft_check_freshness.

When a commenter asked how this compares to Graphify, the maintainer’s answer was specific: MRR 0.73 vs 0.38, recall@10 54% vs 20% — because Graft ranks against code bodies while name-and-path indexes rank against identifiers. He added the part that actually explains adoption: “Claude never actually uses [CLI tools], as it’s trained to use grep. But for graft we set the directive.”


Real usage

npm install -g @nanonets/graft
graft init --dry-run      # see every file it would touch first
graft init --agents claude

init writes nothing until you choose, and on a non-TTY (CI, Dockerfile, piped shell) it writes nothing at all and just prints the command to run. Claude Code gets its own owned file at .claude/skills/graft/SKILL.md — it never edits your CLAUDE.md.

Day-to-day, the CLI is the interesting surface:

graft ask "where does auth happen"        # ranked nodes + exact file:line
graft skeleton src/server.ts              # every signature, no bodies (~1/10th the tokens)
graft callers validateRecord -d 3         # transitive blast radius
graft callers validateRecord --direction out
graft grep "NEEDLE" --in src/             # exhaustive regex, grouped by enclosing symbol
graft map                                 # token-budgeted repo orientation
graft blast --base origin/main --format markdown   # PR comment: what this diff can reach
graft check                               # exit 1 if the graph drifted
graft viz --export site/ --title "PR #12" # self-contained interactive graph page

graft map output is compact enough to paste straight into a prompt:

repo map — 113 files · 687 symbols · 2186 edges · typescript

src/       63 files · 527 symbols   hubs: contextDirFor (node-file.ts, 21←), buildGraph (build.ts, 11←)
test/      43 files · 102 symbols   hubs: edge (graph-traverse.test.ts, 4←)
viewer/     5 files ·  58 symbols   hubs: $ (main.ts, 9←)

graft blast is the sleeper feature. Pointed at a diff, it reports what depends on the lines a change touched — as a markdown PR comment, or JSON for CI. That’s a code-review primitive, not an agent gimmick.

Provider config is vendor-neutral and explicit: GRAFT_PROVIDER (openai for any OpenAI-compatible endpoint, or anthropic native), GRAFT_API_KEY, GRAFT_MODEL, GRAFT_BASE_URL — so OpenRouter, Fireworks, Groq, a LiteLLM proxy or a local server all work.


The benchmarks — and the fight about them

Graft publishes two sets of numbers. Both are worth reading, and one of them took a beating in public.

The controlled sweep. 162 runs, two repos, three variants of the same Claude Sonnet 5 agent with identical file tools — cold, Graft-push, and Graft-pull. Cost is cache-aware (reads ≈0.1×, writes 1.25×). An Opus judge scored correctness with a required-keyword floor so a fast-but-wrong answer couldn’t win.

Metric (mean/task)Cold Claude CodeWith Graft
Cost$0.0429$0.0292 (−32%)
Tokens8,0704,650 (−42%)
Tool calls4.22.3 (−46%)
Latency39.8s15.8s (−60%)
Correctness93%93% (equal)

Note the honest bit buried in the prose: the pull variant traded most of that speed for correctness — 98%, +5 points over cold. Push when you want speed, pull when you want to be right.

SWE-bench Verified. 50 instances, Claude Sonnet 5 on both arms, official swebench 4.1.0 grader, same Docker images and turn limits.

Cold Claude CodeWith Graft
Resolved27 / 50 (54%)33 / 50 (66%)
Tokens142.0M109.4M (−23%)
Cost$52.34$42.43 (−19%)
Wall-clock13,094s8,922s (−32%)

The failure shape is consistent and believable: the baseline patches one file and misses its siblings. On django-11532 it patched 1 of the 5 files the real fix needed and broke 18 previously-passing tests.

Now the criticism. On the Show HN thread (39 points, 44 comments, August 14), commenter seizethecheese noticed the SWE-bench sample had grown 9 → 20 → 36 → 50 across README updates — increments of 11, 16 and 14. He also spotted that one update scored 16/16 and another 2/14, and argued that unless the task selection was random and published, this looks like a set you can keep extending until it reads well.

The maintainer’s reply was disarming and not reassuring: “that’s just when our Claude limits were about to exhaust.” Which is plausible for a small team burning subscription quota — and also exactly why the answer should have been the seed, the 50 instance IDs, and per-instance results. seizethecheese’s response is the right standard: “HN commenters shouldn’t have to be Sherlock Holmes.”

The README also drew fire for being visibly LLM-written — “slop README,” said one commenter; another claimed to identify the model from the first six words. The maintainer conceded it: the README gets updated by coding agents as they iterate.

None of that means the mechanism doesn’t work. Two independent harnesses pointing the same direction, with a plausible causal story (agents miss sibling files; a call graph surfaces siblings), is real evidence. But treat the specific percentages as vendor-reported and not independently reproduced, and run graft blast/graft ask on your own repo before you believe the multiplier.


Honest limitations

  • It’s v0.13.0. Pre-1.0, two months old, 76 open issues, no tagged releases — the API surface is still moving.
  • Large monorepos can OOM. An open issue reports graft build dying with “Fatal process out of memory: Zone” at roughly 2,900+ files. If you’re on a big monorepo, budget time for --extensions scoping.
  • Grammar-level bugs exist. PHP’s tree-sitter grammar crashes on heredocs, and the generic extractor swallowed the failure silently. .vue files went unindexed until recently.
  • init writes outside your repo. Selecting the Codex host touches ~/.codex/config.toml and ~/.codex/hooks.jsonmachine-wide, every repo. The picker labels these, --dry-run lists them, and --no-global skips them. Read the dry run.
  • The crux isn’t in the markdown nodes yet. It ships per-symbol in the code graph under --deep; inlining into concept nodes is still on the roadmap.
  • Non-conforming gateways fail quietly. Enrichment reads results only from toolCalls, so a proxy that ignores forced tool_choice produces empty nodes rather than an error.
  • The staleness question is philosophical, not just technical. One commenter raised it well: fresh sessions catch mistakes precisely because they carry no assumptions. A persistent graph that pre-fills context could reinforce an early wrong conclusion instead of letting a clean session find it. Hooks keep the graph structurally fresh; they don’t make a bad LLM-written summary wrong-detectable.
  • Telemetry is on by default — anonymous buckets and fixed labels only, never code or paths. graft telemetry disable, DO_NOT_TRACK=1, or untick during init.

Should you use it?

Yes, if you run Claude Code or Codex daily on a repo big enough that the agent visibly flails before it finds anything, and you care about per-task cost. The free tier — graft build + ask/grep/callers/map with no key at all — is genuinely useful on its own, and graft blast earns its place in CI regardless of whether you ever wire an agent.

Not yet, if you’re on a 3,000-file monorepo (wait for the OOM fix), you’re primarily in PHP or a broad-tier language where edge resolution is name-based, or your team policy forbids tools that write to user-level agent config.

Related reading: CodeGraph takes the same problem via a pure MCP server with SQLite + FTS5, and Caveman attacks token cost from the opposite end — output verbosity instead of input retrieval.


FAQ

Do I need an API key to use Graft? No, not for the core. graft build, ask, grep, callers, skeleton, map, blast and check are deterministic tree-sitter and never call a model. A key is only needed for graft build --deep, which adds LLM-written concept nodes and per-symbol summaries — under your own provider and model.

Should I commit the graft/ directory? No. graft build adds it to .gitignore automatically — it’s a regenerable local cache. What you commit is the small wiring graft init drops in (.claude/, AGENTS.md, the MCP config); each teammate runs graft build to generate their own graph.

What happens when a teammate changes code without Graft installed? The graph re-syncs on your side. Every retrieval command stats the working tree against the last build’s fingerprint (~3ms) and rebuilds only what moved — so after you pull, the first graft ask refreshes before answering. The refresh is structural and costs nothing.

How is this different from just giving the agent an LSP? Different step in the workflow. LSP answers exact questions about a symbol you already know — go-to-definition, find-references. Graft answers the question before that: “where does auth happen,” returning ranked files and lines to start from. You can have both; graft build --lsp even folds compiler-grade call edges in when rust-analyzer, clangd, gopls, pyright or typescript-language-server is on your PATH.

Which languages get the good treatment? Five get full-fidelity extractors with scope-aware cross-file call and import resolution: TypeScript/JavaScript (including JSX/TSX), Python, Go, Java and R. Sixteen more — Rust, C, C++, C#, Ruby, PHP, Kotlin, Scala, Swift, Elixir, Solidity, OCaml, Zig, Dart, Clojure, Lua — get symbols plus name-resolved call edges. Unlisted languages are skipped, not indexed.

Are the benchmark numbers trustworthy? Partly. Two harnesses agree directionally and the causal story is sound, but everything is vendor-run and the SWE-bench sample was extended incrementally (9 → 20 → 36 → 50) without published instance IDs or seeds — which drew justified criticism on Hacker News. Treat the direction as credible and the exact percentages as unverified until someone reproduces them.


Sources