# diffctx — smart diff context for LLM code review
[](https://github.com/nikolay-e/diffctx/actions/workflows/ci.yml)
[](https://pypi.org/project/diffctx/)
[](https://crates.io/crates/diffctx)
[](https://www.npmjs.com/package/diffctx)
[](https://pypi.org/project/diffctx/)
**diffctx selects the minimum code an LLM needs to review a git diff.**
Instead of pasting whole files, it walks the dependency graph outward from the
changed lines and stops once more context stops paying for itself.
> Formerly published as `treemapper` — every command, flag, and API call works unchanged.
## How it compares
Whole-repo packers (repomix and friends) seed on the repository and export
everything; persistent code-graph servers answer structural queries against a
maintained index. diffctx is **diff-seeded**: the input is a change, the
output is the fragments needed to understand it, packed under a hard token
budget — local, deterministic, no index, no model calls. Measured results and
when the other two families fit better: [COMPARISON.md](COMPARISON.md).
## Install
```bash
uvx diffctx . --diff HEAD~1 # zero-install, run once via uv
pipx install diffctx # recommended: isolated CLI, no venv needed
pip install diffctx # or: into an active environment
pipx install 'diffctx[mcp]' # + MCP server for AI assistants
```
Without Python:
```bash
cargo install diffctx # native CLI from crates.io
npx diffctx . --diff HEAD~1 # npm wrapper over the native binary
docker run --rm -v "$PWD:/repo" ghcr.io/nikolay-e/diffctx . --diff HEAD~1
```
On Windows, via Scoop (this repository is the bucket):
```powershell
scoop bucket add diffctx https://github.com/nikolay-e/diffctx
scoop install diffctx/diffctx
```
Prebuilt binaries for linux (x86_64/aarch64), macOS (arm64) and Windows (x64)
are attached to every [release](https://github.com/nikolay-e/diffctx/releases/latest).
The native binary and Docker image cover diff mode with YAML/JSON output and
write to stdout (redirect to capture); tree mode, Markdown output, the `graph`
subcommand and the MCP server live in the Python package.
## Quick start
```bash
diffctx . --diff HEAD~1 # smart context for last commit → paste into Claude/ChatGPT
diffctx . -f md -c # full codebase export → clipboard in Markdown
```

*`diffctx . --diff HEAD~1` selects only the fragments an LLM needs to review the
last commit, instead of dumping every changed file in full.*
## Diff context mode
Finds the minimal set of fragments needed to understand a change — imports,
callers, type definitions, config dependencies — across 50+ file types. It
builds a code graph (imports, co-changes, type refs), propagates relevance
outward from the changed lines, and stops when relevance drops below `--tau` or
the `--budget` token cap is hit.
`--diff` takes a git range (`HEAD~1..HEAD`, `main..feature`) or a **duration
window ending now** — `24h`, `8d`, `90min`, `1h30m`, `2w` (units `s`, `m`/`min`,
`h`, `d`, `w`, composable). A window diffs the working tree against the last
commit before it, so it covers the commits made inside the window *plus* the
uncommitted and untracked work on top — `diffctx . --diff 24h` is "everything I
touched today". A ref that happens to look like a duration (a branch `24h`)
keeps its git meaning.
| Flag | Default | Description |
|-------------|---------|--------------------------------------------------------------------------|
| `--scoring` | `ego` | `ego` = bounded expansion around changed nodes (fast, predictable radius); `ppr` = Personalized PageRank (global, smoother decay, slower); `bm25` = lexical retrieval against the diff hunks (baseline for sparse graphs); `rrf` = reciprocal-rank fusion of `ego` and `bm25` (widest recall, no scale calibration between the two signals) |
| `--budget` | auto | Hard cap in o200k_base tokens (see [Token counting](docs/product/token-budget.md)): `N` enforces a fixed cap, `-1` disables it, `0` is a strict-zero floor (empty selection; use `--full` for changed files only) |
| `--alpha` | 0.60 | PPR damping; higher = context clusters tighter around changes (`--scoring ppr` only) |
| `--tau` | 0.12 | Relevance threshold for full fragment content; lower-scoring fragments are stubbed or dropped (lower = more context) |
| `--full` | false | Only the changed files, every fragment, no related-code context |
| `--timeout` | 300 | Wall-clock deadline in seconds; on expiry diffctx exits 124 instead of hanging |
| `--with-raw-diff` | false | Also embed git's raw unified diff ahead of the selected fragments — additive (selection unchanged), not charged to `--budget`, lock/ignored/secret-like sections omitted. Python CLI only |
| `--mode` | `pack` | `locate` emits the same ranked selection as compact `diffctx.locate.v1` JSON — path, lines, score, provenance reasons, a blast-radius `summary` and per-item impact `group` (`test`/`type`/`config`), NO source bodies. Adds a `coverage` block naming what the run could not see (`unparsed_files`, `zero_edge_files`, `ppr_truncated`, `next_up`, a heuristic `confidence`) and an `overflow` ranking of what the budget left behind — omitted entirely when there is nothing to disclose. `diffctx . --diff --mode locate` = impact of your uncommitted change. The MCP tool takes it as `mode="locate"` |
### `graph` subcommand
Explore the underlying dependency graph directly, without a diff:
```bash
diffctx graph . # Mermaid graph of directory deps (default)
diffctx graph . --summary # cycles, hotspots, coupling metrics
diffctx graph . --level fragment -f json # fragment-level graph as JSON
diffctx graph . --level file -f graphml -o g.xml # file-level graph as GraphML
```
## Usage
<!-- BEGIN USAGE -->
```bash
# full codebase export:
diffctx . # Markdown to stdout + token count
diffctx . -f md -c # Markdown → clipboard
diffctx . -f json -o tree.json # JSON → file
diffctx . --no-content # structure only, no file contents
diffctx . --max-depth 3 # limit depth
diffctx . -i custom.ignore # custom ignore patterns
# diff context mode (requires git repo):
diffctx . --diff # uncommitted changes (working tree vs HEAD)
diffctx . --diff HEAD~1 # context for last commit
diffctx . --diff main..feature # context for feature branch
diffctx . --diff 24h # everything changed in the last 24 hours
diffctx . --diff 8d # same over 8 days (also 90s, 10min, 1h30m, 2w)
diffctx . --diff HEAD~1 --budget 30000 # limit to ~30k tokens
diffctx . --diff HEAD~1 -c # diff context to clipboard
diffctx . --diff HEAD~1 --with-raw-diff # raw patch + selected context
diffctx . --diff HEAD~1 --mode locate # ranked navigation JSON, no source
```
<!-- END USAGE -->
Every run reports token count and size on stderr — `12,847 tokens
(o200k_base), 52.3 KB`. Counts are exact only for the GPT-4o family; Claude,
Gemini and others tokenize differently, so treat `--budget` as an upper bound
and leave headroom ([details](docs/product/token-budget.md)). Unreadable files
become placeholders like `<binary file: N bytes>`.
## Python API
```python
from pathlib import Path
from diffctx import build_diff_context, map_directory, to_json, to_markdown, to_text, to_yaml
ctx = build_diff_context(
Path("."),
"HEAD~1..HEAD",
budget_tokens=None, # None = auto; 0 = strict-zero floor (empty); -1 = uncapped; N = hard cap
alpha=0.6,
tau=0.12,
full=False,
scoring_mode="ego",
timeout=300,
with_raw_diff=False, # True also embeds the raw unified diff (not charged to budget)
)
print(to_markdown(ctx))
tree = map_directory(
".",
max_depth=None,
no_content=False,
max_file_bytes=None,
ignore_file=None,
no_default_ignores=False,
whitelist_file=None,
)
print(to_yaml(tree))
```
## MCP server
[](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.nikolay-e/diffctx)
[](https://glama.ai/mcp/servers/nikolay-e/diffctx)
diffctx includes an [MCP](https://modelcontextprotocol.io) server that lets AI
assistants (Claude Code, Cursor, Windsurf, etc.) call diff context analysis
automatically during code review. It is published in the official MCP registry
as `io.github.nikolay-e/diffctx`. One-line setup (zero-install via
[uv](https://docs.astral.sh/uv/)):
```bash
# Claude Code
claude mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
# Codex CLI
codex mcp add diffctx -- uvx --from 'diffctx[mcp]' diffctx-mcp
# Gemini CLI
gemini mcp add diffctx uvx -- --from 'diffctx[mcp]' diffctx-mcp
# VS Code
code --add-mcp '{"name":"diffctx","command":"uvx","args":["--from","diffctx[mcp]","diffctx-mcp"]}'
```
With `pip install 'diffctx[mcp]'` already done, replace the
`uvx --from 'diffctx[mcp]' diffctx-mcp` tail with plain `diffctx-mcp`.
The server exposes one tool, `diffctx_context`, that assistants call when
reviewing PRs, explaining changes, or investigating broken tests. It ranks the
code that explains a diff, then reads only the fragments the assistant picked —
two calls that pay for the selection instead of a whole pack. The wider
`get_tree_map` and `get_file_context` tools are opt-in via
`DIFFCTX_MCP_LEGACY_TOOLS=1`. Tool reference and JSON configs for Claude
Desktop, Cursor, Continue, Windsurf, and Zed:
[`src/diffctx/mcp/README.md`](src/diffctx/mcp/README.md).
## Ignore patterns
Respects `.gitignore` and `.diffctx/ignore` automatically — hierarchically at
every directory level, with full gitignore semantics (negation `!important.log`,
anchored `/root_only.txt`). `.diffctx/whitelist` acts as an include-only filter,
and the output file is always auto-ignored. `--no-default-ignores` disables the
built-in patterns; `--no-ignores` disables all ignore rules (tree mode only).
An excluded path never appears in the output in any role: in diff mode it is
dropped both from `changed_files` and from the candidate universe, so it cannot
come back as a related-context fragment either (including under `--full`). The
same guarantee covers secret-like paths (`id_rsa`, `*.pem`, `*.key`, ...),
which are filtered even without an ignore entry.
## Token cache
Diff mode caches per-blob tokenization in the OS cache directory (e.g.
`~/Library/Caches/diffctx/token-cache`) — a pure speedup, safe to delete.
`DIFFCTX_TOKEN_CACHE_DIR` relocates it; `DIFFCTX_TOKEN_CACHE_MAX_BYTES` caps
its size (default 512 MB, `0` disables eviction).
## Exit codes
| Code | Meaning |
|------|---------|
| `0` | Success — output contains content |
| `1` | Runtime error (bad path, permission denied, etc.) |
| `2` | Usage error (invalid flags/arguments) |
| `3` | Environment error (`--diff` outside a git repo, git not installed, no commits yet) |
| `4` | `--diff` produced no semantic context (clean tree, binary-only, everything filtered); output is still emitted. Deletion/rename/lockfile-only diffs list `deleted_files`/`renamed_files`/`lockfile_changes` and exit `0` |
| `124`| `--diff` exceeded the `--timeout` wall-clock deadline |
| `130`| Interrupted (Ctrl-C) |
| `141`| Broken pipe (e.g. piping into `head`) |
## License
Apache 2.0
<!-- mcp-name: io.github.nikolay-e/diffctx -->
<!-- Ownership marker read from the PyPI description by the MCP registry. -->
<!-- Must survive edits verbatim: one space after the colon, case-sensitive. -->
---
- [Documentation site](https://nikolay-e.github.io/diffctx/) — the pipeline
end to end: diff → fragments → graph → relevance → selection
- [GitHub Action](docs/product/github-action.md) — diff context as a CI step
for LLM review
- [Token counting](docs/product/token-budget.md) — which encoder, and what
`--budget` means for non-GPT models
- [Comparison](COMPARISON.md) — measured results, and when a whole-repo packer
or a persistent code-graph server fits better
- [Paper](https://doi.org/10.5281/zenodo.18824579) — budgeted typed-graph
retrieval for diff-aware context selection (Zenodo, 2026)
- [Changelog](CHANGELOG.md)
- [Security policy](SECURITY.md) — threat model and vulnerability reporting
- [Parameter strategy](docs/engineering/parameter-strategy.md) — how `--alpha`,
`--tau`, and edge weights are calibrated