Note: this piece is about context pruning for code review -- cutting the token bill by shrinking what the agent reads. It is not the same as building a full knowledge-graph MCP for bug discovery, which I wrote about separately. Different goal, different pipeline. This one is the small tool.
The first time I checked the token bill for a month of Claude Code reviews, I thought the invoice had a typo. It did not. I was paying to re-read a 40,000-file monorepo on every pull request, the same way a junior engineer might re-read the whole rulebook before answering a Slack thread.
I had been calling this "context engineering." It was bulk loading with a nicer name.
I rebuilt the review path around one rule: only show the agent what is within two hops of the diff. Token bill dropped between 8x and 49x depending on the repo. Review quality went up, not down.
Here is what the split looks like in practice, based on the twelve repos I have measured or seen measured:
| Repo shape | Language | Diff size | Reduction |
|---|---|---|---|
| Solo API server | Python, 800 files | 3-file PR | 8x |
| Startup monorepo | TypeScript, 12k files | 5-file PR | 22x |
| Enterprise monorepo | Go, 40k files | 1-file typo fix | 49x |
| Data pipeline | Python + SQL, 2,900 files | 8-file refactor | 14x |
| Frontend app | TypeScript, 4k files | UI-only PR | 11x |
The 49x cases are the small-diff, huge-repo end. The 8x cases are small repos where there was less to prune in the first place. The bill scales with the difference between "whole repo" and "hop-2 blast radius." Big repos with small diffs win the most.
The bill problem nobody wants to admit
Most Claude Code review setups I see fall into one of two buckets.
Bucket one: give it the whole repo. The lazy default. On a typical 300k-LOC repo, roughly 80% of your tokens go to files the diff never touches. The agent reads utils/format_date.ts for the four hundredth time. It reads three years of test fixtures. It reads the auth middleware you deleted last quarter and somehow still has a .bak in tree.
Bucket two: let RAG decide. Better, but the embeddings have no idea what calls what. A vector store will happily hand the agent a UserService snippet because the query had the word "user" in it, while missing the actual caller two files away because the caller used u as a variable name.
Both buckets treat the codebase as a bag of text. A codebase is a graph.
What "blast radius" actually means
The term is borrowed from security, where it means the area of damage if an exploit lands. In code review it means: if I change function F, which other functions could behave differently as a result?
That is a question with a precise answer. On a call graph, it is a BFS:
def blast_radius(graph, start, max_hops=2):
visited = {start}
by_hop = {0: {start}}
frontier = {start}
for hop in range(1, max_hops + 1):
callers = set()
for node in frontier:
callers |= graph.callers_of(node) - visited
if not callers:
break
visited |= callers
by_hop[hop] = callers
frontier = callers
return by_hop
Walk backward from the diff, hop by hop, along call edges. Stop at hop 2. Hand the agent only the nodes in that set.
The "hop 2" cap is not arbitrary. It is where signal flips to noise on every codebase I have measured.
| Hop | Meaning | Review treatment |
|---|---|---|
| 0 | The change itself | Required reading |
| 1 | Direct callers | Required reading |
| 2 | Indirect callers | Skim for contract drift |
| 3 | Further callers | Reference only |
| 4+ | Far transitive | Default-hidden |
Stop at hop 2 and the agent reads ten to fifty nodes on a typical refactor. Let it run to hop 4 and you are back to hundreds of nodes, which is back to bucket one.
The Tree-sitter step
To compute the BFS you need the call graph. To get the call graph you need to parse the code. To parse code across twelve languages without writing twelve parsers, you use Tree-sitter.
As of 2026, tree-sitter-language-pack ships grammars for 306+ languages behind a single API. The realistic set for a call-graph builder is smaller: Python, TypeScript/JavaScript, Go, Rust, Java, C/C++, Ruby, PHP, and C#. Those cover the majority of what most teams review.
The node types that matter for a call graph are function_definition, call_expression, import_statement, and the class equivalents. Walk each file once, emit nodes for each function and class, emit edges for each call and import, store in SQLite. On a 2,900-file project this indexes in under two seconds. Re-indexing on every save is incremental: hash the file, skip if unchanged.
That is the whole prep cost.
The reference implementation, code-review-graph, runs the graph as an MCP server and reports 6.8x fewer tokens on average, up to 49x on monorepos (source). Those numbers line up with mine, with one caveat below.
The three traps that destroy the savings
Every team I have helped wire this up hits the same three traps. All three look reasonable. All three blow the blast radius back up to bucket-one size.
Trap one: utility functions. If format_date() is called from 400 sites and you treat it as a normal node, every change to it pulls in 400 files. The agent then "reviews" 400 files of irrelevant context and produces nothing useful. Fix: mark functions with caller counts above a threshold (twenty is the boundary I use) as utility nodes and default-exclude them from the BFS. Opt back in for the specific PR that does change format_date.
Trap two: base class methods. BaseRepository.save has fifteen subclasses. Without a cap on inheritance traversal, changing it pulls every *Repository plus their callers, which is half the repo. Fix: cap inheritance depth at one, or tag base classes the way you tag utility nodes.
Trap three: tests as callers. test_user_service.py calls UserService.create. Including test files as callers in the blast radius adds noise without adding information for an implementation review. Fix: keep a separate edge type for test-to-implementation calls and exclude it from the default BFS. Re-include only when reviewing the tests themselves.
Skip any one of these and the numbers degrade fast. With all three caps off, on the largest repo I tested, the "blast radius" of a one-line change was 1,200 files. With all three caps on, it was 23. Those were my numbers, in that order -- I had to run the bad case first before I trusted the caps enough to leave them on.
"Reached" vs "behavior-changed" -- the second axis
One more distinction matters for review quality, separate from the token argument.
Walking the call graph tells you which functions are reached by a change. It does not tell you which will behave differently. Those are two sets.
Change the internal logic of UserService.create without touching its signature or side effects. The call graph says "everything that calls create is reached." But the callers see the same signature, the same return shape, the same exceptions. Their behavior does not change. Review them with a lighter touch than callers of a function whose return type just got tightened.
The cleanest split I have seen: a reach blast radius that is mechanically true, and a behavior-change blast radius that is an LLM estimate based on what actually changed in the diff. Two scored sets, labeled differently. Anthropic's recent guidance on selective context loading lands in roughly the same place (Claude platform docs) -- the harness, not the model, is where the money is.
When blast radius is the wrong frame
Three cases where this makes the bill worse, not better:
- Cross-cutting concerns. Logging, auth, feature flags. Changes here actually do ripple. An artificially small blast radius will hide real risks. Tag these modules and bypass the BFS.
- Configuration-driven dispatch. If your codebase routes calls through a config file or a registry pattern, the static call graph misses the real edges. A second pass with dynamic-call completion is the fix.
- First review of a new repo. The agent has no model of the codebase yet. Skipping the wider read on the first pass means the agent has to learn the system one PR at a time. Let the first three PRs run with a wider hop cap, then tighten.
The recipe in five lines
To try this tomorrow on one repo:
- Pick a single language to start with. Python or TypeScript are the lowest-friction.
- Build the graph once with
tree-sitter-language-pack. Function and class nodes, call and import edges. Store as SQLite. - Wrap it as an MCP server (or a CLI the agent shells out to) that takes a list of changed files and returns the hop-2 set.
- Add the three caps: utility threshold, inheritance depth, test exclusion. Without these the numbers will lie to you.
- Diff your token bill for the next thirty days against the previous thirty.
The number you should see is between 6x and 50x lower, with review comment quality higher rather than lower, because the agent is finally reading the right files instead of all of them.
FAQ
How do you define blast radius exactly?
Set of nodes reachable within N hops on a call and import graph, backward from the diff's changed functions and classes. Default N=2. All three caps applied (utility threshold, inheritance depth, tests as a separate edge type).
What if the diff's dependency is more than two hops away?
You will miss it. On the twelve repos I have measured, missed-dependency review escapes at hop 2 sit under 3%, and the escapes cluster in the "cross-cutting concerns" category above -- which is why you tag those modules and bypass the BFS.
How does this compare against full-context review for finding bugs?
Roughly break-even on bug-finding for typical PRs, meaningfully worse on large-scale refactors (where you actually need to see more of the graph), meaningfully better on small PRs (because the agent stops getting distracted by irrelevant files). The 8-49x token saving applies across the board.
The Practical Knowledge Graph Guide covers the graph build, the MCP wrapper, the hop walker, and each of the three traps in the depth this post did not.
Top comments (0)