商业产业 Hacker News Claude 2026-08-14T15:22 正文 ok 4781 words

Show HN:Graft——可将 Claude Code grep Token 消耗降低 42% 的 Hooks 工具

英文标题:🔶 Show HN: Graft – Claude Code hooks that cut grep tokens by 42%

Graft 是一款面向 Claude Code 等编程 Agent 的本地代码图谱工具,可通过 Hooks 在每轮任务中注入相关代码节点,减少重复探索代码库的成本。作者在 162 次受控测试中称,使用 Graft 后 Token 消耗减少 42%、工具调用减少 46%、延迟减少 60%,正确率保持 93%。在 50 个 SWE-bench Verified 实例中,接入 Graft 的 Claude Sonnet 5 解决了 33 个问题,高于基线的 27 个,同时 Token、成本、工具调用和耗时均有所下降。Graft 的结构化代码图基于 tree-sitter 本地生成,不需要 API Key;可选的 LLM 深度构建则支持 OpenAI、Anthropic、OpenRouter 和本地模型等提供商。
打开原文相关 3 · 机会 0
media537.7 KB media1.9 KB media637 B media2.5 KB media1.1 KB media1.1 KB media2.7 KB media2.4 KB media595 B media599 B media626 B media5.1 MB media131.5 KB media1.3 MB media413.7 KB media395.6 KB media630.4 KB media16.8 MB media14.6 MB media66.4 KB

中文

使用和未使用 Graft 的编程 Agent 并排对比



npm install -g @nanonets/graft   # 仅需安装一次 CLI
graft init                       # 构建图谱并将其接入 Claude Code

这就是全部配置。graft init 会询问你要接入哪些编程 Agent,根据代码构建 graft/,并将状态栏和 Hooks 写入 .claude/。从下一次会话开始,Graft 会随 Claude Code 一同运行:它会将匹配的节点拉入每个提示词,并在每轮结束后于后台重建图谱。无需守护进程,无需记住重新建立索引,也没有任何默认需要运行或维护的组件——图谱就是一组文件。

在你确认前,不会写入任何内容。运行 graft init --dry-run 可先查看它将触及的每一个文件;也可以运行 graft init --agents claude 跳过交互提示,仅接入 Claude Code。

graft build 会自动将 graft/ 加入 .gitignore——图谱是本地、可再生成的缓存(类似 node_modules),而不是要提交的内容。你要共享的是 init 写入 .claude/ 的接入配置;每位团队成员运行 graft build,即可生成自己的图谱:

git add .claude && git commit -m "wire in graft"

不想全局安装?npx @nanonets/graft init 的效果相同。

执行两条命令——npm install 与 graft init——随后 Graft 随 Claude Code 会话运行,状态栏保持同步


每一项任务开始时,编程 Agent 都是“盲”的。在修改任何内容前,它会重新探索仓库:grep 一个术语、打开一个文件、沿着 import 跟踪、回退、再试一次。它正在重新构建一幅代码库的图景——而这幅图它可能一小时前就已建立过,随后又丢弃了。这种重复发现消耗了大部分工具调用、Token 和延迟,且纯属额外开销:

  • 重复。 每项任务都要从零开始重新付出探索成本。
  • 丢弃。 Agent 理解到的任何内容都会随会话结束而消失。
  • 不共享。 下一位团队成员及其 Agent 同样要从头开始。

人类只需完成一次代码库入门;Agent 每一次都要重新入门。

没有代码地图的 Agent 在找到所需内容之前,逐文件游走的探索轨迹


Graft 将这份理解一次性构建出来,并以一个由相互链接的 Markdown 文件组成的目录写入仓库:每个系统、API 或概念对应一个节点。

  • 真实解释,而非符号列表。 每个节点都用自然语言说明系统某个部分的作用,以及它如何与其他部分连接,就像资深工程师会给出的解释。这正是 Agent 跳过探索所需的信息,而不是一份函数名转储。
  • 可阅读的真实图谱。 没有 Embedding、相似度搜索或需要保持热状态的索引。图谱是一组相互链接的文件,Agent 可以像读取仓库中任何其他文件一样打开、grep 和跟随它们。
  • 嫁接到 Git 中。 图谱只是 graft/ 中的文件。提交它后,任何克隆仓库的人都能获得它。没有数据库、服务器或额外设置;Git 负责同步,过期图谱会作为评审中的 diff 出现,而不是在外部存储中悄然腐坏。
  • Diff 与代码共存。 当一次变更移动了代码位置,你会在同一个 Pull Request 中看到图谱 diff,就在导致变更的代码旁边。
  • 你的提供商、你的 Key、你的模型。 摘要可由你选择的任意提供商生成——OpenAI、Anthropic(原生)、OpenRouter、Fireworks、Groq、LiteLLM 代理或本地模型——并使用你自己的 Key。结构化代码图谱(graft build、graft check)则基于确定性的 tree-sitter,完全不会调用模型。

同一个“修复认证 Bug”任务以两种方式运行。冷启动的 Claude Code 会话重新读取仓库、逐文件游走;Claude Code + graft 一次加载代码地图,并通过 Hooks 以一次清晰的流程完成任务。使用 Graft 后:工具调用减少 46%,Token 减少 42%,耗时减少 60%,解决的 SWE-bench 实例增加 22%。


读取图谱的 Agent 应当更便宜、更快,同时不会增加错误答案。这是全部主张,因此我们进行了测量,而非仅作断言。

测试框架以相同的文件工具运行了同一个 Claude Sonnet 5 Agent 的三种变体:cold(从零开始探索)、Graft(预先注入 graft ask --source bundle)以及 pull(使用 graft_find_code/graft_file_api 工具,不注入任何内容——仅在请求时才为上下文付费)。Opus 4.8 裁判通过必需关键词下限评估正确性,因此“快但错误”的回答无法仅因速度快而胜出。成本计算考虑了缓存:读取约为 0.1×,写入为 1.25×,符合 Agent 实际运行时的计费模型。

共进行 162 次运行,覆盖两个仓库(Graft 自身以及一个真实的 Node/Express 认证服务),每项测试重复 3 次;任务涵盖单文件和多文件问题。

指标(每任务均值) 冷启动 Claude Code 使用 graft 的 Claude Code
成本节省(美元) 0.0429 0.0292(+32%)
Token 节省 8,070 4,650(+42%)
工具调用节省 4.2 2.3(+46%)
延迟节省(秒) 39.8 15.8(+60%)
正确率 93% 93%(相同)

在任何语料库中,Graft 的答案都没有比冷启动版本更差。pull 变体为了获得更重要的能力,牺牲了大部分速度优势:正确率升至 98%,比冷启动高 5 个百分点,是本轮测试中最强的单项结果。需要速度时使用 push;正确性比速度更重要时使用 pull。


上面的测试是我们用自己的测试框架衡量自己的机制。因此,我们也运行了行业标准测试——SWE-bench Verified:它包含真实仓库中的真实 GitHub Issue,并由官方 swebench 测试框架评分。没有裁判模型,没有相似度分数:你的补丁会被应用,维护者自己的测试会被运行;你要么在不破坏已通过测试的情况下修复失败测试,要么无法做到。

50 个实例,两组均使用相同模型——Claude Sonnet 5——相同的 Docker 镜像和相同的轮次限制。唯一的差别是是否接入 graft。

正确性与效率 冷启动 Claude Code 使用 graft 的 Claude Code 改进
正确性 27 / 50(54%) 33 / 50(66%) +12 个百分点
Token 节省 142.0M 109.4M +23%
成本节省 $52.34 $42.43 +19%
工具调用节省 1,370 1,031 +25%
API 请求节省 2,455 1,875 +24%
实际耗时节省 13,094s 8,922s +32%

graft 解决了 50 个实例中的 33 个,而冷启动 Claude Code 为 27 个;同时,graft 的工具调用减少 25%、Token 减少 23%、实际耗时减少 32%。每一个正确性胜出的案例都有相同模式:基线版本只修改一个文件,遗漏了它的关联文件。在 django-11532 中,修复需要修改 5 个文件,但基线只修改了其中 1 个,并两次破坏了 18 个此前通过的测试。在 django-16263 中,基线修改了 4 个文件中的 1 个,得分为 102 / 103。graft 找到了其余文件;并且在 django-16263 中仅用一半 Token 和一半时间完成任务。

两套测试框架对应两项主张:受控测试表明 graft 更便宜、更快;SWE-bench 表明它也更加正确。

正确率基于所有实例统计;Token、成本和调用次数则基于两组都成功解决的实例统计,以实现同类对比。使用官方 SWE-bench Verified 镜像和官方 swebench 4.1.0 评分器,原生 x86_64 环境。


Graft 分两轮构建图谱,两轮均由语言模型驱动:

  1. 读取每个文件。 每个源文件会被总结为一段简短说明,描述其功能。
  2. 将内容分组为节点。 这些摘要会被归入一组经过整理的节点(子系统、关键文件和概念),并在节点间建立带类型的链接。Graft 会为你选择合适的细节层级,而不是每个文件创建一个节点,因此大型仓库也能变成数十个可读节点。
flowchart LR
    S[Source files] --> T["Tier 1 — tree-sitter<br/>no model, no key"]
    S --> P1["Pass 1 — LLM summarizes<br/>each file (--deep)"]
    T --> W["graft/.graph/wiring.json<br/>per-symbol code graph"]
    P1 --> P2["Pass 2 — group into nodes<br/>+ typed links"]
    P2 --> N["graft/*.md<br/>markdown node graph"]

加载中

每一轮都会按内容哈希缓存——LLM 调用和 tree-sitter 解析均是如此。再次运行时只会处理发生变更的文件,因此第二次构建既快速又便宜(在本仓库中,124 个文件:冷启动 0.74 秒;编辑一个文件后为 0.18 秒;无任何变更时也为 0.18 秒)。graft build --no-reuse 可强制进行冷启动重新解析。

这种低成本使得每次查询都能在回答前刷新图谱。一次检索调用会将文件树与上一次构建的指纹进行比对(约 3ms),仅在存在变动时重建;因此 ask/grep/callers/skeleton/map 所描述的始终是当前代码状态,其中包括尚未保存到 Git 的修改:未提交、未暂存和已暂存的修改,对从不读取 Git 的 graft 来说没有区别。刷新是结构性的,成本为 $0;它不会调用 LLM。可通过每条命令的 --no-refresh 禁用,或通过 GRAFT_NO_REFRESH=1 全局禁用。

除 Markdown 图谱外,graft build 还会构建 graft/.graph/wiring.json——一个按符号组织的代码图谱——以及一张与源代码树对应的逐文件 wiring 卡片。Tier 1 完全基于 tree-sitter(每个函数、类和调用边;确定性、无模型、无网络),这也是普通 graft build 不需要 Key 的原因。--deep 模式会为每个符号添加一行摘要和一个关键逻辑摘录,并按函数体哈希进行缓存。


Graft 通过 tree-sitter 提供两个保真度层级的解析,并提供一个可选的编译器级别层——全部成本为 $0、结果确定性(无模型、无 Key):

  • 完整保真度(Full-fidelity) ——带有作用域感知、跨文件调用和 import 解析能力的手写提取器:TypeScript / JavaScript(包括 JSX 和 TSX)、Python、Go、Java。

  • 广泛支持(Broad) ——通过通用 tree-sitter 提取器和每种语言的一套语法,获取符号(函数、类、方法、类型等)以及名称解析后的调用边:Rust、C、C++、C#、Ruby、PHP、Kotlin、Scala、Swift、Elixir、Solidity、OCaml、Zig、Dart。

  • 编译器级边(可选) ——当语言服务器位于你的 PATH 中时,graft build --lsp 会添加精确的 lsp_resolved 调用边,以覆盖静态分析无法确定类型的成员调用:rust-analyzer(Rust)、clangd(C/C++)、gopls(Go)、pyright(Python)、typescript-language-server(TS/JS)。这是尽力而为的功能——未安装服务器时,图谱不会发生变化。

共支持 20 种语言。语言不在列表中的文件会被跳过,而非建立索引。新增一种 broad-tier 语言只需做一个较小的贡献——可参阅 CREDITS.md,查看为当前语言集合做出贡献的人员。


一个节点就是一个 Markdown 文件。大多数代码地图止步于“地址”:某个事物位于某个文件、某一行。这只能告诉 Agent 去哪里找,不能告诉它会发现什么,因此它仍需打开源码阅读。Graft 节点将语义直接放入节点中,Agent 可以预先学到所需信息,只有想了解更多时才打开文件。

每个节点包含:

部分 内容
摘要(Summary) 由模型生成并缓存的自然语言说明,解释代码功能。无论代码此前是否有文档,它都会存在;源代码变更后,它会重新生成。
关键逻辑(Crux) 实际承载逻辑的少数几行:守卫条件、跳过条件、状态变更。它们直接从源代码中提取并以内联形式存储,因此 Agent 不仅能看到其功能,还能看到其_实现方式_。
来源(Sources) 构建节点所依据的确切文件,每个文件均按内容哈希跟踪,因此 Graft 可精确判断节点何时过期。
链接(Links) 指向其他节点的带类型连接(depends_on、part_of、uses、implements、produces),以 Agent 可跟随的 [[wikilinks]] 形式写入。
备注(Notes) 你在生成内容块下方写入的任何内容。它会在重新生成时得到保留,因此你自己的上下文永远不会被覆盖。

这在一个文件中提供了三个深度层次:摘要说明代码_做什么_,关键逻辑展示其_如何实现_,来源则在 Agent 需要时指向其余内容。普通索引会迫使它为了解一件事而读取整个文件;Graft 节点会直接把答案内联提供,后续读取往往不再发生。

关键逻辑有意存储为代码文本本身,而非行号范围。每当上方无关代码发生移动时,行号都会漂移,但重要的代码行不会。保存文本而非编号,意味着即使文件周围内容移动,关键逻辑仍能保持正确。

摘要、来源、链接和备注目前已在 Markdown 节点中提供。关键逻辑目前在代码图谱中按符号提供(graft build --deep);下一步将把它内联到 Markdown 节点中。


  • **在你的机器上,无 Key、无网络:**结构化代码图谱。graft build(wiring 图谱与逐文件卡片)、graft check 和 graft ask 都是确定性的 tree-sitter 操作——它们绝不会调用模型。
  • **通过你的提供商 Key:**由 LLM 编写的部分——graft build --deep 会添加概念节点(文件摘要与节点综合),以及按符号生成的摘要和关键逻辑。graft 与供应商无关:设置 GRAFT_PROVIDER(任何 OpenAI 兼容端点使用 openai,原生 API 使用 anthropic)、GRAFT_API_KEY、GRAFT_MODEL,以及——对于 openai wire format——使用 GRAFT_BASE_URL 指向 OpenRouter、Fireworks、Groq、LiteLLM 代理、本地服务器或 OpenAI 本身。也可以通过命令行传入 --provider/--model/--api-key/--base-url。(OPENROUTER_API_KEY 仍可作为已弃用的回退选项使用。)
  • 没有遥测数据,也没有分析服务——唯一的网络调用是你所配置的 LLM 请求。

参阅 .env.example 获取完整设置列表(模型、基础 URL、图谱目录)。


一条命令即可将 Graft 接入你使用的编程 Agent:

npx @nanonet

English / 原文

Side-by-side comparison of a coding agent working with and without graft



npm install -g @nanonets/graft   # install the CLI, once
graft init                       # build the graph + wire it into Claude Code

That is the whole setup. graft init asks which of your coding agents to wire up, builds graft/ from your code, and drops a statusline and hooks into .claude/, so from the next session on Graft rides along in Claude Code: it pulls the matching nodes into each prompt and rebuilds the graph in the background after every turn. No daemon, no re-indexing to remember, nothing to run or maintain by default — the graph is just files.

Nothing is written until you pick. Run graft init --dry-run to see every file it would touch first, or graft init --agents claude to skip the prompt and wire Claude Code alone.

graft build adds graft/ to your .gitignore automatically — the graph is a local, regenerable cache (like node_modules), not something you commit. What you share is the wiring init dropped into .claude/; each teammate runs graft build to generate their own graph:

git add .claude && git commit -m "wire in graft"

Prefer not to install globally? npx @nanonets/graft init works the same way.

Two commands — npm install and graft init — then Graft rides along in a Claude Code session, statusline synced


Every task, your coding agent starts blind. Before it changes anything, it re-explores the repo: grep a term, open a file, follow an import, back out, try again. It is rebuilding a picture of a codebase it mapped an hour ago and threw away. That rediscovery burns most of a run's tool calls, tokens, and latency, and it is pure overhead:

  • Repeated. Every task pays the exploration cost again, from zero.
  • Discarded. Whatever the agent figured out dies with the session.
  • Unshared. The next teammate, and their agent, start from scratch too.

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

A no-map agent's exploration trail wandering file to file before it finds what it needs


Graft builds that understanding once and writes it into your repo as a folder of linked markdown files, one node per system, API, or concept.

  • Real explanations, not a list of symbols. Each node says, in plain English, what a part of the system does and how it connects to the rest, the way a senior engineer would explain it. That is the part an agent actually needs so it can skip the exploration. It is not a dump of function names.
  • A real graph you can read. No embeddings, no similarity search, no index to keep warm. The graph is a set of linked files your agent opens, greps, and follows, exactly the way it reads any other file in the repo.
  • Grafted into git. The graph is just files in graft/. Commit it, and anyone who clones the repo has it. No database, no server, no setup. Git does the syncing, and a stale graph shows up as a diff in review instead of rotting in some external store.
  • The diff lives with the code. When a change moves things around, you see it in the graph diff in the same pull request, right next to the code that caused it.
  • Your provider, your key, your model. Summaries are written by any provider you choose — OpenAI, Anthropic (native), OpenRouter, Fireworks, Groq, a LiteLLM proxy, or a local model — under your own key. The structural code graph (graft build, graft check) is deterministic tree-sitter and never calls a model at all.

The same task, 'fix the auth bug', run two ways. A cold Claude Code session re-reads the repo and wanders file to file; Claude Code + graft loads its map once and rides the hooks to one clean pass. With Graft: 46% fewer tool calls, 42% fewer tokens, 60% less time, +22% more SWE-bench instances resolved.


An agent that reads the graph should be cheaper and faster without getting more answers wrong. That's the whole claim, so we measured it instead of asserting it.

The harness ran three variants of the same Claude Sonnet 5 agent with the same file tools: cold (explores from zero), Graft (a graft ask --source bundle pushed up front), and pull (graft_find_code/graft_file_api tools, nothing injected — context paid for only when asked). An Opus 4.8 judge scored correctness with a required-keyword floor, so a fast-but-wrong answer couldn't win by being fast. Cost is cache-aware: reads ≈0.1×, writes 1.25×, the billing model agents actually run under.

162 runs, two repos (graft itself and a real Node/Express auth service), 3 trials each, tasks split between single-file and multi-file questions.

Metric (mean/task) Cold Claude Code Claude Code with graft
Cost savings ($) 0.0429 0.0292 (+32%)
Token savings 8,070 4,650 (+42%)
Tool-call savings 4.2 2.3 (+46%)
Latency savings (s) 39.8 15.8 (+60%)
Correctness 93% 93% (equal)

Graft never answered worse than cold, on any corpus. The pull variant gave up most of that speed for something bigger: correctness jumped to 98%, +5 points over cold, the strongest single result in the sweep. Push when speed is what you need; pull when being right matters more.


The sweep above is our harness measuring our mechanism. So we ran the industry-standard one too — SWE-bench Verified , real GitHub issues from real repos, graded by the official swebench harness. No judge model, no similarity score: your patch is applied, the maintainers' own tests are run, and you either flip the failing test without breaking the passing ones or you don't.

50 instances , same model on both arms — Claude Sonnet 5 — same Docker images, same turn limits. The only difference is whether graft is wired in.

Correctness & efficiency Cold Claude Code Claude Code with graft Improvement
Correctness 27 / 50 (54%) 33 / 50 (66%) +12 pts
Token savings 142.0M 109.4M +23%
Cost savings $52.34 $42.43 +19%
Tool-call savings 1,370 1,031 +25%
API-request savings 2,455 1,875 +24%
Wall-clock savings 13,094s 8,922s +32%

graft resolved 33 of 50 instances against Cold Claude Code's 27 — and got there with 25% fewer tool calls, 23% fewer tokens, and 32% less wall-clock time. Every correctness win has the same shape: the baseline patches one file and misses its siblings. On django-11532 it patched 1 of the 5 files the fix requires and broke 18 previously-passing tests, twice over. On django-16263 it patched 1 of 4 and scored 102 / 103. graft found the rest — and on django-16263 did it in half the tokens and half the time.

Two harnesses, two claims: the controlled sweep says graft is cheaper and faster, SWE-bench says it's also more correct.

Correctness over all instances; tokens, cost and calls over the instances both arms resolved, for a like-for-like comparison. Official SWE-bench Verified images and official swebench 4.1.0 grader, native x86_64.


Graft builds the graph in two passes, both powered by a language model:

  1. Read each file. Every source file is summarized once into a short description of what it does.
  2. Group into nodes. Those summaries are grouped into a curated set of nodes (subsystems, key files, and concepts) with typed links between them. Graft chooses the right level of detail for you instead of making one node per file, so a big repo becomes a few dozen readable nodes.
flowchart LR
    S[Source files] --> T["Tier 1 — tree-sitter<br/>no model, no key"]
    S --> P1["Pass 1 — LLM summarizes<br/>each file (--deep)"]
    T --> W["graft/.graph/wiring.json<br/>per-symbol code graph"]
    P1 --> P2["Pass 2 — group into nodes<br/>+ typed links"]
    P2 --> N["graft/*.md<br/>markdown node graph"]

Loading

Every pass is cached by content hash — the LLM ones and the tree-sitter parse alike. Re-running only touches the files that changed, so the second build is fast and cheap (on this repo, 124 files: 0.74s cold, 0.18s after one edited file, 0.18s with nothing changed). graft build --no-reuse forces a cold re-parse.

That cheapness is what lets every query refresh the graph before it answers. A retrieval call stats the tree against the last build's fingerprint (~3ms), and rebuilds only if something moved — so ask/grep/callers/skeleton/map describe the code as it is right now, including edits that are unsaved to git: uncommitted, unstaged, or staged all look the same to graft, which never reads git at all. The refresh is structural and $0; it never calls the LLM. Turn it off per-command with --no-refresh, or everywhere with GRAFT_NO_REFRESH=1.

Alongside the markdown graph, graft build builds graft/.graph/wiring.json — a per-symbol code graph — plus a per-file wiring card mirroring your source tree. Tier 1 is pure tree-sitter (every function, class, and call edge; deterministic, no model, no network), which is why plain graft build needs no key. The --deep pass adds a one-line summary and a crux excerpt per symbol, cached by body hash.


Graft parses with tree-sitter at two levels of fidelity, plus an optional compiler-grade layer — all $0 and deterministic (no model, no key):

  • Full-fidelity — hand-written extractors with scope-aware, cross-file call and import resolution: TypeScript / JavaScript (incl. JSX & TSX), Python , Go , Java.

  • Broad — symbols (functions, classes, methods, types, …) plus name-resolved call edges via a generic tree-sitter extractor, one grammar per language: Rust, C, C++, C#, Ruby, PHP, Kotlin, Scala, Swift, Elixir, Solidity, OCaml, Zig, Dart.

  • Compiler-grade edges (opt-in) — graft build --lsp adds precise lsp_resolved call edges (member calls the static pass can't type) when a language server is on your PATH: rust-analyzer (Rust), clangd (C/C++), gopls (Go), pyright (Python), typescript-language-server (TS/JS). It's best-effort — with no server installed the graph is unchanged.

Twenty languages in total. A file whose language isn't listed is skipped, not indexed. Adding a broad-tier language is a small contribution — see CREDITS.md for the folks who added the current set.


A node is a single markdown file. Most code maps stop at an address: this thing lives in that file, on that line. That tells an agent where to look, not what it will find, so it still has to open the source and read. A Graft node holds the meaning inline, so the agent learns what it needs up front and opens the file only when it wants more.

Each node holds:

Part What it holds
Summary A plain-English explanation of what the code does, written by the model and cached. It is there whether or not the code was ever documented, and it is regenerated when the source changes.
Crux The handful of lines that actually carry the logic: the guard, the skip condition, the state change. Lifted straight from the source and stored inline, so the agent sees how it works, not just what.
Sources The exact files the node is built from, each tracked by a content hash, so Graft can tell precisely when a node has gone stale.
Links Typed connections to other nodes (depends_on, part_of, uses, implements, produces), written as [[wikilinks]] your agent can follow.
Notes Anything you write below the generated block. It is preserved across regenerations, so your own context is never overwritten.

That is three depths in one file: the summary says what the code does, the crux shows how , and the sources point to the rest if the agent needs it. A plain index makes it read a whole file to learn one thing. A Graft node hands it the answer inline, and the follow-up read often never happens.

The crux is stored as the code itself, not as a line range, on purpose. Line numbers drift whenever unrelated code above them shifts, but the lines that matter do not. Keeping the text, not the numbers, means the crux stays correct even as the file around it moves.

Summary, sources, links, and notes ship today in markdown nodes. The crux ships per-symbol in the code graph (graft build --deep); inlining it into markdown nodes is next.


  • On your machine, no key, no network: the structural code graph. graft build (wiring graph + per-file cards), graft check, and graft ask are deterministic tree-sitter — they never call a model.
  • Through your provider key: the LLM-written parts — graft build --deep adds the concept nodes (file summaries + node synthesis) and the per-symbol summaries and cruxes. graft is vendor-neutral: set GRAFT_PROVIDER (openai for any OpenAI-compatible endpoint, or anthropic for the native API), your GRAFT_API_KEY, GRAFT_MODEL, and — for the openai wire format — GRAFT_BASE_URL to point at OpenRouter, Fireworks, Groq, a LiteLLM proxy, a local server, or OpenAI itself. Or pass --provider/--model/--api-key/--base-url on the command line. (OPENROUTER_API_KEY still works as a deprecated fallback.)
  • No telemetry and no analytics — the only network calls are the LLM requests you configured.

See .env.example for the full list of settings (model, base URL, graph directory).


One command wires Graft into the coding agents you use:

npx @nanonets/graft init
# detects your agents and writes each one's native instruction file;
# Claude Code additionally gets the live statusline + hooks below

On a terminal, init shows you every agent it knows about — flagging the ones it detected (via their config directories) and listing the exact files each would write — and wires only the ones you select. Claude Code is pre-selected; nothing else is. Selected agents get a marker-fenced Graft section in their shared instruction file — AGENTS.md (Codex, OpenCode and other CLIs that read it), GEMINI.md, .github/copilot-instructions.md — or a wholly-owned rule/skill file for the agents that use one: .claude/skills/graft/SKILL.md, .cursor/rules/graft.mdc, .kiro/steering/graft.md, .windsurf/rules/graft.md, .adal/skills/graft/SKILL.md for AdaL. Claude Code is in the second group: init writes its own skill file and never touches your CLAUDE.md. Re-running only updates Graft's own section (or replaces the owned file) and never touches the rest of your content.

With no TTY to prompt on — CI, a Dockerfile, a piped shell — init writes nothing and prints the command to run instead. Pass --agents <ids> or --yes to make a scripted run explicit.

Flag Effect
--agents <ids...> wire only these, no prompt — ids: agents, cursor, gemini, copilot, kiro, windsurf, adal, claude
--yes, -y skip the prompt and wire every detected agent
--dry-run print every file init would touch, then exit without writing
--all-agents write instruction files for every known agent, detected or not
--no-agents Claude Code wiring only; skip other agents
--list-agents print the known agent ids and exit
--no-mcp skip MCP server registration
--no-hooks skip hook installation
--no-global skip writes outside this repo (the ~/.codex/ entries below)

Selecting the agents host also touches your user-level Codex config, when ~/.codex/ exists:

Path What changes
~/.codex/config.toml registers the Graft MCP server ([mcp_servers.graft])
~/.codex/hooks/graft/graft-hooks.cjs the post-edit hook shim
~/.codex/hooks.json a PostToolUse entry matching `Write

Both configs are user-level, so they apply to every repo you open with Codex, not just this one. The picker labels these machine-wide, --dry-run lists them in their own section, and --no-global skips them while still wiring AGENTS.md.

graft init also registers Graft's MCP server with agents that support it, so these six tools appear natively, no shell required. Claude Code gets this too: graft init writes the server into the project's .mcp.json (restart Claude Code to load it). Skip with --no-mcp; run it manually with graft mcp [dir].

Tool Takes What it's for
graft_find_code a question Ranked nodes with file:line, source inlined — usually the full answer, no follow-up read needed.
graft_file_api a file path Every signature in that file, no bodies — the API surface for a tenth of the tokens.
graft_trace_calls a symbol Who depends on it, or what it depends on with direction: out, N levels deep for blast radius.
graft_find_all a regex Every hit, grouped by enclosing symbol, ranked by how coupled that symbol is.
graft_repo_map nothing A first look at an unfamiliar repo: directory clusters, hubs, hotspots.
graft_check_freshness nothing Whether the local graph has drifted from the code.

Register it by hand if your agent needs it explicit:

{ "mcpServers": { "graft": { "command": "npx", "args": ["-y", "@nanonets/graft", "mcp"] } } }

Where a CLI agent supports user-level hooks.json, init also installs Graft's post-edit hook — blast-radius warnings and automatic $0 graph re-sync after edits (skip with --no-hooks).

Claude Code (deep integration)

graft init always wires up Claude Code, and Claude Code gets more than the skill file above. From then on, any Claude Code session opened in the repo gets:

  • a live statusline — graph size, % enriched, and a ⚠ N stale warning when the code has moved ahead of the graph
  • auto-sync — every graft query brings the graph up to date first, so an answer always describes the code as it is right now, uncommitted edits included. A query refreshes only what it reads; the markdown under graft/ is refreshed by the background rebuild at the end of a turn that touched code. Both are structural and $0 — auto-sync never calls the LLM on its own
  • context on tap — each prompt pulls the matching nodes into the session; editing a file surfaces what depends on it ("blast radius"); new sessions start with the repo map

How Claude Code hooks wire graft in: install, graft init, then the hooks loop (session start, user prompt, post tool use, stop) keeps the graph built, read, and committed automatically
install → init → hooks keep the graph fresh every session

graft's post-edit hook: editing node-file.ts prints its blast radius (who depends on it) inline, the statusline flips stale → syncing → synced on its own, and the same dependents light up in graft viz
edit a file → blast radius appears inline → graph auto-resyncs → confirmed in graft viz

graft init is idempotent and never clobbers your existing .claude/settings.json — it merges its blocks and leaves the rest alone. Want the LLM summaries too? Run graft build --deep (with a key) whenever you like; auto-sync will never do it for you.


graft build [dir]                    # build graft/ from the code at [dir]: wiring graph + per-file cards (no LLM, no key)
graft build --deep                   # add the LLM layer: concept nodes + per-symbol summary/crux (cached)
graft build --extensions .ts .py     # only include these code extensions
graft build --no-reuse               # re-parse every file instead of replaying unchanged ones from cache

graft ask "<task>" [dir]             # query the graph — ranked nodes + exact file:line (no LLM, no key)
graft ask "<task>" --json            # machine-readable result
graft ask "<task>" --in <scope>      # narrow to one sub-project of a monorepo/multi-repo folder (see below)

graft skeleton <file> [dir]          # every signature in one file, no bodies — the API surface for ~1/10th the tokens (no LLM, no key)

graft callers <symbol> [dir]         # who calls/references/imports/implements/extends a symbol (no LLM, no key)
graft callers <symbol> --direction out  # the reverse: what the symbol itself calls/references (was `graft callees`)
graft callers <symbol> -d N          # walk transitively out to depth N — full blast radius (was `graft impact`)

graft grep "<regex>" [dir]           # exhaustive regex search over indexed files, grouped by enclosing symbol (no LLM, no key)
graft grep "<regex>" --in <path>     # narrow to files at or under this path prefix
graft grep "<regex>" -i --fixed      # case-insensitive; treat the pattern as a literal string, not a regex

graft map [dir]                      # token-budgeted repo orientation — dir clusters, hubs, hotspots (no LLM, no key)
graft map --max-dirs N               # raise/lower the number of directories shown

graft check [dir]                    # fail (exit 1) if graft/ has drifted from the code (never auto-refreshes — it's the drift report)
graft check --json                   # print the drift report as JSON

# ask / skeleton / callers / grep / map all refresh the graph first if the working tree moved:
#   --no-refresh                     # answer from the graph exactly as it is on disk
#   GRAFT_NO_REFRESH=1               # same, for every command
#   GRAFT_REFRESH=hash               # hash every file instead of trusting size+mtime

graft viz [dir]                      # see the graph: serves an interactive viewer on localhost
graft viz --port 5000 --no-open      # pick a port; don't auto-open the browser

graft init [dir]                     # pick which agents to wire (prompts on a terminal; writes nothing until you choose)
graft init --dry-run                 # list every file it would touch, then exit
graft init --agents cursor kiro      # wire only these agents, no prompt (ids: agents, cursor, gemini, copilot, kiro, windsurf, adal, claude)
graft init --yes                     # no prompt; wire every detected agent
graft init --no-global               # skip writes outside this repo (~/.codex/ config + hooks)
graft init --no-build                # wire the files only; don't build the graph
graft init --all-agents              # wire every known agent, detected or not
graft init --list-agents             # list known agent ids and exit

graft version                        # print the installed + latest published npm version
graft upgrade                        # npm install -g the latest published version
                                     # a new version is announced automatically (checked once a day);
                                     # after upgrading, the next session refreshes this repo's wiring itself

# global
graft --dir <path>                   # use a context dir other than <repo>/graft
graft --version, -v                  # print the installed version and exit

Method calls resolve through the receiver's type — constructor assignments (self.router = APIRouter()) and type annotations, not just the call-site name — so callers/grep --in return calls bound to the right type on method-heavy code, not every method anywhere with that name.

Search & orient (graft grep / graft map)

graft grep "<regex>" is exhaustive over every indexed file and groups hits by enclosing symbol, ranked by the same in-edge coupling graft map uses — built for "every occurrence of this pattern" tasks where graft ask's ranked top-N isn't enough:

"NEEDLE" — 2 hits in 2 symbols across 1 files (searched 1 indexed files)

heavilyCalled · function · src/a.ts:L1-L3 · 3 in-edges
  L2: console.log("NEEDLE hit in heavilyCalled");

rarelyCalled · function · src/a.ts:L4-L6 · 0 in-edges
  L5: console.log("NEEDLE hit in rarelyCalled");

graft map is a token-budgeted first look at a repo — directory clusters with file/symbol counts, each dir's local hubs, and the global hotspots — all ranked by in-degree, no LLM, no key:

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

src/                63 files · 527 symbols   hubs: contextDirFor (node-file.ts, 21←), wiringPath (write.ts, 14←), buildGraph (build.ts, 11←)
test/               43 files · 102 symbols   hubs: edge (graph-traverse.test.ts, 4←), graphOf (graph-traverse.test.ts, 4←), fileNode (graph-map.test.ts, 3←)
viewer/             5 files · 58 symbols   hubs: $ (main.ts, 9←), activeGraph (main.ts, 5←), cvar (data.ts, 5←)
scripts/            2 files · 0 symbols

hotspots: contextDirFor · function · src/context/node-file.ts:L100-L103 · 21←  wiringPath · function · src/graph/write.ts:L20-L22 · 14←  buildGraph · function · src/graph/build.ts:L104-L218 · 11←  ...

Monorepos & multi-repo folders

Graft handles two shapes without any config:

  • A monorepo with one.git (a pnpm-workspace.yaml/package.json workspaces, or per-package go.mod/pyproject.toml/Cargo.toml) — graft build discovers each sub-project as a ranking scope. ask/map rank every scope on its own terms and fuse the results, so the biggest sub-project can't drown a small one; hits carry [scope/] labels, and graft map groups its directory clusters by scope first.
  • A folder of separate git repos (no .git at the top) — graft build auto-splits: each child gets its own (git-ignored) graft/, and the parent gets a graft/workspace.json index. Queries from the parent federate across every child, always labeled <child>/. Run graft build inside a child to work on just that repo.

Either way, narrow to one sub-project with graft ask "<task>" --in <scope>/ once you know where you're working.

graft init at the parent of a multi-repo folder wires every child repo too , not just the parent — an agent session opens at a repo root and reads its instruction files from there, so each child needs its own. A session started in the parent gets the federated view; one started in a child sees that repo alone.

Commands also find the graph from a subdirectory: with no [dir] argument they walk up to the nearest graft/, so graft ask works from src/deep/inside/ without a cd to the repo root.

graft viz opens a local, interactive view of both graphs — no install, no dev server; the viewer ships prebuilt inside the package.

graft viz — searching a symbol and jumping to it lights up its dependency graph: amber edges are what it depends on, teal is what depends on it
search → jump to a node → dependency graph lights up

  • Context tab — the architecture graph from graft/*.md. Nodes colored by type, sized by connectedness.
  • Code tab — the per-symbol graph from graft/.graph/wiring.json (run graft build first).
  • Outline tab — the file → class → method hierarchy as a collapsible tree.

Edges speak the code's language. Every link is one of a closed set of verbs, each answering a question someone building or reviewing code actually asks:

Verb The question it answers
part_of / contains where does this live?
uses / calls / imports / depends_on what breaks if I change this?
produces where does this output come from?
configures what changes its behavior without a code change?
validates what checks or judges this? (tests, drift checks, scoring)
extends / implements what contract must this honor?

Select a node and its edges take on direction: amber = what it depends on, teal = what depends on it , with the verb written on each highlighted edge. Chips above the canvas filter by verb; tree-sitter-extracted edges draw solid while LLM-inferred ones draw dashed. The viewer live-reloads when graft/ changes on disk. Older graphs with vague verbs (influences, supports) are normalized on load — no regeneration needed.


Tested on your popular repos

The benchmarks measure the mechanism. The real test is whether graft helps an agent ship real changes on code people actually run, not just answer questions. So we benchmark it on popular open-source repos: 15 tasks each , 10 real developer questions plus 5 actual implementation tasks (real merged pull requests, each re-implemented from its base commit and scored against the files the maintainers actually changed). Same agent (Claude Opus), same file tools; the only difference is whether graft is wired in.

Across these repos graft runs up to 4× cheaper and 3× faster , with better or no loss of correctness: it reproduces the real merged PRs by touching the same files the maintainers did. Per-repo detail below.

PocketBase (Go, ~350 files)

Aggregate over 15 tasks Standard Claude Code With graft
Cost $13.91 $11.02 (−21%)
Wall-clock 2,044s 1,762s (−14%)
PRs reproduced 5 / 5 5 / 5 (same files as the maintainers)

Cheaper and faster with no loss of correctness: graft reproduced all five merged PRs, touching the same files the maintainers did. The gap is widest on cross-file understanding — "how does auth work across OAuth2 providers" dropped from $2.19 to $0.84.

The 10 questions we asked

  1. Orientation — Give me a map of PocketBase's architecture: the main subsystems and how an HTTP request flows through to the database.
  2. Entry-point trace — Trace end-to-end what happens when a client creates a record via the REST API, from route handler to database write.
  3. Feature location — I want to add a brand-new collection field type. Where do I hook it in, and which pieces must change?
  4. Bug localization — Realtime subscriptions silently stop delivering events after a while. Where would you start looking, and why?
  5. Blast radius — If I change the signature of the record-validation logic, what depends on it and what could break?
  6. Cross-file synthesis — How does auth work across OAuth2 providers: where are tokens issued, validated, stored, and refreshed?
  7. Extensibility — How do I use PocketBase as a Go framework to register a custom route plus an on-record-create hook?
  8. Security discovery — Where is user input validated, and where are collection API access rules enforced before a query runs?
  9. Public API — As an external app, how do I authenticate and then list and filter records over the REST API?
  10. Test verification — Where are the tests for the record CRUD API, and what do they assert about access rules?

The 5 merged PRs we re-implemented

Each PR was reset to its base commit; graft's diff was scored against the files the merged PR changed.

PR Type What it does Files the maintainers touched
#6744 feat Generate & serve WebP thumbnails apis/file.go, tools/filesystem/filesystem.go
#6947 fix Uniform char distribution in regex random strings tools/security/random_by_regex.go
#6690 refactor Patreon OAuth2 to use x/oauth2/endpoints tools/auth/patreon.go
#2726 perf Drop a redundant admin-count query on a hot middleware path apis/middlewares.go
#3192 fix Restore prior API rules on automigration rollback plugins/migratecmd/templates.go
Method

Two clones of PocketBase at the same commit: one wired with graft init, one untouched and verified graft-free. Each task run headless (claude -p, Claude Opus) with an empty MCP config. Understanding questions were graded by whether the answer pointed to the right files and functions; PR tasks were scored on whether the agent's diff touched the same files as the merged PR. Every transcript was audited to confirm graft was actually used in the graft arm and absent from the standard arm.


git clone https://github.com/NanoNets/context-graph-engine.git && cd context-graph-engine
npm install
npm run build
npm test

npm run cli -- build --deep .      # run the CLI from source

MIT. See LICENSE.