Skip to main content
Glama

cairn

CI PyPI Python License: Apache-2.0 M8ven Score

A map of all your repos for coding agents. cairn scans every git repo in a folder once, works out how they connect, and gives your agent:

  • a tiny always-loaded index;

  • a short card per repo.

The agent knows where things live without re-exploring.

Cairns are the stacked-stone markers that guide hikers along a trail. cairn leaves small, cheap markers that guide coding agents to the right repo, and then to the right file.

Works with Claude Code, Codex, Gemini CLI, and Cursor, on Windows, macOS, and Linux. Using Claude Code? Install cairn as a plugin, or paste that section into Claude Code and it installs everything for you.


Why

Coding agents work inside one repo. Real work spans many: a web app, an API, a shared types package, a payments service, a reporting job. Say "add a tip amount to trips end to end" and the agent has to:

  1. discover that sibling repos exist;

  2. grep through all of them;

  3. read a pile of files;

  4. guess which service owns the trips table.

That costs tokens and turns, and weaker models guess wrong.

Hand-maintained "related repos" docs help until they go stale. cairn builds that map from the code itself and keeps it fresh:

~/code/                          ~/code/.cairn/
├── rider-web/        cairn      ├── INDEX.md           ← ~25 tokens per repo, always loaded
├── trips-svc/        ─────▶     ├── cards/trips-svc.md ← read on demand (≤ 800 tokens)
├── payments-svc/     scan       ├── workspace.json     ← the full graph
├── analytics-etl/               └── authored/          ← your summaries and decisions
└── …

Related MCP server: koragraph

What your agent sees

The index is a few lines, loaded into every session through CLAUDE.md or an equivalent:

# Workspace repos (cairn)
- admin-console (@fleetline/admin-console): Next.js back-office for ops staff; queries trips… · nextjs
- analytics-etl: Nightly SQL reporting jobs over trips, payments, drivers… · python
- payments-svc (payments): Go service that charges completed trips and records weekly payouts · go
- trips-svc: FastAPI service owning the trip lifecycle and the trips/trip_events tables · fastapi · used by admin-console
- ui-kit (@fleetline/ui-kit): Shared React components (Button, Card) · react · used by admin-console

used by lists the repos that depend on, call, or reference that one, so a change's blast radius is visible before any card is opened.

A card is read only when the agent needs that repo:

# trips-svc
> FastAPI service owning the trip lifecycle and the trips/trip_events tables.
`./trips-svc` · python, fastapi · HEAD 1fee22d

## Relates
← infra: references path trips-svc (extracted · infra/docker-compose.dev.yml:3)
→ admin-console: shares tables trips (inferred · admin-console/lib/db.ts:4)
→ analytics-etl: shares tables trips (inferred · analytics-etl/jobs/daily_revenue.sql:2)
→ payments-svc: shares tables trips (inferred · payments-svc/internal/charge/charge.go:3)

## Run
test `pytest`

## Layout
app/ → routes/pages
migrations/ → DB migrations

Every relationship comes with evidence (file and line) and a trust level:

  • extracted: found directly, such as a package dependency or a path reference;

  • inferred: strong signals, such as a table that only one repo creates;

  • ambiguous: hidden until you confirm it.

Install

You need Python 3.11+ and git.

Tool

Command

uv (recommended)

uv tool install cairnmap

pipx

pipx install cairnmap

run once, no install

uvx --from cairnmap cairn init

pip

pip install cairnmap

The package is called cairnmap; the command is cairn.

As a Claude Code plugin

If you use Claude Code, the plugin is the whole setup: it builds the map when you ask, loads it into every session, and starts cairn's MCP tools. It runs cairn from PyPI through uv.

Type it yourself in Claude Code, then restart Claude Code:

/plugin marketplace add Moe1177/cairn
/plugin install cairn@cairn

Or ask Claude Code to do it. Paste this section, or the link to it, into Claude Code and say "install this". The steps below are written so Claude Code can follow them.

  1. Check that uv is installed: uvx --version. If it isn't found:

    • uv may be installed but not on PATH (pip install --user uv puts it in %APPDATA%\Python\Python3*\Scripts on Windows, ~/Library/Python/3.*/bin on macOS). If it's there, tell the user to add that folder to PATH instead of installing uv again.

    • Otherwise, ask the user before installing uv with its official installer:

      • macOS and Linux: curl -LsSf https://astral.sh/uv/install.sh | sh

      • Windows: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

  2. Add the marketplace and install the plugin:

    claude plugin marketplace add Moe1177/cairn
    claude plugin install cairn@cairn
  3. Check it: claude plugin list shows cairn@cairn as enabled.

  4. If ~/.claude/skills/cairn/SKILL.md exists, cairn was also installed with cairn install claude. Run cairn uninstall claude, or every cairn tool appears twice.

  5. Tell the user to restart Claude Code, then open it inside any repo of their folder of repos. If that folder has no map yet, Claude offers to build one (or say "build the cairn map").

After that, every session starts with the map, and the cairn MCP tools are ready. The plugin never edits CLAUDE.md. If uv goes missing later, the first session says so and Claude offers to install it, only if you agree.

To

Run

Update

claude plugin marketplace update cairn, then claude plugin update cairn@cairn

Remove

claude plugin uninstall cairn@cairn

Codex, Gemini CLI and Cursor use cairn install instead (see Quick start).

Quick start

cd ~/code              # the folder that CONTAINS your repos
cairn init             # scan, then add the index to ./CLAUDE.md (asks first)
cairn install all      # optional: Codex, Gemini CLI, Cursor, the /cairn skill, the MCP server

Open your agent in any repo under that folder. It now knows about every sibling repo.

To give each repo a good one-line summary, run /cairn inside your agent. It writes the summaries with cairn set-summary and settles uncertain links. Until then, the index shows cairn's draft from what it detected, such as draft: web app (104 routes, owns 46 tables) or draft: library (used as a package by 3 repos), and "no summary yet" where it found nothing to go on. A draft is built from counts only: README text is deliberately kept out of always-loaded context.

How to use it

Inside any repo, run cairn links, or ask your agent "which repos does this repo link to?". Both give the same answer:

trips-svc is linked to 3 repos:
  admin-console  ↔ both ways  shares 2 tables, mentions each other in docs
  payments-svc   ← one way    calls this repo (2 routes)
  rider-web      ← one way    calls this repo (3 routes)

→ one way means this repo uses the other one, ← one way that the other one uses this repo, and ↔ both ways that each uses the other or they share something, like a database. Add --evidence for the file and line behind each link. In Claude Code, the plugin's /cairn:links does the same.

Day to day

cairn status               # repos, relationships, unconfirmed links, missing/stale summaries
cairn refresh              # re-read only repos that changed (fast)
cairn hooks install        # optional: refresh automatically after each commit/merge
cairn annotate-edge "web->api:shares_db" --confirm
cairn annotate-edge "shop->blog:shares_db" --reject --why "different databases"
cairn set-summary payments-svc "Charges completed trips and pays drivers weekly." --alias payments

These decisions live in .cairn/authored/ and .cairn/relations.yaml. They survive every re-scan, and you can commit them so your team shares them.

From your agent (MCP)

cairn install <harness> registers cairn's MCP server. Agents can call these tools:

Tool

What it answers

resolve_repo

"Which repo is 'the payments service'?"

repo_card

The card for a repo (re-scanned first if its HEAD moved)

links

Which repos a repo is linked to, one line each with its direction (→ one way, ← one way, ↔ both ways); empty means the repo the session is in

related

Everything connected to a repo, with evidence

find_across

Which repos expose or use a table, package, or path

query

Where inside a repo: file:line hits, grep first, plus a deep index's symbols when built; says which search answered

find_similar

Where the org already implemented a feature (its repos on GitHub, nothing cloned); needs orgs: in the config

read_org_file

A file from an org repo, without cloning it

org_profile

The org's stack: the datastores, frameworks, auth, test and deploy tools and libraries its active repos use, and its own shared packages

org_packages

The org's own libraries its repos share, where each is defined and who uses it; with a name, one package's users and code that calls it

refresh

Update the map now

What cairn detects

Signal

Examples

HTTP calls

Next.js routes, Express/Fastify/Hono, FastAPI/Flask, Go (net/http, chi, gin, echo) and OpenAPI routes, matched with fetch/axios, requests/httpx, and Go http client calls

gRPC

Go, Python, TypeScript and Java servers matched with their client stubs

Service names

Calls addressed to a sibling's service name, as Docker and Kubernetes DNS do: http://catalogue, http://carts:8080/carts, *.svc.cluster.local, Hostname("payment"); Railway's private network (api.railway.internal, also in Caddyfiles, *.conf and env templates) and its reference variables for another service's address (${{api.RAILWAY_PRIVATE_DOMAIN}}); Fly.io apps by their fly.toml app name (orders.internal, iad.orders.internal, orders.flycast, orders.fly.dev)

Pub/sub topics

Kafka, NATS, Redis and RabbitMQ (Spring AMQP) publishers matched with subscribers (trip.completed)

docker-compose

depends_on between services built from (or named after) your repos

Deploy repos

compose, Kubernetes and Helm files that run your repos' images (image: acme/catalogue:1.2); Render Blueprints (render.yaml) whose services deploy from a sibling repo (repo: https://github.com/acme/orders)

Render services

fromService references in a Blueprint link the repo of the service that reads another's address (property: hostport) or env var to the repo of the service it reads, wherever the Blueprint sits

Package dependencies

npm (workspace:*, scoped packages), PyPI, Go modules, Cargo, Maven and Gradle (group:artifact, in any module folder)

Packages inside monorepos

npm/yarn/pnpm, Cargo, go.work and uv workspaces, listed on the card and resolvable by name

Shared database tables

SQL migrations and queries, Prisma, Drizzle, Supabase, MongoDB (Mongoose models, db.collection("x"))

Copies of one app

Repos that share their first commit (an app cloned per event or per client): "change one, check the other". The same package name only suggests it

Path references

../trips-svc in docker-compose, tsconfig, and other config files

Documentation

READMEs and docs that mention a sibling repo

AWS resources

Which repo creates each SQS queue, SNS topic, DynamoDB table, S3 bucket, EventBridge bus, Kinesis stream, Lambda function, SSM parameter and CloudFormation export (Terraform, CloudFormation/SAM, Serverless Framework, CDK), and which repos use it: ARNs, queue URLs, ${ssm:}/${cf:} lookups (an SSM parameter nothing creates links to the repo its path names exactly), Fn::ImportValue, Terraform data blocks, SAM policy templates, SDK calls, env vars like ORDERS_TABLE. EventBridge PutEvents sources are matched with rule patterns

Shared env vars

Specific names read by both sides. These only back up another link; they never make one on their own

Look-alikes are deliberately ignored:

  • health-check routes;

  • calls to other companies' APIs;

  • vague topic names;

  • .proto files with no implementer;

  • generic env vars such as PORT;

  • localhost, public domains, and URLs in comments;

  • public images (mongo:3.4) and look-alike names (catalogue-db is not catalogue);

  • a queue that a repo declares but never consumes;

  • the same schema in two copies of one app (copies often use a database each, so no link);

  • Firestore's db.collection(...), which looks like MongoDB's;

  • the Terraform state bucket every repo's backend names, IAM wildcards (arn:aws:sqs:*:*:*), AWS's own event sources (aws.ec2), and generated deploy output (cdk.out, .serverless).

Evaluation workspaces in the test suite keep every one of these at precision 1.0.

Why not read AWS resource tags?

Tags record who owns a resource in an AWS account. cairn answers a different question, which code depends on which code, and it gets that from the repos alone:

  • Tags only describe the creator. Half of every link is who uses a resource, and the repos that read a queue or a table don't tag it. Those links come from ARNs, SSM paths, exports and SDK calls in the users' code.

  • cairn reads code, not the account. It makes no network calls and needs no credentials. The only tags it could see are in the IaC block that creates the resource, and that block already tells cairn which repo creates it. A tag only adds something when a central infrastructure repo creates resources for app repos; there, each user already links to that infra repo.

  • No tag key means "owning repo". team, app, service and Name mean different things in different places, and tags drift from the code without anything noticing. A link from code evidence says which file and line it came from.

cairn bench-links scores links against real workspaces (One Login's AWS repos among them), so a new signal like tags can be measured before it's trusted.

Finding the place in a repo

The map tells an agent which repo to open; the query MCP tool tells it where inside. Ask "where is login handled?" and it answers with src/auth.py:12 login() — grep: login (definition). How it searches:

  • It runs grep first, through cairn's hardened git grep. It looks for the question's identifiers, routes and quoted messages, and failing those, its words.

  • For "who calls X", it lists the callers before the definition.

  • It also searches repos the question names, and the repos the map relates to the one asked about, unless an identifier, route or message already answered.

  • It never searches files cairn never opens (.env, keys, secrets.yaml…).

  • Every answer says which search answered and why. It needs nothing installed.

Deep indexes are optional, and built per repo with graphify. A code graph names the symbol on grep's hits and answers when grep finds nothing. cairn asks grep first because, on its offline locate benchmark, grep matched or beat the code graph on every kind of question (results).

cairn deep enable                      # installs graphify if needed (asks first), indexes every repo
cairn deep status                      # size, build sha, fresh or stale
cairn refresh                          # also rebuilds stale deep indexes once any exist (--no-deep skips)
cairn deep build trips-svc             # one repo by hand; cairn deep clear trips-svc deletes it

cairn deep enable installs graphify as its own tool (uv tool install graphifyy, else pipx, else pip), so cairn's own environment is left alone; cairn doctor shows how many repos are indexed. You can also install cairn with the extra (pip install 'cairnmap[graphify]'); cairn refresh --deep forces a deep rebuild check even before any index exists.

  • graphify always runs --code-only: no LLM, no network, and an allowlisted environment, so no API key reaches it. It writes only to .cairn/deep/<repo>/, never into the repo.

  • cairn answers queries itself from the saved graph, offline, so serving needs no graphify.

  • The first build is something you ask for (cairn deep enable or cairn deep build): a large repo can take minutes. After that, cairn refresh (and the git hooks' background refresh) keeps indexes current, one build per repo at a time. When an index still falls behind, query and cairn deep status say so; the card's Deeper section flags new commits.

Prior art across your org

Before an agent builds a feature, it should know whether the org already built it somewhere: the team's rate limiter, its way of verifying webhooks, the retry helper everyone uses. In an org of thousands of repos nobody has them all cloned, so cairn asks the code host instead. Nothing is cloned or downloaded beyond the files an agent chooses to read.

Name the org in .cairn/config.yaml:

orgs:
  - org: acme              # github.com; for GitHub Enterprise add `host: git.acme.com`

cairn uses your GitHub login (gh auth login, or GH_TOKEN), so it sees exactly the repos you can. Then an agent calls find_similar with the feature and a few words its code would contain, and read_org_file to read what it finds:

find_similar(task="stop one client from flooding our API",
             terms=["RateLimiter", "X-RateLimit-Remaining", "express-rate-limit"])
Prior art in acme for `RateLimiter`, `X-RateLimit-Remaining`, `express-rate-limit`: 10 files (3 searches, 0.6s).
- gateway/src/middleware/rate-limit.ts  (matched RateLimiter, X-RateLimit-Remaining)
- ...

From a terminal: cairn similar "stop one client from flooding our API" --terms RateLimiter,X-RateLimit-Remaining.

The words matter. A request rarely shares words with the code that answers it ("flooding" never appears in a rate limiter), so the agent, which understands the request, supplies the code's words. Each term is one code search; they run at once. Hits are merged by rank, so a file several searches find rises, and what isn't prior art sinks or goes: vendored copies of libraries (node_modules, site-packages, Lambda layers), docs, notebooks, configs, tests. On ~9,300 repos (aws-samples), a file that already implements the task was in the top five for 17 tasks in 18 on the tasks cairn was tuned on and 10 in 12 on tasks written after it, against 5 in 18 and 2 in 12 for one plain search of three of the task's words, in about 0.6 s a task (results).

Code search allows 10 requests a minute, so find_similar makes at most four (one per term); past the limit it waits, and its answer says how long.

The org's stack

New code should be built with what the org builds with. org_profile (or cairn org-profile) says what that is, read from its 200 most recently pushed repos: their languages, datastores, queues, web frameworks, auth, test and deploy tools, their most used libraries per ecosystem, and the org's own packages that other repos depend on, each with the share of repos that use it:

The stack of aws-samples (github.com): what its 200 most recently pushed repos use (share of those repos), read 2026-10-17.
Languages: Python 40%, TypeScript 26%, JavaScript 8%, ...
Datastores: S3 42%, DynamoDB 27%, OpenSearch/Elasticsearch 10%, PostgreSQL 8%, MySQL 7%, Redis 6%, ...
Auth: Cognito 26%, JWT 14%
Deploy and CI: Docker 54%, AWS CDK 50%, GitHub Actions 43%, Terraform 19%, ...
Python libraries: boto3 42%, strands-agents 24%, pytest 18%, requests 18%, ...

It reads one listing page per 100 repos, each repo's file tree and up to three of its manifests (about 15 s, some 800 API requests), then answers from a cache for a week.

It does not make find_similar find more. Terms written after reading the profile found prior art for 10 of 12 held-out tasks against 11 of 12 for terms written from the task alone (results), so cairn doesn't tell agents to search with it.

The org's own packages

An agent adding logging, retries or a service client should reach for the org's own library, not a public one, and call it the way sibling repos do. org_packages (or cairn org-packages) lists the libraries the org's repos use from one another, most shared first, with where each is defined:

The own packages of hashicorp (github.com): the 226 libraries its 604 active repos use from one another, most used first (by how many repos), read 2026-10-17. ...
- github.com/hashicorp/go-hclog (Go) · 89 repos · in hashicorp/go-hclog — A common logging package for HashiCorp tools
- github.com/hashicorp/vault/api (Go) · 43 repos · in hashicorp/vault/api
- github.com/hashicorp/go-retryablehttp (Go) · 16 repos · in hashicorp/go-retryablehttp — Retryable HTTP client in Go

query narrows the list ("retry http"); a name shows one package: its manifest, every repo that uses it (and which of a monorepo's projects), and up to five files that import it, found with one code search. A package counts as the org's when one active repo defines it and another uses it, or its name is under the org (@acme/..., github.com/acme/...); a fork is its home only under the org's name, and a name several repos define is a template's, not a library.

It reads every active repo's file tree and manifests (hashicorp's 604 repos: 35 s, about 1,300 API requests), then answers from a cache. After a day it reads again, but only the repos pushed since (2.3 s when none were). When the hour's API budget runs low, it stops with 500 requests to spare and the next call reads the rest, most recently pushed first.

Told what to build, an agent that read the list named the right library for every hashicorp task, and for all 14 held-out grafana tasks. With the org's name masked (a private org's packages, which no model has read about), the agent alone named none (results).

Benchmarks

cairn ships a benchmark harness (cairn bench). It runs real tasks through headless Claude Code under five conditions, each in a fresh, isolated copy of a multi-repo workspace:

Condition

A

No map (the agent explores)

B

A hand-written "related repos" doc

C

cairn's index only

D

Index + repo cards

E

D + cairn's MCP server

Tasks cover:

  • orientation: "who owns the trips table?";

  • localization: "which files change to add a tip amount end to end?";

  • cross-repo impact: "what breaks if drivers.license_no is renamed?";

  • control: questions answerable within one repo.

Answers are graded deterministically on the files and facts they must name.

Results (2026-10-06): 840 runs over four workspaces, two of them real open-source systems: Sock Shop (9 microservice repos; cairn's newest link types were developed on it) and the Supabase JS client family (6 repos, held out: never used to tune cairn). 28 tasks, 3 runs per cell, Haiku 4.5 and Sonnet 5.5. A result is called significant only after Holm adjustment.

Per task, cairn INDEX (C)

Haiku 4.5

Sonnet 5.5

Fresh tokens vs no map

-19% (significant)

-10% (n.s.)

Cost vs no map

-25% (n.s. after adjustment)

-12% (n.s.)

Cost vs a hand-written doc

-6% (n.s.)

-12% (significant)

Cost vs no map, Sock Shop only

-27% (n.s.)

-26% (borderline, adjusted p = 0.055)

What this shows:

  • cairn tends to make cross-repo work cheaper: fewer fresh tokens for Haiku, and cheaper than a hand-written related-repos doc for Sonnet. The largest raw savings were on Sock Shop.

  • It doesn't measurably raise success. Sonnet answers 99-100% of these tasks in every condition; Haiku rises from 86% to 93% with the MCP server, which isn't significant.

  • It isn't a win everywhere. On fleetline, Sonnet cost 7-10% more with cairn than without (n.s.); the held-out Supabase effects are small and not significant.

  • Correction: our first, smaller run (2026-10-05) reported Haiku reaching 100% with the INDEX. With more runs that doesn't replicate (89-92%, the same as no map).

  • A bigger workspace isn't enough to make no map hard. A pilot on OpenFaaS (38 real repos, 2026-10-10, Sonnet 5.5, no map, one run per task) answered all ten of its tasks, for $0.07 a task with two or three searches each: every answer holds a literal string one workspace-wide search finds.

  • Neither are hard tasks (2026-10-11 pilot, one run per cell). There are 11 of them, on OpenFaaS and One Login. No search for a task's own words finds its answer, and the prompt doesn't name the repo the answer is in. The agent has to read an identifier in code and follow it to a repo it works out itself.

    • Sonnet answered 11/11 in every condition.

    • Haiku answered 10/11 in every condition.

    • With a map, Sonnet's cost fell 17-19% on OpenFaaS and rose 19-29% on One Login.

    • The one failure a map clearly prevented: Haiku with no map never looked outside the repo it started in.

    • bench/published/2026-10-11-hard-tasks.md has the tasks, the definition and every failure.

Methods, every table with 95% intervals, paired Wilcoxon tests (raw and Holm-adjusted), the regressions, and the held-out results: bench/published/2026-10-06-real-world.md. The first run is kept at 2026-10-05-shopverse-fleetline.md.

Run the benchmarks yourself from a source checkout. They use your Claude usage.

cairn bench bench/suites/sockshop --runs 3 --model haiku    # fetches the pinned repos once
cairn bench bench/suites/shopverse --conditions A,C --tasks gift-message
cairn bench bench/suites/sockshop --resume bench/results/<stamp>.jsonl   # after a usage limit
cairn bench bench/suites/sockshop --harness codex --runs 3      # Codex instead of Claude Code
cairn bench-regrade bench/suites/sockshop bench/results/<stamp>.jsonl   # grade a log again

A task marked hard: true in a suite must be one that no search for the prompt's own words answers, and whose prompt doesn't name the repo the answer is in. cairn bench-reach checks this offline, and fails on a hard task that breaks either rule:

cairn bench-reach bench/suites/openfaas bench/suites/onelogin

With --harness codex, each run is codex exec in a read-only sandbox that ignores your Codex config and rules and turns off every tool beyond reading the workspace (connected apps, plugins, web search, sub-agents). The condition's context reaches it as developer instructions. A global AGENTS.md in your Codex home would load into every run, so the benchmark won't start while one is there.

The locate benchmark is offline and free: no agent runs. Each locator (grep, the code graph, and the hybrid of both) answers every question in a suite's locate.yaml, and is scored on where the answer file lands, how much text it returns and how long it takes.

cairn bench-locate bench/suites/sockshop bench/suites/networkx   # builds code graphs with graphify
cairn bench-locate bench/suites/shopverse --no-build --locators grep

The prior-art benchmark asks the question an agent has when it starts a feature in a large org: does an implementation of this already exist, and where? Each suite's prior_art.yaml names a GitHub org and lists feature tasks with files in it that already implement them, found and read by hand. Every locator answers every task over GitHub's API, with nothing cloned, and is scored on where the first answer file lands, how much text it returns and how long it takes. An org holds more implementations than a key can list, so the report also says what share of each locator's top five the key has judged; an unjudged hit is unknown, not wrong. It needs a GitHub login (gh auth login, or GH_TOKEN), and code search allows 10 requests a minute, so a run waits out the limit as it goes (about ten minutes for both suites). The cairn locator searches with the terms an agent would pass, kept beside each key in agent_terms.yaml: a model wrote them from the task alone (--make-terms), and nobody edits them.

cairn bench-prior-art bench/suites/aws-samples bench/suites/aws-samples-held

The packages benchmark asks whether an agent reaches for the org's own library. Each suite's packages.yaml lists tasks with the org's packages that serve them. A model names the libraries it would use, best first, from the task alone and after reading the list org_packages returned (kept beside the key as org_packages.md, made with --make-list). It does each twice, once with the org renamed, since a public org's libraries are in its training data and a private org's aren't. The answers are kept in agent_picks.yaml (--make-picks), so a run is repeatable and free.

cairn bench-packages bench/suites/hashicorp bench/suites/grafana

The link benchmark is offline and free too. Each suite's links.yaml lists the pairs of repos that really link, written from the code and never from cairn's output; cairn bench-links scans the suite and scores the links cairn shows (and, separately, its unconfirmed suggestions) as pairs of repos: how many real links it finds, how many it finds that aren't real, and whether it points them the right way.

cairn bench-links bench/suites/sockshop bench/suites/supabase-js

Links cairn shows (2026-10-10)

real links

found

not real

precision

recall

Sock Shop (real, 9 repos)

17

15

0

1.00

0.88

Supabase JS (real, held out)

5

5

0

1.00

1.00

fleetline + shopverse (synthetic)

26

26

0

1.00

1.00

GOV.UK One Login (real AWS, 10 repos, held out)

24

12

0

1.00

0.50

OpenFaaS (real, 38 repos)

88

48

4

0.92

0.55

What the misses and wrong links are:

  • Sock Shop: orders reads users and carts through URLs the front-end puts in each order; no hostname for them appears in orders' code.

  • One Login: cairn finds 11 of its 12 AWS links (SQS queues, DynamoDB tables and SSM parameters behind stack parameters, shared Lambdas, KMS keys, exports). It misses the HTTP links (core calling each credential issuer, known from Pact contracts; stubs standing in for services) and one export whose stack name only a deploy script sets.

  • OpenFaaS: it was held out until its first run showed 19 wrong links, all READMEs telling readers to run faas-cli, and missed Go modules outside a repo's root (gateway/go.mod). Both are fixed (it scored 0.70 precision and 0.51 recall before), so it is now tuned on, not held out. Most misses left are images used as Dockerfile base images, HTTP clients of the gateway, and the function store's JSON catalogue.

Unconfirmed suggestions add one or two wrong links in most suites (a table name two repos share, look-alike repos).

Performance

cairn walks each repo once, reads each file once, and scans repos in parallel. A refresh re-reads only repos whose HEAD or working tree changed, and asks git one question per unchanged repo. On a synthetic workspace of 200 repos and 50,000 files (Windows 11, 12 cores):

0.3

0.4

First scan

156 s

53 s

Refresh, nothing changed

15 s

5.4 s

Most of a refresh on Windows is git process start-up; Linux and macOS start processes faster. Reproduce with uv run python bench/perf_scan.py --out <dir>.

Safety and privacy

cairn treats every scanned repo as untrusted input. It:

  • never modifies your repos, apart from the opt-in hooks and Cursor's --per-repo rule (both marked blocks, removed cleanly);

  • never opens .env files, private keys, or credential files, and never reads through a symlink or junction inside a repo;

  • never runs code from a repo: its git calls turn off fsmonitor, hooks, and the repo's own filters.

It also:

  • redacts common secret formats from every stored snippet, and stores git remotes without credentials;

  • keeps README text out of always-loaded context. Repo names that do appear are flattened and capped, so they can't inject instructions or break cairn's blocks.

cairn collects no telemetry, and mapping your folder makes no network calls. It talks to a server only when you ask it to:

  • with orgs: in the config, find_similar, read_org_file, org_profile, org_packages, cairn similar, cairn org-profile and cairn org-packages call your code host's API (GitHub, with your own login: GH_TOKEN, or GH_ENTERPRISE_TOKEN for an Enterprise host, else gh auth token). They send the search terms (the task's own words when no terms are given) and the org's name, and read back matching files; nothing from your local repos is sent. They only ever reach the orgs in your config: a call naming another org, or another owner's repo, is refused, and a search hit from outside the org is dropped;

  • cairn bench-prior-art calls the same API, and with --make-terms runs the claude CLI (with --make-profile, it also reads the org's profile); cairn bench-packages reads the org's packages with --make-list and runs the claude CLI with --make-picks;

  • cairn deep enable runs your package installer (uv, pipx or pip) to add graphify;

  • cairn bench, cairn bench-locate and cairn bench-links fetch their pinned benchmark repos with git, and cairn bench runs the claude CLI.

See SECURITY.md for the threat model and how to report a vulnerability.

Reference

Commands

Command

What it does

cairn init

Scan, then offer to add the index to Claude Code

cairn scan [--full] [--verbose]

Map every repo under the folder into .cairn/

cairn refresh [--no-deep]

Re-read only repos whose HEAD or working tree changed, and rebuild stale deep indexes

cairn deep enable / cairn deep build REPO…|--all|--stale [-w PATH]

Build graphify code indexes so query also names symbols (optional extra)

cairn deep status / cairn deep clear [REPO…]

List deep indexes (fresh or stale) / delete them

cairn status

What cairn knows, unconfirmed links, missing or stale summaries

cairn annotate-edge KEY --confirm|--reject [--why TEXT]

Settle a relationship

cairn set-summary REPO TEXT [--alias NAME]

Save a summary (- reads stdin)

cairn install <claude|codex|gemini|cursor|all>

Load cairn into an agent harness

cairn uninstall <name|all>

Remove it again

cairn hooks install|uninstall

Opt-in git hooks that refresh after commits and merges

cairn serve

The MCP server (harnesses start it for you)

cairn links [REPO]

Which repos a repo is linked to, one line each with its direction; the repo you're in by default (--evidence, --all, --json)

cairn context

What a Claude Code session should know about its folder: the index, or an offer to build a map (the plugin's session hook runs it)

cairn similar TASK --terms A,B

Where your org already implemented a feature, over GitHub's API

cairn org-profile [--refresh]

Your org's stack, read from its recently pushed repos over GitHub's API (cached for a week)

cairn org-packages [NAME] [--query WORDS] [--refresh]

Your org's own packages: where each is defined and which repos use it (cached for a day)

cairn bench SUITE

Run the benchmark harness

cairn bench-locate SUITE...

Score grep, graph and hybrid locate offline

cairn bench-links SUITE...

Score cairn's links against each suite's real ones, offline

cairn bench-prior-art SUITE...

Score finding an org's existing implementation of a feature, over the host's API (no clone)

cairn bench-packages SUITE...

Score whether an agent names the org's own library for a task, from memory and from org_packages

cairn bench-regrade SUITE LOG

Grade a finished run's log again with today's grading

cairn bench-reach SUITE...

Which tasks a search for the prompt's own words answers, offline; fails on a hard task that one does

cairn doctor

Check git, Python, the map, write access, harnesses and graphify; exits 1 on a failure

cairn --install-completion

Tab completion for your shell (bash, zsh, fish, PowerShell)

cairn --version

Versions of cairn, Python, the platform, and mcp

Exit codes: 0 means success, 1 an error (one line on stderr), and 2 a usage error.

What cairn writes, and where

Path

What it is

<folder>/.cairn/INDEX.md

One line per repo: name, aliases, summary, stack

<folder>/.cairn/cards/<repo>.md

One card per repo

<folder>/.cairn/workspace.json

The full graph

<folder>/.cairn/cache/, logs/, .lock

Scan cache, last scan log, lock file

<folder>/.cairn/relations.yaml, authored/

Yours: aliases, manual links, summaries, decisions

<folder>/CLAUDE.md

A marked block holding the index (cairn install claude)

~/.claude/skills/cairn/ and Claude's user MCP config

/cairn skill and MCP server

~/.codex/AGENTS.md, config.toml, skills/cairn/

Codex pointer, MCP server, skill

~/.gemini/GEMINI.md, settings.json, commands/cairn.toml

Gemini CLI pointer, MCP server, command

~/.cursor/mcp.json, commands/cairn.md

Cursor MCP server and command

<repo>/.git/hooks/post-commit, post-merge

Only with cairn hooks install

~/.cairn/registry.json, backups/

Mapped workspaces; one private backup of each config file cairn first edited

cairn only edits its own key or marked block in other tools' files, and refuses to touch a file it can't parse.

Uninstall

cairn uninstall all          # harness entries, skills, commands, Cursor rules
cairn hooks uninstall        # if you installed hooks
uv tool uninstall cairnmap   # or: pipx uninstall cairnmap

Then delete <folder>/.cairn/ and ~/.cairn/.

Troubleshooting

Symptom

Fix

Something seems off

Run cairn doctor: it checks each dependency and says what to fix

"No git repos found under …"

Run cairn from the folder that contains your repos

"… is inside the git repository …"

Same: run from the parent folder, not inside a repo

"git not found on PATH" warning

Install git; without it, remotes, HEAD and caching are off

"another cairn process is still updating this workspace"

A scan or hook refresh is running; retry in a moment

An agent says it can't find the workspace

Run cairn init in the workspace folder, then restart the agent session

cairn install codex/gemini/cursor refuses a config

That file isn't valid TOML/JSON; fix it and re-run (cairn never guesses)

Hooks did nothing

cairn hooks install lists the repos it skipped and why

Roadmap

  1. ✅ Core map, precision pass, MCP server, harness integrations, freshness, benchmarks, release hardening (0.1).

  2. ✅ HTTP, gRPC, pub/sub, compose and env-var relationships; packages inside monorepos (0.2).

  3. ✅ Deep per-repo queries via graphify (0.3).

  4. ✅ Efficiency: 3x faster scans, faster and more reliable CI, cairn doctor, shell completion, and "used by" on INDEX lines (0.4).

  5. ✅ Real-world reach (service DNS, deploy repos, RabbitMQ) and benchmarks on real open-source workspaces with significance testing (0.5).

  6. ✅ Deploy config: which repo creates each AWS resource and which use it (Terraform, CloudFormation/SAM, Serverless Framework, CDK, Lambda, SSM), and Railway, Fly.io and Render services (0.8).

  7. ✅ Codex in the benchmark, Maven and Gradle, real-world link benchmarks, draft summaries.

  8. ✅ Harder benchmark tasks: 11 tasks that no search for their own words answers, checked by cairn bench-reach. Without a map, Sonnet still answered all of them.

  9. ✅ Prior art across an org too big to clone: find_similar and read_org_file over GitHub's API, measured by cairn bench-prior-art on ~9,300 repos.

  10. ✅ The org's stack: org_profile lists what the org's active repos build with, in about 15 s and from a cache after. Writing search terms from it didn't find more prior art, and the benchmark says so.

  11. ✅ The org's own packages across repos nobody cloned: org_packages says where each is defined, which repos use it and which files call it, measured by cairn bench-packages.

  12. Benchmark sessions that start with only their own repo in view, the way a session opened in one repo does.

Contributing

Issues and pull requests are welcome. See CONTRIBUTING.md for setup and checks, and the changelog for what's new. By participating you agree to the code of conduct.

License

cairn is licensed under the Apache License 2.0: use it, modify it, and ship it, commercially too. Keep the license and the NOTICE file with any redistribution. Full text: LICENSE.

Runtime dependencies and their licenses:

Package

License

typer

MIT

pydantic

MIT

PyYAML

MIT

pathspec

MPL-2.0 (used unmodified as a library)

mcp

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Enables AI coding agents to query a semantic cross-repository code graph for symbols, references, callers, dependencies, and change impact across registered repositories.
    12
    23
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables coding agents to query a local, multi-repo code graph for symbol exploration, blast radius analysis, co-change mining, and durable code-anchored memory via MCP tools.
    69 npm
    14
    Business Source 1.1
  • A
    license
    A
    quality
    A
    maintenance
    Enables coding agents to get a compact, token-budgeted map of any repository in one call, including file tree, language stats, entry points, and key symbols, so they read only what they need.
    4
    1
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Provides local-first repository architecture analysis with file-level proof, enabling agents to map codebases, locate implementations, trace call paths, and assess change impact with deterministic evidence.
    4 npm
    7
    MIT