New
Apps 1,094Makers 1,092This week 1,094
Promote your app−30%
ctx screenshot 1
1views in
the last 30 days

ctx

ctx — a queryable code graph for coding agents, across Rust, Python, TypeScript and Go. Call...

Built with Claude Code

by eagle-logic · added Oct 29, 2026
no ratings0 comments

ctx

The binary is ctx. The crate is code-context (ctx was taken on crates.io in 2017).

A queryable code graph for coding agents. Not a map you paste at boot — a set of queries an agent runs mid-task: who calls this, how does execution get here, what breaks if I change it, what must a move touch, does my port still match the original.

One Rust binary. Tree-sitter for Rust, Python, TypeScript/TSX, JavaScript/JSX, Go, Java, and Markdown. No language servers, no embeddings, no index to warm. The graph is a pure function of the source tree — same code in, same answer out — and it builds in ~100 ms, so every query runs against current source.

$ ctx callers resolve_call
1 caller(s) of 'resolve_call':

extract::Walk::rec  (src/extract/mod.rs:2368-2462  #ab0418a77d9f)  → resolve_call

completeness: no call site named `resolve_call` went unresolved anywhere in this tree —
this blast radius is complete to the limit of what ctx parses. At least 1005 undescended region(s) in this tree were not read; see `ctx doctor`.

That last line is the whole idea, and it does two jobs. It claims completeness for what ctx read, and it bounds that claim by counting what it never read — macro bodies and static/const initializers, whose contents are not parsed as code. A call hiding in one is absent rather than unresolved, so nothing else would have revealed it. Every other code graph tells you what it found. ctx tells you what it missed — per edge, in aggregate, and with the grep list to check it yourself.

It matters because the failure mode of a code graph is silent. A blast radius that comes back short reads exactly like a blast radius that is complete, and an agent cannot tell the difference — so it changes the signature and ships the break. Every tool in this space has gaps; this one is built so you can enumerate them.

→ EXAMPLES.md — ten commands run against this repo, verbatim output, including the parts where ctx reports its own limits.

→ BENCHMARK.md — the same claims re-run on six public repositories pinned to exact commits, reproducible with ./bench/run.sh. Including the row where it finds a limitation in ctx.

→ What the integration itself costs — ctx's own MCP tool definitions sit in every turn whether a tool is ever called or not, which makes them 2.8× the standing cost of the CLI instructions block. That is the shape this project objects to, measured on itself.

Four things you won't find elsewhere

1. Every edge tells you how much to trust it

Every static analyzer guesses. ctx is the one that says where.

An edge backed by a resolved import, a path, a self receiver, or a declared type is unmarked — rely on it. An edge inferred from an opaque receiver (expr.method(), where nothing in the source states the type) is marked ~. A branch of a dynamic-dispatch fan-out is marked *. And ctx doctor names every callee it could not pin at all, with counts:

## Internal recall — the number to trust
  1397/1467 = 95.2%   of call sites that could be internal, ctx pinned this many.

## What ctx missed (callee names that exist here but went unpinned)
grep these; every other edge in the map is one ctx could prove.
     49  walk
      9  context
      3  est_tokens
      3  path

## Low-confidence zones (edges to distrust — grep to confirm)
  parity                           18% heuristic (11/61 edges)
  refactor                         5% heuristic (1/20 edges)
  crate                            4% heuristic (6/154 edges)

The recall number comes with the exact grep list for everything it doesn't cover. The denominator is honest too: a call into std or a third-party crate is excluded, because no internal edge could exist for it however good the resolver gets — and that classification is by evidence (is this name defined anywhere under the root?), not a hardcoded list.

Heuristic method attribution is also language-scoped. A unique method name is evidence only within one language; across languages it is coincidence, so tok.apply_chat_template(...) in Python is never attributed to a Rust method of the same name. Before that guard, 45 of 50 reported callers of that name were artifacts and ~18% of module dep edges were impossible Python→Rust edges — which then distorted core's ranking.

The honest bit isn't that coverage is high. It's that the gaps are enumerable.

That property is not decoration — it is how this tool gets built. Go support went from 36.8% to 89.6% internal recall on go-chi/chi in six steps, and every step was a class of call site the census had already named by count: package scope spanning files, constructor return types, calls leaving through an external import, a func literal shadowing its enclosing scope, the /vN in a module path binding the wrong package name, and method chains. Not one of those was visible in the aggregate number. Each was one line in What ctx missed, with the grep to confirm it.

A tool that reported only "36.8%" would have told you it was bad without telling you why, which is the position every other code graph leaves you in.

2. changed --api — a breaking-change gate that names who breaks

Builds the public API surface at a base ref (in a throwaway detached worktree) and in the working tree, then reports what was removed or whose signature changed — each with the callers it breaks.

$ ctx changed --api --since HEAD~1
# API changes vs HEAD~1
0 removed, 4 changed, 7 added.

## Changed signature — potentially breaking
- query::coverage_report  [fn]  (src/query.rs:1420)
    was: pub fn coverage_report(g: &Graph, unsupported: &[(String, usize)], json_out: bool) -> String
    now: pub fn coverage_report(g: &Graph, unsupported: &[(String, usize)], explain: bool, json_out: bool) -> String
    callers (5): mcp::dispatch, query::tests::coverage_separates_internal_external_and_blind_spots, …

--strict turns it into a CI gate, and fails only on removals. That asymmetry is deliberate. A removal is unambiguous: nothing can call what is gone. A signature change might be an added optional parameter or a new struct field — routine, compatible work — and ctx is structural, with no type resolution, so it cannot tell that from a changed parameter type. Failing on both was tried and would have blocked ordinary additive commits on the first real repo it met; a gate that cries wolf gets switched off, which is worse than no gate. So signature changes are reported, and under --strict say explicitly that they were reported and not failed.

Treat it as a tripwire that surfaces an unintended removal in review, not as a semver authority — a language-specific tool with a type system (cargo-semver-checks, apidiff, japicmp) is stricter within its language. The trade ctx makes is breadth: one gate across Rust, Python, TypeScript, JavaScript and Go in a polyglot repo.

# .github/workflows/ci.yml — PR-only; fetch-depth 0, the default shallow clone
# has no merge base.
- uses: actions/checkout@v7
  with: { fetch-depth: 0 }
- run: cargo install code-context --locked
- run: ctx changed --api --strict --since "origin/${{ github.base_ref }}"

This isn't a context tool. It's CI.

3. move-plan — a refactor oracle, not an actuator

move-plan <from> <to> emits every site a module relocation must touch; move-verify checks the result. ctx never writes source files.

ctx move-plan native::gate native::routing::gate   # file move + every import rewrite
# ... agent applies the edits ...
ctx move-verify native::gate native::routing::gate # exits non-zero if anything is orphaned

The reasoning: an agent can already make edits cheaply and precisely. What it cannot do is know it found every site, or prove nothing was orphaned. The scarce thing is ground truth, not typing — and staying read-only keeps ctx free of partial application and undo semantics.

Scope is bounded by what ctx can prove. Moves ride on import and link resolution — path arithmetic, deterministic, non-heuristic — which is ctx's strongest signal, so the site list is exact for the languages it parses. Dependents reached only by receiver inference have no literal string to rewrite and are listed separately as unverified, never mixed in with real sites. Rust mod declarations are not imports, so the plan names them as a required manual step rather than omitting them silently. Renaming a method is deliberately not offered: it would ride on receiver inference, which resolves a minority of call sites, and a plan built on that would silently miss some.

4. parity — cross-language port fidelity

Porting Python to Rust, or TypeScript to Python? parity answers "is this port a faithful structural copy?" Because the item model is language-neutral, a source module and its port are two renderings of one skeleton.

$ ctx parity research/gate.py src/gate.rs --aliases py-rust
# ctx parity — source → port
source 6 members · target 5 · aligned 5 (83% of source) · 1 via alias

## Missing in port (1) — in source, no counterpart in target
  fn   Gate.record                  gate.py:18

## Arity drift (1)
  fn   Gate.score               source=2 → port=1   gate.py:7

## Aligned via alias (1) — matched through a rename rule, not exactly
  fn   Gate.__init__  →  Gate.new   (via init → new)

parity: 5/6 source aligned · 1 missing · 1 arity · 0 call · 0 moved

A dropped method, a dropped parameter, and the __init__→new rename, in one command. --strict exits non-zero on any missing member. I'm not aware of another tool that does this. It might be more or less useful depending on your specific languages and abstractions.

"How is this different from aider's repo map?"

Aider's repo map — and the MCP servers that repackage it, like RepoMapper — compresses a repository into a ranked blob that gets injected at the start of a session, sized to a token budget. It's a good answer to "what is this codebase" for a model that has seen none of it.

ctx answers a different question. Once the agent is in a task, it doesn't need the repo ranked — it needs specific facts: trace, path, callers, context, changed --api, move-plan, parity. Those are queries mid-task, not a blob at boot. Ranking is one small command here (ctx core), not the product.

The nearer comparison is Serena, which does LSP-backed symbol navigation. Against it, ctx trades semantic precision for two things:

ctx LSP-based (Serena)
setup one binary, grammars compiled in a language server per language, configured and running
startup ~100 ms graph build, per query server warm-up, project indexing
determinism pure function of the source tree depends on server state, versions, build artifacts
uncertainty marked per edge (~, *) + a miss census resolved or absent, silently
coverage 7 languages (today), one graph across all of them as many as you install servers for

If you need type-perfect resolution inside one language, use an LSP. If you want the same graph across a polyglot repo with nothing to install and answers that state their own confidence, that's this.

ctx does ship the boot map too (ctx map), so you can have it if you want it. But we'll say plainly what dogfooding taught us: it's the least useful thing here. More on why.

"Why not just grep?"

Often you should. Here is the measurement, including where grep wins.

Counting tokens. Every figure in this README is ctx's own estimate — len/3, the same one --max-tokens and --metrics use. Recompute with a real tokenizer and you will get different absolutes; the ratios are what the argument rests on, and they come from one estimator on both sides. len/3 is also conservative for source code, which tokenizes closer to 3.5–4 characters per token, so if anything the grep figures below understate the gap.

The grep below is the one a competent agent would actually write — call syntax, language-filtered, skipping the same directories ctx skips — not a bare grep -rn name:

grep -rn --include='*.py' --include='*.rs' --include='*.ts' \
  --exclude-dir={.git,node_modules,target,__pycache__,.venv,venv,dist,build} \
  '\bload(' .

On a small repo, grep wins. This repository, 14 source files:

grep -rn --include='*.rs' --exclude-dir=target '\bcoverage_report(' src/

194 tokens against ctx callers coverage_report's 215. Jump-to-definition is worse for ctx — grep -rn --include='*.rs' --exclude-dir=target 'struct Universe' src/ is 14 tokens against ctx def's 124. At this size the graph is overhead and you should use grep.

On a large repo it inverts. A 14,073-file tree:

"who calls X" ctx callers fair grep grep lines callers ctx found
load 4,014 32,994 784 90
run 441 11,017 242 8
boot 1,693 4,744 98 31

3× to 25× fewer tokens. grep's cost scales with how common the string is; ctx's scales with how many call edges actually exist. boot is the weak case at 2.8× — worth knowing, because a tool that only ever reports its best ratio is advertising rather than measuring. run is the strong one: 242 matching lines, genuinely called from 8 places, and grep's output for it would not fit in most context windows.

But the size difference is not really the point — the two answer different questions. Same task, same repo:

grep:  src/mcp.rs:475:    query::coverage_report(&g, &unsupported, barg("explain"), false),
       src/query.rs:1548:pub fn coverage_report(
       src/query.rs:2177:        let out = coverage_report(&g, &[], false, false);

ctx:   mcp::dispatch  (src/mcp.rs:361)  → query::coverage_report
       query::tests::coverage_separates_internal_external_and_blind_spots  (src/query.rs:2856)
       query::tests::doctor_names_what_it_could_not_pin  (src/query.rs:2194)

grep returns lines containing the text — including the definition itself, mixed in with the calls. ctx returns the functions that call it. "What breaks if I change this signature" is answered by a list of callers, not a list of lines, so grep's output needs a second pass to read around each hit and work out which function it lands in. That pass is the part ctx has already done.

And grep is complete where ctx is not. ctx only reports edges it could resolve, so it undercounts — which is why every answer carries its own error bar:

completeness: 240 call site(s) named `load` could not be pinned
(scripts.regress 10, api 6, tests::progressive_integration 6, ...) —
this blast radius may be incomplete; grep `load` to confirm.

So: grep is complete and imprecise; ctx is precise and tells you where it isn't. They compose. ctx narrows 242 lines to 8 callers and then names the places you still need grep — which is a better division of labour than either tool doing the whole job.

Install

No Rust toolchain required — prebuilt binaries for macOS, Linux and Windows:

# macOS / Linux
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/Eagle-Logic/context/releases/latest/download/code-context-installer.sh | sh

# Windows (PowerShell)
irm https://github.com/Eagle-Logic/context/releases/latest/download/code-context-installer.ps1 | iex
# Homebrew
brew install eagle-logic/tap/ctx

With a Rust toolchain (1.90+):

cargo install code-context        # the binary it installs is `ctx`

# or from source
git clone https://github.com/Eagle-Logic/context
cd context && cargo install --path .

Single binary, no runtime dependencies — the tree-sitter grammars are compiled in. Prebuilt for x86_64/aarch64 macOS, x86_64 Linux (gnu and static musl, so it drops into a distroless or scratch container), and x86_64 Windows.

macOS: use the installer script or Homebrew above. The release archives are not code-signed, so a .tar.xz downloaded through a browser gets quarantined by Gatekeeper and refuses to run until you clear it (xattr -d com.apple.quarantine ./ctx). The curl | sh path doesn't set the quarantine attribute and is unaffected.

The queries

Start here: ctx core to orient, ctx def to find a symbol, ctx context to work on it. That sequence is the tool. Everything below is a specialisation of it, and the whole-repo map at the bottom is measured at 42× the cost of core for the same orientation job.

# Everything needed to edit a symbol, in one call (def + types + callees + callers)
ctx context streamChat --max-tokens 4000
ctx context streamChat --include-source   # + the source, so it answers alone

# Who calls this? (resolved reverse call edges — the blast radius)
ctx callers basename

# How does execution get here? (transitive call tree, not one hop)
ctx trace decode_step --depth 4
ctx trace decode_step --reverse       # everything that reaches it

# The shortest call path between two symbols, hop by hop
ctx path main flush_kv_cache

# Where is a symbol defined? (jump-to-def without knowing the module)
ctx def SteerConfig
ctx def 'Type::method'                # qualified to disambiguate

# Breaking-change gate: public API removed/changed since a ref + who breaks
ctx changed --api --since main
ctx changed --api --strict --since main   # exit non-zero on a removal — for CI

# Plan a module move, then prove it landed
ctx move-plan native::gate native::routing::gate
ctx move-verify native::gate native::routing::gate

# Impact map of your current diff: changed modules + deps + callers
ctx changed                           # working tree vs HEAD
ctx changed --since main

# Structural diff between two refs (review a whole branch/PR)
ctx diff main..feature                # changed modules + who they break
ctx diff main..feature --api          # breaking API changes across the range

# Cross-language parity: is the Rust port faithful to the Python source?
ctx parity research/gate.py src/gate.rs
ctx parity research/gate.py src/gate.rs --aliases py-rust   # bridge __init__→new
ctx parity research/gate.py src/gate.rs --strict            # non-zero if missing

# Coverage report: internal call-graph recall + exactly what went unpinned
ctx doctor
ctx doctor --explain                  # full per-name census

# The modules that matter most (dependency centrality; --churn weights by volatility)
ctx core
ctx core --churn

# Wider views. `subtree` is the one to reach for; `map` is a cold-start or
# human-reader tool, not an agent's opening move — see "Maps, when you want them"
ctx subtree core::inference           # one module + upstream + downstream
ctx modules                           # module list + dep edges only
ctx map --view skeleton               # whole repo, architecture only
ctx map --max-tokens 8000             # hard cap: reduces detail, then prunes
ctx map -o CODEBASE_MAP.md            # write it out (goes stale on the next edit)

# Scoping (global, repeatable)
ctx map --lang code                   # code only, no prose
ctx map --exclude 'docs/archive/**'   # vendored trees, dead code

# Show each edge's call sites in map/subtree/callers (context always shows them)
ctx callers basename --verbose-edges

# Machine-readable
ctx map --format json

# Print the agent instructions block, measured for this repo
ctx snippet >> AGENTS.md              # or CLAUDE.md, .cursorrules, …

Every command except parity takes a repo path as its last positional argument, defaulting to .; parity takes a source and one or more targets instead. See EXAMPLES.md for each of these run against a real r

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#code-graph · #agents · #cli