calldiff: diffing call stacks across git commits
Diffs for function call stacks across git commits. 22 languages supported (AST-based, built using Tree-sitter). Great for agentic review.
At a glance
- What is it?
- calldiff renders call trees for exported functions and diffs them between two git refs, using tree-sitter grammars for 23 languages. It is aimed at agentic code review, and its limits are as interesting as its output.
- Who is it for?
- Adopt calldiff if you review changes where the shape of the call flow matters more than the line count, especially when an agent has rewired an entrypoint and you want to see which callees appeared, disappeared, or moved. Skip it if your code resolves calls at runtime through reflection, dependency injection or dynamic dispatch, because the README states plainly that this is syntactic and dynamic calls will not resolve.
- 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 22 days 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 September 16, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
What calldiff shows that a line diff hides
A normal git diff tells you which lines changed. It does not tell you that createCodingTools() disappeared from under PiService.createAgentSession(options) and was replaced by a new branch through PiService.getServices(). On a large refactor, or on a change an agent made on your behalf, the line diff is mostly noise and the structural change is buried in it. calldiff is built for exactly that gap. The README describes it as "like git diff, but for who-calls-whom", and the intended audience is agentic code review: the case where a coding agent has rewired call flow and you want the shape of the change rather than the text of it. The README even ships a prompt to paste at an agent, telling it to walk you through its changes using npx calldiff@latest. That framing is the project's whole identity. It is a reviewer's tool, not a build-time checker, and it produces no pass or fail verdict.
How the AST pipeline turns two git trees into a call tree diff
The README lists four steps. calldiff reads source from both sides of the comparison, using git show for committed refs and the filesystem for the working tree. It detects the language from the file extension, loads a tree-sitter grammar (bundled, or fetched on demand into ~/.cache/calldiff/grammars), and parses the file. From that parse it builds per-function callee lists and expands them into call trees. Then it diffs the trees, prints one, or searches paths between two symbols, with structured output available for agents and scripts.
Two design choices follow from this and both matter. First, grammars install on first use, and the cache location can be overridden with CALLDIFF_GRAMMAR_CACHE. That means the first run against a language you have not used before needs network access, and offline or air-gapped runs need the cache pre-warmed. Second, and more consequential, the README states the analysis is syntactic and AST-based, not a full typechecker, and that dynamic calls will not resolve. Anything dispatched at runtime, through a registry, a decorator, a string lookup or an interface with several implementations, simply will not appear as an edge. The tree you get is the tree the parser can see.
The labels in the output are worth knowing before you read a diff. A free function appears as functionName, a method as ClassName.method, a constructor as new ClassName, and a JSX or TSX tag as a Component with its children nested underneath. Conditional arms show up as if (cond), else and else if (cond), with no continuing vertical rail, which is how the tool signals that a branch is an alternative rather than a sequence. With --locs enabled, each node carries a source location: the root uses the definition file:line, and children use the call site inside the parent, either file:line or file:line-line. The README compares this to the fromRanges of an LSP Call Hierarchy rather than Go to Definition, and that distinction is the one to internalise before you start reading locations as if they pointed at the function being called.
Installing calldiff and reading your first call stack diff
The README gives two install paths. The quickest is to run it without installing, which pins nothing and always fetches the latest published version. The alternative is a global install. Node 22.18.0 or newer is required, per the engines field in package.json.
npx calldiff@latest
# or
npm install -g calldiffWith calldiff on your path, the default invocation compares HEAD against the working tree, which is what you want when you have unstaged changes and want to see how they reshaped the call flow.
calldiff diffTo compare two refs, pass them as positionals or use the named flags. The README documents both forms, and they are equivalent.
calldiff diff abc123 def456
calldiff diff --from main --to featureIf you do not pass --entry or --file, calldiff infers exported functions whose expanded call trees changed and may show several of them. That inference is convenient but unpredictable in a monorepo, so for a focused review name the entrypoint yourself. A bare function name works, as does a ClassName.method label.
calldiff diff main feature --entry createAgentSession
calldiff diff main feature -e PiService.createAgentSession -e bootIn the output, lines prefixed with a minus were present in the from side and are gone in the to side, and lines prefixed with a plus are new. The README's example shows a removed block of three callees replaced by a single new callee that itself expands into three children, which is the kind of restructuring a line diff renders almost unreadable. For a tree with no diff markers, use the tree subcommand, which requires --entry or --file and accepts an optional ref.
calldiff tree -e createAgentSession
calldiff tree HEAD -e PiService.createAgentSessionWhen you need to know whether one symbol can still reach another after the change, reach prints every call path from the entrypoint to the target, including alternate if and else arms. Both --entry and --to are required.
Machine-readable output, MCP registration and agent skills
The human-readable tree is the default, colored on a TTY and colorless when piped, but calldiff also targets scripts and agents directly. The --format flag accepts json, yaml, md and jsonl, and produces a structured result containing from, to and either trees or paths, with nested nodes and a per-entry ascii rendering. That ascii field is the detail that makes the JSON usable by a model: you get both the structure to traverse and the rendered tree to read.
calldiff diff --format json
calldiff --llms
calldiff skills add # install agent skill files
calldiff mcp add # register as MCP serverThis layer comes from incur, the CLI framework calldiff is built on, which the README credits for skills add, mcp add, --llms, the CTAs shown after diffs, and typed flags. The practical effect is that calldiff can be registered as an MCP server so an agent calls it as a tool rather than shelling out and parsing text. The --llms flag emits a description of the CLI for a model to read. There is also a reach subcommand that answers a different question from diff: given an entrypoint and a target, it prints every call path between them, which is what you want when you are checking whether a refactor broke a route rather than reviewing what changed.
Where calldiff breaks down
The syntactic approach is the source of every real limitation here. Dynamic dispatch does not resolve, so a call made through a variable, a dictionary of handlers, a decorator, a dependency-injection container or a plugin registry is invisible. In a codebase that leans on those patterns, the tree will look thinner than the program actually is, and a diff can show a callee as removed when it is merely no longer called statically. That is a false negative you have to be watching for, not a bug you can configure away.
The language list is broad, 23 languages in the README and package description, but breadth and depth are different things. The bundled dependency tree in package.json includes tree-sitter-typescript; the README says other grammars are fetched on demand into the cache directory on first use. That fetch is a network dependency at run time, and a grammar that is fetched is not the same as one that has been tuned for the constructs of its language.
Entrypoint inference is the other soft spot. Omitting --entry or --file makes calldiff guess which exported functions changed, and the README says it may show several. On a small repository that is fine. On a monorepo with many exports, you get a wall of trees and no way to know which one the reviewer cares about. Passing --file narrows it to every exported symbol in one indexed source file, which is the practical escape hatch, but the matching rules deserve attention: an exact path works, or a unique suffix like boot.ts resolving to packages/api/src/boot.ts, and ambiguous matches error out rather than guessing. That error is the right behaviour, though it means you will occasionally have to type a longer path than you expected. Finally, calldiff reports on call structure only. It says nothing about whether the change is correct, whether tests pass, or whether the new call path is reachable in production.
calldiff against a language server call hierarchy
The closest conventional alternative is your editor's Call Hierarchy feature, which in VS Code and similar editors is backed by a language server and shows incoming and outgoing calls for a symbol. The difference in approach is fundamental. A language server typechecks: it resolves symbols through the type system, follows interface implementations, and understands imports. calldiff parses with tree-sitter and resolves what the syntax shows, which is why it works across 23 languages without a per-language server and why it cannot follow a dynamic call. The second difference is the unit of comparison. Call Hierarchy shows you one symbol's neighbourhood in one state of the code. calldiff compares two states and marks what appeared and what disappeared, which is the operation you actually want during review. If you need to know which concrete implementation of an interface is invoked at a call site, a language server is the right tool and calldiff is not. If you need to see, across a whole entrypoint, what an agent's commit did to the call flow, and you want that in JSON for a script, the language server has no equivalent.
Licence, maintenance and the cost of upgrading
calldiff is MIT licensed and published to npm as version 0.6.0, with the bin entry pointing at dist/cli.js. MIT is permissive: you can use it commercially, modify it and redistribute it, provided the copyright notice and licence text travel with it. That is a statement about the licence file, not legal advice, and if you are vendoring it into a product you should read the LICENSE at the repository root yourself.
On maintenance, the last push to the default branch was on 2026-08-27, which is recent, and the repository is not archived. There are no retrieved releases, so the version you get from npm is the version you get; there is no changelog to read before upgrading. That matters more than usual here because the on-demand grammar cache is a runtime dependency. If a grammar download changes shape or a tree-sitter version bump alters parse output, your diffs can change without any change to your code. The package.json pins tree-sitter at ^0.25.1 and tree-sitter-typescript at ^0.23.2, with an override forcing the TypeScript grammar onto the same tree-sitter version, which suggests that version alignment has been a real concern. Upgrading is cheap in the sense that there is nothing to migrate, and risky in the sense that the diff output is the interface and it is not versioned against your expectations. Pinning an exact version in CI is the safe move.
Working in a monorepo with --file and suffix matching
Monorepos are where the entrypoint problem bites hardest, and the README addresses it with --file (or -F), which takes an indexed source path and expands to every exported symbol defined in that file. That is a coarser instrument than --entry, and deliberately so: it lets you say "show me everything that changed in this route module" without naming each export. Matching accepts an exact path or a unique suffix, so boot.ts can resolve to packages/api/src/boot.ts. When two files could match, calldiff errors rather than picking one, and the fix is to pass a more specific path. The distinction between the two flags is worth keeping straight: --file expands to exported symbols defined in a file, while --entry names symbols directly. A file as entrypoint is the useful case in a monorepo because it survives someone renaming a function, which a hardcoded --entry value does not. The cost is a larger tree and a noisier diff. The examples directory in the repository, with examples/checkout and examples/react, is what the README's own dev commands point at, and it is the place to look for the shape of the output before you point calldiff at your own code.
Editorial conclusion
Adopt calldiff if you review changes where the shape of the call flow matters more than the line count, especially when an agent has rewired an entrypoint and you want to see which callees appeared, disappeared, or moved. Skip it if your code resolves calls at runtime through reflection, dependency injection or dynamic dispatch, because the README states plainly that this is syntactic and dynamic calls will not resolve. Before trusting a diff, run calldiff tree -e <entrypoint> --locs on both refs and check that the call sites shown match what you expect, since the tool reports the call site in the parent rather than the callee definition.
Frequently asked questions
What is calldiff used for?
It diffs call stacks across git commits, showing which callees appeared, disappeared or moved under an entrypoint. The README frames it for agentic code review, where a line diff buries the shape of a change an agent made.
How do I install calldiff?
Run npx calldiff@latest for a one-off, or npm install -g calldiff for a global install. Node 22.18.0 or newer is required according to the engines field in package.json.
Which languages does calldiff support?
The README lists TypeScript, TSX, JavaScript, JSX, Python, Go, Rust, Java, Ruby, C, C++, C#, PHP, Kotlin, Swift, Scala, Lua, Elixir, Bash, Haskell, Zig, Solidity, OCaml and Perl. Grammars are bundled or fetched on first use into ~/.cache/calldiff/grammars.
Does calldiff resolve dynamic calls?
No. The README states the analysis is syntactic and AST-based rather than a full typechecker, so dynamic calls will not resolve. Calls made through variables, registries or dependency injection will not appear as edges in the tree.
Can calldiff output JSON for an agent?
Yes. The --format flag accepts json, yaml, md and jsonl, producing a structured result with from, to, and trees or paths containing nested nodes plus a per-entry ascii rendering. calldiff mcp add registers it as an MCP server.
Community notes