cairn
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.
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:
- discover that sibling repos exist;
- grep through all of them;
- read a pile of files;
- guess which service owns the
tripstable.
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
└── …
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.
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 says "no summary yet"
for that repo. README text is deliberately kept out of always-loaded context.
How to use it
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
Settle uncertain links once
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) |
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: symbol — file:line from a deep index, else which folders to start in |
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") |
| 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) |
| Package dependencies | npm (workspace:*, scoped packages), PyPI, Go modules, Cargo |
| 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 |
| Path references | ../trips-svc in docker-compose, tsconfig, and other config files |
| Documentation | READMEs and docs that mention a sibling repo |
| 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;
.protofiles 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-dbis notcatalogue); - a queue that a repo declares but never consumes.
Evaluation workspaces in the test suite keep every one of these at precision 1.0.
Deep queries
The map tells an agent which repo to open. A deep index tells it where inside: the query MCP
tool answers "where is login handled?" with login() — src/auth.py:12 hits and their neighbours,
instead of a list of folders.
Deep indexes are optional and built per repo with graphify:
uv tool install 'cairnmap[graphify]' # or: pip install 'cairnmap[graphify]'
cairn deep build trips-svc # one repo (or --all)
cairn deep status # size, build sha, fresh or stale
cairn refresh --deep # after changes: rebuild only the stale indexes
cairn deep clear trips-svc # delete it
- 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.
- Building is never automatic (the first build of a large repo can take minutes). When an index
falls behind the repo (a new commit or an uncommitted edit),
queryandcairn deep statussay so; the card's Deeper section flags new commits.
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_nois 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).
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
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-reporule (both marked blocks, removed cleanly); - never opens
.envfiles, 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 makes no network calls and collects no telemetry. cairn bench is the only feature that
runs another program that does (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 [--deep] |
Re-read only repos whose HEAD or working tree changed (--deep: also rebuild stale deep indexes) |
cairn deep build REPO…|--all|--stale [-w PATH] |
Build graphify code indexes so query answers with file:line (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 bench SUITE |
Run the benchmark harness |
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
- ✅ Core map, precision pass, MCP server, harness integrations, freshness, benchmarks, release hardening (0.1).
- ✅ HTTP, gRPC, pub/sub, compose and env-var relationships; packages inside monorepos (0.2).
- ✅ Deep per-repo queries via graphify (0.3).
- ✅ Efficiency: 3x faster scans, faster and more reliable CI,
cairn doctor, shell completion, and "used by" on INDEX lines (0.4). - ✅ Real-world reach (service DNS, deploy repos, RabbitMQ) and benchmarks on real open-source workspaces with significance testing (0.5).
- More harnesses in the benchmark (Codex), and more ecosystems.
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 |
Metadata
Release files for cairnmap 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cairnmap-0.5.0.tar.gz | 122.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cairnmap-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 279.0 kB
Release files / cairnmap-0.5.0.tar.gz
| Download URL | cairnmap-0.5.0.tar.gz |
|---|---|
| Size | 122.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
54f01f4a15fae71b029850ed53db3ccc38e2f6a84b5bf7bfd9c274d77f509745
|
|
BLAKE2b-256 checksum How to use checksums |
af8ab1ab46e93c04ba5aec09d8f6713816c6f968d5750cdf8760daa88b471fa6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / cairnmap-0.5.0-py3-none-any.whl
| Download URL | cairnmap-0.5.0-py3-none-any.whl |
|---|---|
| Size | 156.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6daa17a75f64f65357fdeea1786644de09bfd5aea0901d2e560ca8af260e4b9e
|
|
BLAKE2b-256 checksum How to use checksums |
92e5cc892bb2a1f30e5bcd20439984d981b8b1cafd169ab6eeeaf262f935f409
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency log