Trace what a C# codebase actually runs.

Text search stops at the interface. csmesh compiles your solution's projects with Roslyn into a frozen symbol graph and prints the resolved path — which implementation the container injects, which handler a Send() reaches, which endpoints break if you touch a property — with a fixed cap on how much comes back, and a confidence score on every edge it draws.

curl -fsSL https://raw.githubusercontent.com/nRafinia/CsMesh/main/install.sh | sh
Read the source
.NET 10Native AOTRoslynBlazor & RazorMCP Serverincremental re-indexingzero idle contextMIT
$ csmesh trace PaymentController.Post --budget 600 exit 0118 tokens28 ms

Colour is information here, not decoration: amber is the implementation the container registered, violet is a mediator dispatch, teal is an HTTP entrypoint. The 28 ms is a warm query on the measured NativeAOT win-x64 channel (csmesh 0.6.0, 2026-09-21).

The loop this replaces

Ask an agent where a payment ends up in a layered .NET solution and it starts hopping files. Every hop costs a turn, and the context window pays for all of them.

grep, then read, then grep again

  1. grep for the interface12 hits: mocks, fakes, an XML doc comment — ~600 tokens
  2. open PaymentService.csfinds interface dispatch, not the body — ~800 tokens
  3. grep Program.cs for the registrationsifts through test fixtures — ~1,200 tokens
  4. open the handler, hit another Send()and the chain starts over — ~1,500 tokens

4 turns spent, roughly 4,100 tokens burned, still no answer

or ask the graph once

csmesh trace PaymentController.Post --budget 600

The container binding, the mediator hop, the concrete repository and the DbContext underneath it — resolved from compiler symbols, ordered so the code that actually runs comes first.

1 turn, 118 tokens, exit 0

Three things ripgrep will never resolve

Every layered .NET app is held together by indirection that exists only at compile time or at container-build time. Lexical search cannot follow any of it.

services.AddScoped<IStore, SqlStore>();

The binding

Grep finds twelve types implementing IStore — mocks, fakes, a legacy adapter. Exactly one is registered. csmesh tags it [di-bound] and ranks it first.

await _mediator.Send(cmd);

The dispatch

The handler sits in another project with no textual link back to the caller. csmesh matches the request type to its IRequestHandler and draws the edge across the boundary.

[HttpPost("/payments")] / @page "/checkout"

The entrypoint

Attribute routes, Blazor @page endpoints, MassTransit consumers, hosted services. Tagged as entrypoints, so a blast radius tells you which HTTP calls actually break — not which files mention the word.

Every edge says how it got there

A two-argument AddScoped is read off the compiler’s symbol table. A Scrutor assembly scan says a family of types is wired, not which pair. Both belong in the graph. Only one is a fact, and an engine that flattens the difference is one an agent goes back to grep to double-check.

Confidence = 1.0 | 0.75 | 0.55

How sure

1.0 when the compiler bound the symbol. 0.75 for a filtered assembly scan, 0.55 for an unfiltered one. The trust threshold is 0.8 — below it an edge is a lead to verify, and it never outranks an explicit registration in impl.

Source = semantic-registration

From where

roslyn-symbol, semantic-registration, semantic-request, factory-lambda, assembly-scan, short-name-match. The reasoning that produced the edge, not just its endpoints.

Site = src/Api/Startup.cs:41

Wired where

The file and line of the registration, or of the Send() call. The class’s own location is already on the node; this is the other half, so “now change the binding” doesn’t start with a search.

$ csmesh impl IStore --budget 600 exit 0
IStore  -- 2 implementation(s)
  SqlStore  [di:scoped, ?0.75 assembly-scan]  src/Data/SqlStore.cs:12  @ src/Api/Startup.cs:41
  InMemoryStore  [test]  tests/Fakes/InMemoryStore.cs:8

The scan-derived binding is still ranked first, still tagged — and still marked as a guess, with the score and the reason that produced it.

And what it could not resolve

A graph that answers “none” for every question it failed at reads like a graph that understood everything. csmesh unresolved --kind di lists the registrations that produced no edge and why each one failed. csmesh silence explains why a symbol has no callers. csmesh doctor reports index health, reference count, stale generated sources and version drift — run it before trusting an answer that surprises you.

Core commands, real output

Pick one. Every answer is capped by --budget, counted as roughly four characters per token — an estimate to size a reply, not a model’s tokenizer. Pass --heal to auto-refresh stale nodes on the fly. When the answer is bigger than the cap, the process exits 2 with a hint.

$

Empirical benchmarks on a real-world backend

Benchmarked across a private 29-project .NET solution (1,945 symbols, 5,328 edges; measured 2026-09-23 on csmesh 0.7.0). Comparing standard AI agent exploration (Ripgrep + sequential file reads) against single-command csmesh queries.

28msmedian query

Median of 20 warm runs on a NativeAOT win-x64 build, csmesh 0.6.0, 2026-09-21 — bypasses multi-hop model reasoning latency.

~93%less context

Measured ~93–94% less context in the two scenarios with a token baseline; the rest return csmesh token counts against qualitative baselines, not percentages.

1shell command

Replaces 3 to 7 exploratory turns into a single invocation that cannot overflow the budget it was given.

Workflow Scenariogrep & sequential readscsmesh (single command)Turn & Token Savings
DI binding (e.g. IOrderRepository)grep + read, ~1,500 tokenscsmesh impl IOrderRepository — 27.6 ms median, 97 tokens~94% token reduction
Call chain trace (OrderEndpoints.CreateOrderAsync, depth 2)5–7 turns, ~4,000 tokenscsmesh trace OrderEndpoints.CreateOrderAsync --depth 2 — 27.8 ms median, 288 tokens~93% token reduction
Blast radius (OrderService.UpdateAsync)4–6 turns, 24+ noisy hits (no token baseline)csmesh blast-radius OrderService.UpdateAsync — 27.4 ms median, 105 tokensCallers, not text matches; no percentage
Path finding (OrderEndpoints.CreateOrderAsync → OrderWriter)Impossible with grep (no token baseline)csmesh path OrderEndpoints.CreateOrderAsync OrderWriter — 29.7 ms median, 115 tokens1 deterministic step; no percentage
Entrypoint & worker discoveryMultiple greps + manual parsing (no token baseline)csmesh entrypoints — 24.8 ms median, 738 tokensEvery routing form it knows; no percentage
Type signature (OrderRecord)Locate + read whole file (no token baseline)csmesh context OrderRecord — 27.7 ms median, 478 tokensNo percentage
Architecture mapping (29 projects)Read .slnx + 29 .csproj files (no token baseline)csmesh map — 28.8 ms median, 766 tokensNo percentage

Symbol names above are placeholders; the numbers were measured on the real codebase with symbols of the same shape. Median of 20 warm runs (graph pre-indexed and held in the OS file cache) on a NativeAOT win-x64 build of csmesh 0.6.0, 2026-09-21, on a 13th Gen Intel Core i7-13620H (10 cores / 16 threads, 48 GB RAM, NVMe SSD). Token counts use csmesh's own estimator at roughly four characters per token.

Exit codes an agent can branch on

No text parsing, no heuristics. The process says what happened and what to do next.

0ok

Complete answer, within budget. Use it.

1not-found

No such symbol here. Check the spelling or run csmesh where to discover symbols.

2over-budget

The answer exists but is bigger than the cap. Narrow --depth rather than raising the budget.

3ambiguous

Several symbols match, including the same name declared in more than one project. Each candidate prints its project; re-run with --project <path> taken from that list, or with Type.Member. Two overloads in one project defeat --project; use the parameter list each candidate prints, e.g. Type.Member(int, string).

4no-index

No usable graph: none on disk, or (for review) one that predates HEAD. Run csmesh index.

5changed

review only: unaccepted structural change vs. the base revision. Fix it, or csmesh review --accept.

64usage-error

Bad flags or arguments, including review --accept while the index predates HEAD. Run the command with --help.

70internal-error

csmesh itself failed. Re-run with --debug; that is a bug, not your query.

75contended

The index file is held by another process and nothing was written. Wait briefly and re-run.

Teaches your assistant to use it

One command writes the skill and rule files each tool expects, in the repository or machine-wide with --global. Pass --mcp or --all to configure MCP server integration automatically. Shared files like AGENTS.md get a tagged block, so rules you wrote yourself survive the install.

csmesh install
VS Code.vscode/mcp.json + Copilot
JetBrains Rider.mcp.json + AGENTS.md
Claude Code.claude/skills/csmesh/SKILL.md
Cursor.cursor/rules/csmesh.mdc
Google Antigravity.agents/skills + mcp_config.json
Windsurf.windsurfrules
Cline & Roo Code.clinerules + cline_mcp_settings.json
GitHub Copilot.github/copilot-instructions.md
Kilo Code.kilocode/rules/csmesh.md
MiMo Code.mimocode/skills + AGENTS.md
Codex CLI and KimiAGENTS.md
Gemini CLIGEMINI.md
Anything reading AGENTS.mdopen agent standard block
A human in a terminalcsmesh --help

Install

Two paths. The prebuilt Native AOT binary needs nothing installed and, on the measured NativeAOT win-x64 channel, answers a warm query in ~28 ms median (csmesh 0.6.0, 2026-09-21). The global tool needs the .NET SDK, and from 0.6.1 it fetches the same native binary on win-x64, win-arm64, linux-x64, linux-arm64, osx-arm64 and osx-x64, falling back to the framework-dependent build elsewhere.

Standalone binary

curl -fsSL https://raw.githubusercontent.com/nRafinia/CsMesh/main/install.sh | sh

The scripts download the prebuilt Native AOT binary (win-x64, win-arm64, linux-x64, linux-arm64, osx-arm64, osx-x64) and fall back to dotnet tool automatically when no release asset matches your architecture.

.NET global tool

dotnet tool install --global CsMesh
dotnet tool update --global CsMesh

From 0.6.1 this is a RID-specific package: on win-x64, win-arm64, linux-x64, linux-arm64, osx-arm64 and osx-x64 it delivers the Native AOT binary; elsewhere it falls back to the framework-dependent build. Then, in any repository with a .sln, .slnx or .csproj: restore first (dotnet restore) so package references are on disk, then run csmesh index; when the code uses source generators, build with dotnet build --no-incremental -p:EmitCompilerGeneratedFiles=true so their output is on disk too. A cold checkout resolves less completely than a restored one. Use --heal on any query or rerun csmesh index when files change.