calldiff: a git diff for who-calls-whom
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?
- An AST-based call-graph differ that shows which callees appeared, disappeared or moved under an entrypoint between two commits, built for reviewing rewrites an agent just made. It is syntactic rather than semantic, so dynamic calls silently fail to resolve, and the language count is given three different ways across the repository.
- Who is it for?
- Adopt calldiff if you review refactors, particularly ones an agent has just made, where a line diff hides the fact that three sibling calls got wrapped in a new function. Do not adopt it as a reachability oracle for code that dispatches dynamically, since it works on syntax trees and says so in a single sentence.
- 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 44 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 October 10, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The language count disagrees with itself three ways
Count the claims. The repository description says 22 languages are supported. The README's opening says 23, and the package manifest's description repeats 23. Then count the README's own list: 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. That is 24 entries. None of these three numbers can all be right, and for a tool whose selling point is breadth, the discrepancy is worth resolving before you rely on it. The more consequential detail sits underneath: the manifest declares exactly two tree-sitter packages, `tree-sitter` and `tree-sitter-typescript`, so four of those languages are covered by shipped dependencies and the remaining twenty arrive at runtime. The pipeline detects the language by file extension, loads a grammar bundled or on-demand into `~/.cache/calldiff/grammars`, and parses. Grammars install on first use, and the cache location can be overridden with `CALLDIFF_GRAMMAR_CACHE`.
Syntactic, and quiet about what it cannot see
One sentence in the How It Works section is the most important thing in the documentation: this is syntactic and AST-based, not a full typechecker, so dynamic calls will not resolve. There is no hedging and no list of exceptions. What that costs you depends on the language. In Python, a call through `getattr`, a dispatch table or an entry-point lookup simply does not appear in the tree. In JavaScript and TypeScript, a computed property access or a string-keyed method lookup has the same fate. So the tree calldiff prints is the tree the syntax describes, and a caller that reaches a callee through indirection looks like it does not call it at all. That matters for the tool's headline use, because if you are reviewing an agent's rewrite and you need to prove a path is gone, the absence of an edge in the output is not proof that the path is gone. Reachability, here, means syntactic reachability.
Three verbs, and only one of them diffs
The CLI has three subcommands and they answer three different questions. `calldiff diff` compares call trees: with no arguments it compares HEAD against the working tree, with one ref it compares that ref against the working tree, and with two it compares them to each other. A line marked `-` was present in the first side and is gone in the second; a line marked `+` is new. If you do not force an entrypoint with `--entry` or `--file`, it infers exported functions whose expanded trees changed, and may show you several. `calldiff tree` prints a plain ASCII tree with no diff markers, and requires `--entry`/`-e` or `--file`/`-F`. `calldiff reach` is the one that has no equivalent in git: it prints every call path from an entrypoint to a target, including the alternate arms of conditionals, and requires `--to`. If your question is whether A can still reach B after a commit, that is the subcommand, and a line diff cannot answer it at any verbosity.
Ambiguous paths fail instead of picking one
Entrypoints can be named two ways. `--entry` and `-e` take symbols only, in the form `functionName` or `ClassName.method`. `--file` and `-F` take an indexed source path and expand to every exported symbol defined in that file, which the documentation calls useful in monorepos. Path matching accepts either an exact path or a unique suffix, so `boot.ts` can resolve to `packages/api/src/boot.ts`. What happens when the suffix is not unique is the part that matters: ambiguous matches produce an error so you can pass a more specific path. That is a deliberate choice against the usual behaviour of a resolver, which would take the first match and hand you a tree for the wrong file. In a monorepo where dozens of files are called `index.ts` and `boot.ts`, silently picking one would produce a confident and wrong answer, and a confident wrong answer in a review tool is worse than a failed command.
Call sites, not definitions, with an LSP precedent
Source locations are off by default and enabled with `--locs`, and when they are on the rule is precise enough to be worth reading twice. The root node uses the definition location, `file:line`. Children use the call site in the parent, either `file:line` or a range as `file:line-line`. The documentation names the precedent explicitly: the same idea as LSP Call Hierarchy `fromRanges`, and explicitly not the same as Go to Definition. The difference is what you are trying to find. A definition tells you where a function lives. A call site tells you where the parent invokes it, which is what you need when you are asking why a change to one call should have propagated somewhere else. No other diff tool in this category reports this distinction, and getting it wrong would send you to the callee's declaration when you meant the caller's line.
Conditionals and JSX tags are nodes, which is the whole point
The label vocabulary explains why the trees can show a shape that a call graph normally flattens. A free function prints as `functionName`, a method as `ClassName.method`, and a construction as `new ClassName`. JSX and TSX component tags print as `Component`, and their children nest under the parent, so a `<Button />` inside `<Modal />` is a parent-child pair in the tree rather than a string in a file. Conditionals print as `if (cond)`, `else if (cond)` and `else`, and they deliberately carry no continuing vertical rail, which is a small typographic decision that stops an alternate arm from reading as a deeper level of nesting than it is. Put those together and the output can express control flow, not just linear invocation. That is what the opening example shows, and it is the clearest argument for the tool existing: three sibling constructor calls replaced by one wrapper that now contains them is a structural change that a line-oriented diff renders as an unrelated scatter of additions and deletions.
The agent prompt calls your assistant 'dearest clod'
The README ships a prompt template under the heading for agents, meant to be pasted verbatim when you want a walkthrough of call-flow changes: dearest clod, walk me through the code changes you made using `npx calldiff@latest`. It is worth pausing on, because it makes a decision that most tool documentation avoids. The intended workflow is not that a person reads a diff, but that the agent that produced the diff is asked to explain its own structural change using this tool as evidence. That is either a joke or a serious ergonomics choice, and it has the same effect either way, because a specific, slightly absurd phrase is far easier to paste correctly than a paragraph of instructions. Installing is the ordinary two routes, `npx calldiff@latest` or a global npm install, and the dev scripts show the same three verbs against the bundled examples:
npm run dev -- diff main HEAD --entry PiService.createAgentSession
npm run dev -- tree -e runCheckout -- examples/checkout
npm run dev -- reach -e runCheckout --to sendEmail -- examples/checkoutThose three lines are also the fastest way to understand the tool, since they are the three questions in order.
incur brings MCP, skills, and prompts after every diff
The CLI is built on `incur` from wevm, and that dependency shows. Incur supplies `skills add`, `mcp add`, an `--llms` flag, typed flags, and CTAs printed after diffs. The first two mean the tool can install itself as either an agent skill or an MCP server, which covers both integration styles in current use. The last one is the interesting choice: a review tool that prints a call to action after every diff is deciding to interrupt you rather than stay silent, and that is a bet about whether people act on the output. The rest of the manifest is small and deliberate. Four runtime dependencies, `incur`, `picocolors`, `tree-sitter` and `tree-sitter-typescript`. An `overrides` block that forces `tree-sitter` to a single shared instance inside the TypeScript grammar, which is the standard fix for the duplicate native-binding problem when two copies of a node library get loaded. Node 22.18.0 or newer. Version 0.6.0, linting through oxlint rather than eslint, tests through vitest, and a published tarball that declares only `dist` as its content. There are no GitHub releases, so npm is the only distribution channel, and the last push to main was on 2026-08-27.
Editorial conclusion
Adopt calldiff if you review refactors, particularly ones an agent has just made, where a line diff hides the fact that three sibling calls got wrapped in a new function. Do not adopt it as a reachability oracle for code that dispatches dynamically, since it works on syntax trees and says so in a single sentence. Verify first which grammars you need will actually be present, because only the TypeScript packages ship as dependencies and everything else downloads into a cache directory on first use.
Frequently asked questions
What does calldiff compare?
Call trees rather than lines. `calldiff diff` compares HEAD against the working tree, `calldiff diff <from>` compares a ref against the working tree, and `calldiff diff <from> <to>` compares two refs. A line marked `-` was present in the first side and is gone in the second, while `+` marks something new in the second.
How many languages does calldiff support?
The repository is inconsistent about it. The description says 22, the README headline and the package manifest say 23, and the README's own list of 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 enumerates 24.
Does calldiff resolve dynamic function calls?
No. It is syntactic and AST-based rather than a full typechecker, so dynamic calls will not resolve. Only `tree-sitter` and `tree-sitter-typescript` are declared dependencies; other grammars install on first use into `~/.cache/calldiff/grammars`, and the cache location can be overridden with `CALLDIFF_GRAMMAR_CACHE`.
What does the calldiff reach subcommand do?
It prints every call path from an entrypoint to a target, including the alternate arms of conditionals. It requires either `--entry`/`-e` or `--file`/`-F` plus `--to`, and works against the working tree or against a named ref.
How do I run calldiff?
With `npx calldiff@latest` or a global `npm install -g calldiff`. Node 22.18.0 or newer is required, and the manifest version is 0.6.0. Output is colored ASCII on a TTY and colorless when piped, or structured data with `--format json|yaml|md|jsonl`.
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/tanishqkancharla-calldiff)