- Rust 97.3%
- Shell 1.3%
- Python 0.7%
- WebAssembly 0.3%
- PowerShell 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
CI / cargo doc (intra-doc links) (push) Has been skipped
CI / Grammar rebuild from source (nightly) (push) Has been skipped
CI / OSS corpus tier-3 scale (nightly) (push) Has been skipped
CI / cargo test (push) Has been skipped
Package CI / package-corpus (svelte) (push) Successful in 2s
CI / cargo test (daemon transport) (push) Has been skipped
CI / Plugin path cost + pool throughput (nightly) (push) Has been skipped
Package CI / package-corpus (ruby) (push) Successful in 2s
CI / Report scheduled failures (push) Has been skipped
Package CI / package-corpus (python) (push) Successful in 3s
Package CI / package-corpus (rust) (push) Successful in 3s
Package CI / package-corpus (xaml) (push) Successful in 1s
Package CI / package-corpus (vue) (push) Successful in 3s
Package CI / package overhead budgets (nightly, one package at a time) (push) Has been skipped
Package CI / package-corpus (typescript) (push) Successful in 3s
Package CI / package-corpus (timeline) (push) Successful in 3s
Package CI / Report scheduled failures (push) Has been skipped
Pilot plugin compatibility / Fetch the pinned runtime once (push) Successful in 17s
Pilot plugin compatibility / Pilot compatibility (csharp) (push) Successful in 9s
Pilot plugin compatibility / Pilot compatibility (timeline) (push) Successful in 11s
Pilot plugin compatibility / Pilot compatibility result (push) Successful in 1s
Release Build / Generate Version (push) Successful in 7s
Release Build / Required CI green (push) Successful in 29s
Release Build / Build linux-aarch64 (push) Successful in 11m42s
Release Build / Build windows-x86_64 (push) Successful in 26m33s
Release Build / Build linux-x86_64-musl (push) Successful in 16m36s
Release Build / Build linux-x86_64 (push) Successful in 17m39s
Release Build / Assemble the release archives (push) Successful in 41s
Release Build / Windows archive smoke (msvc) (push) Successful in 54s
Release Build / Seed the package channel (push) Successful in 12s
Release Build / Create Forgejo Release (push) Successful in 26s
Pilot plugin compatibility / Pilot scope (push) Successful in 12s
CI (Windows) / changes (what this run must test) (push) Successful in 17s
CI (Windows) / fmt + clippy + build + test (windows) (push) Has been skipped
CI / changes (what this run must test) (push) Successful in 18s
Package CI / package suites (clippy) (push) Has been skipped
Package CI / changes (which packages this run must test) (push) Successful in 18s
Package CI / package (csharp) (push) Successful in 1s
Package CI / package (php) (push) Successful in 2s
Package CI / package (svelte) (push) Successful in 1s
Package CI / package (rust) (push) Successful in 1s
Package CI / package (javascript) (push) Successful in 3s
Package CI / package (ruby) (push) Successful in 4s
Package CI / package (python) (push) Successful in 3s
Package CI / package (timeline) (push) Successful in 2s
Package CI / package (vue) (push) Successful in 2s
Package CI / package (typescript) (push) Successful in 2s
Package CI / package (xaml) (push) Successful in 3s
CI / cargo fmt (push) Successful in 26s
CI / cargo clippy (push) Has been skipped
Package CI / package-corpus (php) (push) Successful in 2s
CI / cargo check (MSRV 1.99.1) (push) Has been skipped
CI / cargo check (windows-gnu) (push) Has been skipped
CI / cargo test (abi, 32-bit + wasm32) (push) Has been skipped
CI / cargo deny (push) Has been skipped
CI / OSS corpus (tier 1) (push) Has been skipped
Package CI / package-corpus (javascript) (push) Successful in 3s
Package CI / package-corpus (csharp) (push) Successful in 4s
CI / CI lane wall-clock headroom (push) Has been skipped
CI / guest crates (fmt, clippy, doc) (push) Has been skipped
Fixes #584, #585 and #586. Qualification and the four CI rounds are in the pull request description. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014XLNPso9cCvzGq5Kt72PSr |
||
| .cargo | ||
| .config | ||
| .forgejo | ||
| _prdoc | ||
| crates | ||
| distribution | ||
| docs | ||
| fuzz | ||
| tests | ||
| vendor | ||
| .gitattributes | ||
| .gitignore | ||
| .mcp.json.example | ||
| AGENTS.md | ||
| ARCHITECTURE.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
| deny.toml | ||
| install.ps1 | ||
| install.sh | ||
| README.md | ||
| rust-toolchain.toml | ||
code-index
Fast structural code index for AI coding agents, exposed over MCP.
See the MCP argument reference for shared options and errors.
See the configuration reference for .code-index.toml and environment variables.
Language packages cover Rust, Python, TypeScript / JavaScript, C#, PHP, Ruby and additional languages. Live updates via filesystem watcher. Symbols, references, imports, full-text search. Local SQLite, no cloud, no telemetry, no account. The daemon is a local process on a token-authenticated loopback socket, not a network service.
Quick start
Install, and update, with one command
curl --proto '=https' --tlsv1.2 -sSfL https://git.h-dv.de/h-dv/code-index/raw/branch/master/install.sh | sh
The same command installs and updates: it asks the release API for the
latest tag, and if that version is already installed it says so and
exits without downloading anything. There is no separate self-update
subcommand, deliberately — a self-update cannot install the first copy,
so it can only ever be half a mechanism, and the half that rots.
sh install.sh --check # installed vs published; writes nothing
sh install.sh --tag v0.27.1 # a specific release, including an older one
sh install.sh --prefix "$HOME/.local" # default: /usr/local if writable, else ~/.local
sh install.sh --with-plugin de.h-dv.xaml # also put this package in the user's
# store; it GRANTS NOTHING
--check (PowerShell: -Check) also reports the path and version of a
code-index found on PATH outside the selected prefix.
--with-plugin <id> installs a package and grants it nothing. It
resolves the id through the distribution catalog and writes the bytes
into the user-scoped package store — which runs no code: an installed
package extracts nothing until a project approves it, and approving is
the separate step that names the capabilities. That step is
code-index plugin add <id>, run by you, from inside the project,
and the installer prints the line rather than running it. It used to run
plugin add --grant requested --yes, which made the FIRST capability
grant on a fresh machine non-interactively, inside a curl | sh, for
whichever project the working directory happened to detect. The first
grant through this installer path stays a human decision. If the package install
fails the script stops and names the state you are in: the four binaries
ARE installed and answering, the failure is about the plugin only, and
nothing was granted to any project.
It refuses rather than guesses, and each refusal is a hazard that has a name:
- It never installs bytes it has not verified. The
.sha256sidecar published beside every archive is downloaded and compared. If neithersha256sumnorshasumis onPATHit refuses instead of skipping the check — a verification that silently does not run is worse than none, because it reads as having run. - A partial archive installs nothing, and neither does a partial write. The four binaries are staged in a directory inside the target, verified there, and only then renamed into place — so a failure part way through cannot leave two new binaries beside two old ones. That mixture is a state nothing tests, and the build-skew disclosure would then be describing a set rather than a build.
- A release that cannot run here is refused before anything is
replaced. The staged
code-indexis asked its own version first, so a wrong-libc or mis-rolled archive leaves your existing install untouched instead of bricking it with no way back. - An archive member that is a symlink is refused. A symlink resolves
against your machine, so it would install local bytes under one of
our names — and the default prefix is
/usr/local. - macOS is refused by name, citing
#59, because no macOS
archive is published and a Linux binary placed on a Mac fails later
and less clearly. An unrecognised
unameprints what it saw. - Windows is handed over, not half-served. Windows ships a
.zip, not a.tar.gz; this script says so and points atinstall.ps1, which does the same job natively — see Windows. - It never signals a daemon. If it finds a daemon lockfile it says an
older build may still be answering and names
code-index doctor.
crates/cli/tests/installer_e2e.rs grades every one of those refusals
against a real archive, and asserts in each case that nothing was
installed — not merely that the exit code was non-zero. Which
platforms the script must handle is not a list anyone maintains:
crates/cli/tests/installer_targets.rs derives the published set from
the release workflow's own matrices and fails if an archive is neither
installable nor waived by name.
Anything else you would rather do by hand: grab an archive from
git.h-dv.de/h-dv/code-index/releases
(linux-x86_64, linux-x86_64-musl, linux-aarch64, windows-x86_64; bundles
the four binaries plus a per-platform README and
.code-index.toml.example) and verify it against the .sha256 beside
it.
Windows: one command, and the same command updates
install.ps1 is a first-class installer and updater for Windows, and
like install.sh it is one script for both directions — the install path
and the update path are the same code, so an update cannot rot while
installs keep working.
irm https://git.h-dv.de/h-dv/code-index/raw/branch/master/install.ps1 | iex
That form takes no arguments. iex runs the downloaded text, and
there is nowhere in that pipeline to put a -Tag — anything after iex
is an argument to iex, not to the script. So for anything with flags,
download it and run it:
irm https://git.h-dv.de/h-dv/code-index/raw/branch/master/install.ps1 -OutFile install.ps1
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Check
.\install.ps1 -Check # installed vs published; writes NOTHING
.\install.ps1 -Tag v0.27.1 # a specific release, including an older one
.\install.ps1 -Prefix D:\tools\ci # default: $env:LOCALAPPDATA\Programs\code-index
.\install.ps1 -BaseUrl D:\offline\assets # a directory works, which is what makes an
# offline install possible
.\install.ps1 -WithPlugin de.h-dv.xaml # also put this package in the user's
# store; it GRANTS NOTHING
.\install.ps1 -AddToPath # opt in to a USER PATH edit
.\install.ps1 -Help # the full list
The four .exe files go directly in the prefix — %LOCALAPPDATA%\Programs\code-index\code-index.exe,
not ...\bin\code-index.exe. That is where this deliberately differs from
install.sh: a bin\ level is a POSIX shape, and Windows software does
not install that way.
-AddToPath is opt-in, and the default is to print the line rather than
run it. The default prefix is not on anybody's PATH, so the installer
knows perfectly well that you will want it there — and editing your PATH
is still a mutation you did not ask for. Without the flag your environment
is not touched; with it the edit is to the user environment only, never
the machine one.
-WithPlugin <id> installs a package and grants it nothing, exactly as
on POSIX: the bytes go into the user-scoped package store, which runs no
code, and code-index plugin add <id> — the step that names the
capabilities — is printed for you to run from inside the project. The
first grant through this installer path stays a human decision. If the package install
fails, the script stops and names the state you are in: the four binaries
ARE installed and answering, and nothing was granted to any project.
It refuses rather than guesses, and each refusal is a hazard that has a name:
- It never installs bytes it has not verified. The
.sha256sidecar published beside the archive is downloaded and compared. If neitherGet-FileHashnorSystem.Security.Cryptography.SHA256can be reached it refuses instead of skipping the check — a verification that silently does not run is worse than none, because it reads as having run. - A hostile archive member refuses the whole archive, before a byte is
written.
Expand-Archivedoes not protect you from one. So every entry is inspected first: an absolute name, a..segment, a backslash, a name that escapes the destination once normalised, a symlink, a reparse point, or anything that is not a plain file or directory, and the install stops with nothing unpacked. - A partial set installs nothing. All four binaries are staged inside the install directory, verified there, and only then moved into place. A failure part way through rolls back, so a truncated archive cannot leave two new binaries beside two old ones.
- A release that cannot run here is refused before anything is replaced.
The staged
code-index.exeis asked its own--versionfirst, so a mis-rolled or wrong-architecture archive leaves your existing install whole. - It never signals a daemon. If it finds one it says so and names the command; it does not kill a process it did not start.
The running-daemon case, which is a Windows problem and not a POSIX
one. Windows will not open a running image for writing, so an update
while the daemon is alive would fail with access denied half way through.
A running .exe can be renamed, though — so each target is renamed
aside to <name>.old-<stamp> and the new file is moved into the name it
vacated, and if any of the four fails the whole set is rolled back.
Deleting an .old- file then fails for as long as the old process lives,
which is fine and expected: those files are swept by the next run. You may
see them beside the binaries in the meantime.
Supported on Windows also means what it meant before: windows-x86_64 is
built for every release, the whole suite is fmt/clippy/build/tested
natively on a Windows runner, and since 2026-08-11 that job gates
releases — a red Windows leg blocks publishing rather than being noted
afterwards. The cfg(windows) code paths (payload-DACL hardening, the
child-process e2e tests) run there and nowhere else.
install.sh is still a POSIX shell script and still refuses by name
under MinGW, MSYS, Cygwin or with Windows_NT in the environment, rather
than half-unpacking a .tar.gz that was never published for you — and
that refusal now prints the install.ps1 command instead of a manual
download. install.ps1 refuses the mirror case — pwsh on Linux or
macOS — by name, and points back at install.sh.
There is still no winget package and no MSI.
The archive by hand is the fallback, not the route. If you would rather not run a script at all:
$Tag = "v0.41.53"
$Base = "https://git.h-dv.de/h-dv/code-index/releases/download/$Tag"
Invoke-WebRequest "$Base/code-index-$Tag-windows-x86_64.zip" -OutFile ci.zip
Invoke-WebRequest "$Base/code-index-$Tag-windows-x86_64.zip.sha256" -OutFile ci.zip.sha256
# VERIFY BEFORE UNPACKING. The sidecar is `<hash> <filename>`; compare the hash.
(Get-FileHash ci.zip -Algorithm SHA256).Hash.ToLower()
Get-Content ci.zip.sha256
Expand-Archive ci.zip -DestinationPath "$env:LOCALAPPDATA\Programs"
# then put the extracted folder on PATH
The archive contains the four .exe binaries, a per-platform README.md,
VERSION.txt and .code-index.toml.example. Keep all four binaries
together — see Binaries. Note that Expand-Archive there is
doing the unpacking unguarded, which is the check install.ps1 adds and
the reason to prefer it.
For automation, new releases attach install.sh, install.ps1 and a
.sha256 for each to the release itself. Pin both the script URL and its
tag argument; pinning the URL alone still asks the script to install the
latest binaries:
TAG=v0.41.53
curl -sSfLO "https://git.h-dv.de/h-dv/code-index/releases/download/$TAG/install.sh"
curl -sSfLO "https://git.h-dv.de/h-dv/code-index/releases/download/$TAG/install.sh.sha256"
sha256sum -c install.sh.sha256 && sh install.sh --tag "$TAG"
$TAG = "v0.41.53"
$Base = "https://git.h-dv.de/h-dv/code-index/releases/download/$TAG"
Invoke-WebRequest "$Base/install.ps1" -OutFile install.ps1
Invoke-WebRequest "$Base/install.ps1.sha256" -OutFile install.ps1.sha256
# the sidecar is `<hash> install.ps1`; compare before running the script
(Get-FileHash install.ps1 -Algorithm SHA256).Hash.ToLower()
(Get-Content install.ps1.sha256).Split()[0]
powershell -ExecutionPolicy Bypass -File .\install.ps1 -Tag $TAG
Release-pinned install.sh assets are available starting with v0.28.1;
install.ps1 is attached from the first release published after it was
added. Older releases use the source URL above — and for a release older
than that, the archive-by-hand recipe above is the only path.
Or build from source
# build
cargo build --release
# initialise a project (one-off)
./target/release/code-index init
./target/release/code-index index
# health check
./target/release/code-index doctor
The source build does not assemble the signed package bundle carried by release
archives. Automatic first-party maintenance can fetch compatible packages when
the project is indexed or opened; see automatic maintenance
for its policy and opt-outs. With maintenance disabled, install and approve the
needed packages explicitly. The default Cargo features include only Ruby's
native fallback; --features builtins enables the native test oracles.
macOS: no prebuilt archive, and no macOS job in CI. macOS release
builds are deliberately deferred
(#59); no date is
promised. The source carries macOS-only code paths — the FSEvents
watcher subtree subscription, the daemon's stale-PID detection and its
flock handling — and nothing in CI compiles or runs them. A
check-only gate is not a cheap substitute: cargo check --target x86_64-apple-darwin still runs build scripts, and the tree-sitter
grammars compile C through cc, which fails on this image with
cc: error: unrecognized command-line option '-arch'. So building
from source on macOS is not something we have verified; treat it as
unsupported rather than untested-but-probably-working.
Wire into Claude Code (or any MCP client):
// claude_desktop_config.json or equivalent
{
"mcpServers": {
"code-index": {
"command": "/path/to/code-index-mcp",
"args": ["--root", "/path/to/your/project"]
}
}
}
That's it. The MCP server auto-spawns the daemon on first connect and
initialize returns in milliseconds (see I015) regardless of project
size; the daemon runs its initial reconciliation in the background and
lives independently of your editor session until 30 minutes of idle
(configurable), shutdown or a schema change. The MCP tools, resources, and prompts are
available immediately — calls in the cold-start window either succeed,
return state: "reconciling", or return a structured index_incomplete
error; follow its retryable flag and remedy.
If startup reconciliation fails, the RPC daemon stays alive and serves
the last durable snapshot instead of entering an index/fail/respawn loop.
project_overview reports state: "degraded" with state_detail, and
the daemon retries in place with an exponential cooldown that is never
shorter than the failed attempt itself. The state clears after recovery.
Upgrading to 0.32
- Run
code-index indexonce in each project after upgrading. This release changes what the extractor records, so the first reconcile reparses and re-resolves every code file: about a minute on a django-sized repository, roughly the cost of a cold index. An MCP session's daemon does this on its own and answers withstate: "reconciling"meanwhile. A one-shotcode-index querywith no daemon running (a hook, a pre-commit check, CI) waits up to 10 s for it; if that is not enough it starts the code-index daemon to finish it and answersindex_incompletewith areconcile_pendingblock. Readretryableandreconcile_pending.converges_on_retry: a durable resolution queue or serving daemon can make retries converge, including with--no-daemon, which never starts a daemon. Otherwise follow the reported remedy. Onecode-index indexsettles it up front instead. If a daemon is already serving the project (an MCP session is open),code-index indexdoes not index a second time: it names the daemon's pid, says how to follow its progress (code-index query project_overview '{}', orcode-index doctor), and exits 0. The daemon is already doing the reconcile. - pytest fixture parameters bind the fixture they request. A
parameter of a
test_*function, of atest_*method of aTest*class, or of a fixture binds the@pytest.fixtureof that name, found the way pytest finds it: the test's class, then its module, then the nearestconftest.pyupward.name=aliases are honoured and a fixture never resolves to itself. Nothing is bound for two fixtures of one name at the deciding level, for a name@pytest.mark.parametrizesupplies, or for a call of the parameter (that calls the fixture's value).
Upgrading to 0.13
- Gitignored files are no longer indexed by the live watcher. It now applies the same ignore-FILE stack the cold walk does, so the two agree. If you were relying on a gitignored path being searchable, it was only ever searchable until the next re-walk deleted it.
- On Windows,
index.dband its sidecars are now owner-only. They previously inherited the parent directory's ACLs, whiledaemon.toml— which holds only the token guarding access to that index — did not. See the note below on what the index actually holds. WatchOptsgainsignore_change_rewalk;WatcherStatswas removed in 0.12.
Upgrading to 0.12
Four changes you will notice:
- A nested git checkout is no longer indexed as part of the project.
A subdirectory carrying its own
.gitor.jj— a submodule, a linked worktree, a nested clone — belongs to another project at another commit, and indexing it here duplicated every symbol into these tables. If you relied on that, index it as its own project withcode-index link add <name> <path>, which also gives it correct resolution. The walker logs each one it prunes. - Excluded hidden paths are purged from existing indexes at the next reconcile.
The live watcher used to index what the cold walk excluded, so
.env,.claude/,.vscode/and.gitinternals could end up in the index — contents included, retrievable throughsearch_text. Those rows go away on upgrade. Nothing you index today is lost: the cold walk never yielded them. CI directories.github,.gitlaband.forgejoremain eligible; hidden children inside them are still excluded. project_overviewcan reportstate: "resyncing". A full re-walk is in progress. Results stay readable and every tool still answers; freshness is unverified until it clears, which it does on its own.WatcherStatsis removed fromcode-index-indexer's public API. Nothing ever populated it.
A branch switch now heals in seconds instead of waiting out the periodic reconcile, and on Windows a large event burst — where the OS gives no overflow signal at all — triggers recovery once the filesystem goes quiet.
The live watcher now applies the ignore-FILE stack too (.gitignore,
.ignore, .git/info/exclude, .code-index-ignore, and your global
core.excludesFile), so it agrees with a cold index rather than
indexing gitignored files and deleting them again on the next re-walk.
Editing a .gitignore while the daemon runs is honoured immediately.
The index holds what your working tree holds
.code-index/index.db is a structural copy of every file the walker
indexes, plus their text. Treat it with the same care as the source
tree itself — it is not a filtered or sanitised view, and no exclusion
rule makes it one. What the exclusion rules give you is AGREEMENT
between a cold index and a live one, not a security boundary.
So the index is protected at the level source code is: the state
directory is created 0700, index.db is pre-created 0600 before
SQLite opens it (a chmod afterwards would leave a window), the WAL and
shm sidecars inherit that, and on Windows all three get a protected
owner-only DACL — the same hardening daemon.toml gets, because
protecting the capability token but not the index it guards is not a
threat model. .code-index/ is added to .git/info/exclude so the
index never follows your source into a commit.
Project-scoped .mcp.json: gotcha
If you drop a per-project .mcp.json (instead of the user-scope
claude_desktop_config.json shown above), Claude Code will not
register the server retroactively in an already-running session. Symptom:
code-index tools don't appear in the agent's tool list, and the agent
silently does without them. Two paths out:
- Restart Claude Code in the project dir and approve the prompt. When a
project-scoped
.mcp.jsonis detected for the first time, Claude Code asks the user to approve each declared server. Decline once and it stays declined. - Or pin code-index at user scope so it's available everywhere
without per-project approval:
claude mcp add code-index code-index-mcp claude mcp list # verify
code-index doctor includes an mcp registration check that surfaces
the most common cause ("project hasn't been opened in Claude Code yet")
with a pointer to claude mcp list.
What the agent gets
- Handles, not blobs. Symbol responses carry IDs, locations, and
signatures — never embedded source. The agent calls
read_codeonly when it actually needs bytes. - Paginated, token-bounded responses. Lists return
{results, total, next_cursor};read_codeis hard-capped at 4000 tokens with atruncated: trueflag on overflow. - Structured errors.
{error, query?, did_you_mean[], hint?}— not free-text strings. - Live freshness. Edits, creates, renames, and deletes in your editor flow into the DB within a few hundred milliseconds. Atomic saves are coalesced, and references re-resolve per change batch — cheaply, thanks to an outline-hash firewall that skips the global pass when only function bodies changed.
- Two denominators, never one. Symbol rows carry
ref_count(references that RESOLVED) andname_fallback_count(same-name references that resolved to nothing). The second is an upper bound on how much the first undercounts — not more references: a common method name is dominated by unrelated same-name calls, nearly all of them stdlib. Both numbers ship in the row, so ask your own repository rather than reading a figure from here;project_overview'scount_basismeasures which zeros are uninformative on it.0is the informative case: nothing unresolved could be this symbol, soref_countis tight for the shapes that counter can see (it cannot see alias-qualified calls, Rust/PHP fields, or Rust consts — so 0 is never proof of "unused"). A blast radius you can trust is worth more than a number that flatters. - Coverage you can ask about.
index_coverage(path)answers "is this indexed, and if not, why not?" withindexed/pending/never— andneveronly ever with proof, naming the rule that fired (hidden,skip_dir,ignore_file,too_large, …). No static documentation can answer this, because half the rule is your own.gitignore:deploy/dev.envis indexed in one repo and excluded in the next. - An inventory, not just a search.
list_files(path_glob)answers "what is HERE" — path,lang, line and byte counts measured from disk, symbol count from the index, and per row whether the file is parsed for symbols or reachable only as text. Beside it,not_indexednames the files in that directory the index cannot see at all, with the rule that hid each one.read_codetakes a bare path too, so reading a file you have not outlined yet is one call and a truncated read reportstotal_linesrather than a silent prefix. - Precise-enough references. Resolution is three-tiered: symbol
visibility (recorded by the language extractors) gates candidates,
cross-file bindings additionally require reachability (same dir,
or an import naming the candidate's file, identifier, or container
type — so a lone exported
getno longer swallows every stdlib.get()call), same-file definitions win over same-name twins elsewhere, and the ref's own imports break remaining ties; unqualified heuristic shortcuts never apply to qualified calls, soVec::new()-style externals stay honestly unresolved. The rate is a property of your repository, not a headline number:project_overviewreportsrefsandrefs_resolvedas counts,resolutionas the per-language percentage, andresolution_by_kindas the split that says which shapes the rate is coming from. response_format: "concise"on tools that support it cuts rows to handles + signatures (roughly half the tokens) when scanning many results.find_referencesandfind_callerstakeexclude_tests: trueto hide test-file references;excluded_test_refsthen reports the hidden count split intoresolvedvsname_fallback, because the filtered mode is the one where you can no longer readresolutionper row. Both tools carrytotal_resolvedbesidetotal, and everyconfidenceblock carriespage_rowsso apage_resolved: 0can be told apart from an empty page.- Fewer calls, fewer re-reads.
read_codetakes a LIST of targets (up to 8, onebatchesentry each, sharing an 8,000-token content budget that names any entry it could not read), assearch_textandsearch_symbolstake a list of queries.search_text'smax_lines_per_file(default 20, hard cap 1,000) lists every matching line of a file for a rename check;lines_wideningstates the cap that applied and names any file it could not widen.find_callerson a type answerstarget_is_type— call edges attach to its members — with the members that do have callers. envelope: "minimal", on any tool, opt-in. Drops thesemanticsprose and the per-callanswer_provenance/index_snapshotblocks and keeps every disclosure FIELD — counts,*_unmeasuredmarkers, verdicts, refusals, freshnessstate/incomplete, and a skeweddaemon_build, hoisted. The reply says what it omitted in oneenvelopefield. The default reply is unchanged. Measured on a 14-call mix over this repository it saved 18.3% of the characters (index_coverage68%,project_overview23%, a longfile_outlineunder 3%);code-index://docs/envelopehas the table.
Agent skill
The agent skill is served over MCP as skill://code-index/SKILL.md,
with skills/list and skills/get for discovery and retrieval.
The published manifest's SHA-256 and byte size let clients verify the document.
Clients which do not read MCP resources can save that resource's text themselves.
Orientation and diff awareness
Two tools answer the questions agents ask at the start and end of a task:
// "Where do I even look?" — token-budgeted architectural map, ranked
// by personalized PageRank over the reference graph. Pass `focus`
// to pull a task-relevant neighborhood to the top.
repo_map { "token_budget": 1500, "focus": ["resolve_ref_targets"] }
// "What did I just touch, and what depends on it?" — symbols
// intersecting your git diff, with per-symbol direct_callers.
// Replaces the git-diff + N-greps ritual before a commit.
changed_symbols { "base": "HEAD", "staged": false }
In watcher-backed sessions, changed_symbols and review_diff wait
briefly for changed source files whose index row is behind disk. If
reconciliation does not finish inside the bounded wait they return
index_updating; they never return shifted symbol ids/spans as usable
output. Snapshot mode (--no-daemon) cannot heal and instead preserves
the explicit per-file index_stale: true diagnostic. A changed file the
index does not cover at all — a skip directory (dist/, target/,
node_modules/, …), any per-directory ignore rule the walker honours
(.code-index-ignore, a nested .gitignore, .git/info/exclude), a
hidden dot-file, one over the file-size cap, or one the OS will not let
the indexer read at all (a permissions problem, a lock, a directory
sitting where a file belongs) — never heals, so it is disclosed per file
as not_indexed: true instead of blocking the call. When the bounded
wait does run out, the index_updating refusal NAMES the paths it is
still waiting on (up to five, and it says when it is showing only a
prefix): a path still named on the next call is not watcher lag, and
index_coverage will give it a verdict with the rule that proved it.
The project root may sit anywhere inside the work tree (a monorepo
member, a workspace crate): git's output is rebased onto it, so paths
stay project-relative and a sibling project's changes never appear. A
file git reports as deleted that is still on disk was untracked
(git rm --cached), not removed: it is reported as
change: "untracked" with no symbol rows, never as an API break.
Other corrective misses (for example an unknown focus) carry
did_you_mean suggestions.
Language packages
| Language | Extensions | Package grammar |
|---|---|---|
| Rust | .rs |
tree-sitter-rust |
| Python | .py, .pyi, .pyw |
tree-sitter-python |
| TypeScript / JavaScript | .ts .tsx .js .jsx .mjs .cjs |
tree-sitter-typescript / -javascript |
| C# | .cs (skips *.Designer.cs, *.g.cs) |
tree-sitter-c-sharp |
| PHP | .php .php3 .php4 .php5 .phtml |
tree-sitter-php |
| Ruby | .rb .rake .gemspec, Rakefile, Gemfile |
tree-sitter-ruby |
Ruby extraction is Rails-aware: association macros (has_many,
belongs_to, has_one, has_and_belongs_to_many) emit a type reference
to the associated model class (honoring class_name: and singularizing
plural names), and mixins (include/extend/prepend) reference the
mixed-in module.
Default-skipped directories include node_modules, vendor, __pycache__,
.venv, .tox, dist, build, .next, .code-index, plus
target/ (Rust roots only) and bin//obj/ (.NET roots only).
Add exclusions via .code-index-ignore (same syntax as .gitignore);
it cannot restore these default-skipped directories.
Binaries
| Binary | Purpose |
|---|---|
code-index |
CLI: init, skill, query, index, watch, doctor, link, languages, plugin |
code-index-daemon |
Long-lived watcher + RPC server |
code-index-mcp |
MCP stdio bridge for AI agents |
code-index-plugin-host |
Bounded worker that runs plugin packages |
You normally only invoke code-index-mcp; it spawns the daemon for you,
and the daemon spawns code-index-plugin-host when a plugin package is
active. Keep all four together — the host is what enforces the bounds a
package runs under, so a missing one is not a degraded feature.
CLI reference
code-index query prints one JSON document. Every content_in text reference,
including nested and batch entries, is copied into content; unreferenced MCP
content items remain in unreferenced_content_items. content_in records the
original MCP location, not a pointer into this CLI document. Raw MCP transport
keeps its split content shape.
code-index init # write only .code-index.toml, with detected
# languages enabled
code-index query <tool> '<json>' # ask the index from a shell: the MCP tools,
# reachable by a hook, a pre-commit or a
# human. Exit 0 answered, 1 refused, 2 usage;
# a batch exits 1 only if EVERY entry refused
code-index index # one-shot index of the current project
code-index watch # initial index + live updates (foreground)
code-index doctor # diagnose: DB, schema, daemon, index freshness,
# resolver health, watch set, free disk space
code-index link add <name> <path> [--relationship <kind>]
# add a link; infer worktree for the same Git repo
code-index link list [--json] # list configured links + live daemon status
code-index link remove <name> # remove a [[links]] entry
code-index approve [<request>...] # grant what a repo's .code-index.toml asks for (a
# [[links]] entry, a raised size cap); no args lists
code-index languages # which languages are enabled, who owns
# each, and which are present but not
# enabled (with the command)
code-index languages enable <lang>... # opt a language in
code-index languages owner <lang> auto|builtin|package
# who extracts it; see "Languages" below
Add --root <path> to operate on a project other than the current dir.
Languages: enabled, and who owns each one
[languages] in .code-index.toml records language enablement and owner
choices. Automatic first-party maintenance defaults on; its machine and project
opt-outs are described under automatic maintenance.
[languages]
enabled = ["ruby", "rust"] # the first-party languages this project opted into
ruby = "auto" # owner: auto | builtin | package (default: auto)
enabledis decided for you once.code-index initdetects the languages in the tree and writes exactly those to.code-index.toml. The first daemon orcode-index-mcpstart in a project with noenabledkey does the same — from disk plus any existing index, so an upgraded project keeps every language it already indexes — but writes it only to.code-index/languages.toml, inside the index state directory: an automatic start never writes into your project root and never dirties your git tree. It says so inproject_overview.language_ownership.enablement(written_to,change_with). Automatic maintenance can extend a nonempty generated list when new file types need another verified first-party package. Set top-levelautomatic_plugins = falseto keep a project's list fixed. A language excluded by the effective list is reported aslanguage_not_enabled, withcode-index languages enable <lang>as repair.- Precedence:
.code-index.tomlalways wins. Its[languages] enabledis used whenever it is present;.code-index/languages.tomlis read only while it is not. A missing key means the same thing whether the config file exists or not: use the automatic decision, or enable all languages until a first start records one.initandlanguages enablerecord an explicit list.linkandlanguages ownercan create the file without that key; package approval and automatic updates can extend an existing explicit list.language_ownership.enabled_sourcesays which one is in force (config,stateornone). - Release binaries contain no native language parsers. Rust, Python, JavaScript, TypeScript, C#, PHP and Ruby run through signed language packages. Text indexing remains part of the runtime. An unavailable or unapproved package is reported; there is no native fallback.
- The owner setting:
autoselects an enabled, approved, healthy package;packageexplicitly selects a package. Existing enablement and approvals are preserved on upgrade. A project explicitly pinned tobuiltinstays unindexed when this build has no builtin for that language. Runcode-index languages owner <lang> autoto migrate that choice. Development builds may still opt into frozen native fallbacks. - Bundled first-party packages. A release archive carries the
signed Ruby, PHP, JavaScript, TypeScript, C#, Rust and Python packages in
code-index-packages/, beside the binaries;install.shandinstall.ps1install that directory beside them.code-index languages enable rubyadopts the bundled package without downloading anything: it is verified exactly asplugin installverifies any file (the signature against your trust store, which holds the key compiled into this binary, and the reserved-id rule), then installed, graded and approved throughplugin addwith the package's requested grant — the confirmation block is printed.code-index languages listandcode-index plugin statusshow what is bundled, what was refused and whether this project approved it. On first start, enabled languages underautoadopt bundles signed by a key shipped in the binary through the same install and approval checks. Withdrawn keys and operator choices remain authoritative; a third-party bundle requires explicit approval. - A running daemon re-extracts the files whose owner changes and
re-arms within a few seconds of the edit.
project_overview.language_ownership.owner_switchandindex_freshness.owner_switchsay which languages moved and how many files were re-extracted. Offline indexing and startup reconcile a changed activation through an isolated full rebuild before replacing active facts. - First-party packages keep the plain ids (
ruby,php,rust, …): their rows are byte-identical inlangto the builtin's, so filters, baselines and bridges do not move when the owner changes. Those ids are reserved — seeplugin trust add --first-partybelow. - Compatibility: a
code-indexolder than this release refuses a[languages] <lang> = …key. Onlyenabled(which every release parses) is ever written automatically.
code-index plugin — language packages (EXPERIMENTAL)
A plugin package (.cip) is a language extractor that runs inside
code-index-plugin-host, an isolated worker process. The extractor is
wasm with zero imports, so the guest as loaded has no way to name a
syscall at all — no file, no socket, no clock. That property is
structural and holds on every platform.
The worker is not a kernel sandbox, and we do not claim it is one. There is no namespace and no capability drop. Code that escaped wasmtime inside a worker could still read any file the invoking user can read; the cwd is private and empty, but absolute paths are unaffected by that. Network denial is a seccomp filter that denies a named set of syscalls, and it exists only on Linux x86_64 and aarch64 — on every other target there is no filter at all.
Two surfaces say so, rather than leaving you to know.
code-index plugin status prints a containment line for this build's
target, and project_overview carries
plugin_activation.worker_containment whenever a plugin host is
running. Both report filesystem: unconfined on every platform, and
neither ever reports the network filter as enforced: a worker's kernel
outcome reaches only that worker's self-report, which no shipped path
enables, so the honest states are requested_unverified (a filter is
installed and nobody upstream can confirm the kernel took it),
unsupported (this target has none) and not_requested. An ABSENT
block means no plugin host is running, or an older daemon — never that a
worker is contained. _prdoc/guides/80-threat-model.md §6 enumerates
exactly what an escaped guest could still reach, and §7 lists what that
document could not verify.
A package must be signed by a trusted key: unsigned is refused, at
install and again at every load, and there is no --allow-unsigned.
Installing one grants it nothing; approval for a project is a separate,
explicit step that names the capabilities being granted. Automatic first-party
maintenance can approve verified first-party packages under its separate policy;
see automatic maintenance.
Our own publisher key ships inside the binary
(sha256:1cb03259a8c870b6db02360abd9351e17e67724d1f8c3509d85c4bf6b06fa72c,
shown as [BUILTIN] by plugin trust list), so a package we publish
installs with nothing to set up first. It extends no trust you had not
already extended — forging that anchor means forging the binary — and
it is revocable like any other: plugin trust remove <fingerprint>
records a denial in your trust directory and the key stops verifying at
the next load. Anchoring a THIRD-PARTY publisher is still a deliberate
act: plugin trust add, with a fingerprint you obtained somewhere other
than the key file.
Plain language ids are reserved for first-party packages. A package
that declares [[languages]] id = "ruby" (or any id a compiled-in plugin
stamps: rust, python, typescript, javascript, csharp, php,
ruby) stamps its rows with that plain id, exactly as the builtin does.
So it is refused — language_reserved, by name, at install and again at
every load — unless the key that signed it is the one shipped with this
build or one you anchored with --first-party (for a rebuild that
publishes its own first-party languages). plugin trust list marks such
keys [FIRST-PARTY].
# the one command: verify, show what approving does, ask once, do it all
code-index plugin add [<file.cip|digest>] [--grant requested|none|<c,...>] [--yes]
code-index plugin add <id>[@<version>] # resolve through the distribution
# catalog; the catalog IS the pin
code-index plugin add <https://…/pkg.cip> --sha256 <digest> [--signature-url <url>]
# fetches the .cip AND the .cips
# beside it; --sha256 is REQUIRED
# for a URL and optional for a file
# --registry-url <url|path> overrides
# which catalog an id resolves in
# authoring
code-index plugin pack <dir> [--out <file>] # build a .cip from a directory tree
code-index plugin inspect <pkg> [--json] # print what a package declares
code-index plugin validate <pkg> # parse + validate, printing any refusal
code-index plugin digest <pkg> # install identity + extraction identity
code-index plugin sign <pkg> --key <keyfile> # write the detached .cips beside it
# publisher keys and trust (user-scoped; no project)
code-index plugin key generate <keyfile> # Ed25519 pair; prints the fingerprint
code-index plugin key fingerprint <key.pub> # read one back; READ ONLY, no store
code-index plugin trust add <key.pub> --fingerprint <fp> --name <label>
code-index plugin trust add <key.pub> --fingerprint <fp> --name <label> --first-party
# this key may publish RESERVED plain
# language ids (ruby, php, rust, ...)
code-index plugin trust list # every anchor this machine holds
code-index plugin trust remove <fingerprint> # takes effect at the NEXT LOAD
# (a [BUILTIN] key is denied, not deleted)
# operating
code-index plugin install <pkg> --sha256 <d> # into the user store, pinned by digest
code-index plugin install <id> # same, resolved through the catalog:
# --sha256 optional (the catalog pins),
# and it needs NO project — this is
# what `install.sh --with-plugin` runs
code-index plugin check <digest> # run it on its own fixtures, record a verdict
code-index plugin check <digest> --repo <dir> # run it over THAT tree, record nothing
code-index plugin enable <digest> \
[--grant requested|none|<c,...>] \
[--capabilities <c,...>] [--bridges <b,...>] # approve for THIS project
code-index plugin disable <digest> # withdraw approval; stays installed
code-index plugin remove <digest> # delete from the store and withdraw
# updating (see "Updating a package" below)
code-index plugin update # check every [update."<id>"] entry; apply
# only where the AUTHORITY is unchanged
code-index plugin update --check # check and record only; never applies
code-index plugin update <id> # newest version THIS host may install,
# from the signed package channel
# the distribution catalog (read only; no project)
code-index plugin catalog # the catalog compiled into this binary
code-index plugin catalog <url|path> # fetch/read it, VERIFY <source>.sig, report
code-index plugin catalog-merge --out <dir> [--base <channel>] [--include <release catalog>]
[--package <x.cip> --url <u> --signature-url <u>] # release tooling: build the package
# channel (registry.v2.json + its v1 projection)
# state
code-index plugin status # installed, approved, granted, what is owed
code-index plugin doctor # diagnose the installation (read only)
code-index plugin retry # finish what an interrupted activation owes
code-index plugin rollback # switch back to the retained generation
code-index plugin gc [--dry-run] # reclaim unreachable generations and bytes
add verifies the signature before it renders anything, shows the
publisher under your label for the key plus the grant and the measured
reindex domain, and asks once — a bare Enter is no. Given a URL it
fetches the .cip and its .cips into a staging directory outside the
store — delegating the transfer to curl, because the transport is
trusted for nothing here — and then runs exactly those checks. --sha256
is required for a URL and optional for a file, and the difference is not
caution: the signature already decides who published the bytes, but the
bytes at a URL are chosen by whoever controls the URL and the network, so
a validly signed but different package can be served. The digest pin
closes that, and nothing else. enable refuses a
package that has not passed check, and rollback executes no package
at all. A signature says these bytes are the ones that key signed; it
does not say they are safe.
catalog is the read-only leaf for the distribution catalog — the
document that says which package versions exist, at which URLs, pinned to
which digests. With no argument it reports the catalog compiled into this
binary. Given a URL or a path, or with COSI_REGISTRY_URL /
COSI_REGISTRY_PATH set, it reads that one and requires <source>.sig
beside it, verified against the keys you anchor — so plugin trust remove reaches this door exactly as it reaches an installed package.
There is no flag that accepts an unsigned catalog: a catalog decides
which bytes plugin add <id> fetches and which digest they are pinned
to, so an unsigned one moves the pin. The compiled-in catalog is the one
exception, because forging it means forging the binary. It prints the
source, the byte count, the fingerprint that verified it, the platform
state and the plugin list, and it exits non-zero on any refusal — which
is how the release checks its own signature through the same code path a
customer takes.
The operator guides under _prdoc/guides/ cover
packaging, the capability model, recovery and the threat model in full.
The distribution catalog lists the packages available for this build.
After its signed assets are published, plugin add <id> resolves them
without copying a URL. Release binaries contain no native language
fallbacks; auto selects an enabled, approved, healthy language package.
Explicit builtin owner choices remain authoritative and need migration.
| id | claims | notes |
|---|---|---|
de.h-dv.xaml |
.xaml |
binds x:Class / Click= into the paired C# code-behind |
de.h-dv.timeline |
.dataset .xsql .shd .lgd |
two languages out of one extractor; resolves inside a definition file only |
de.h-dv.ruby |
.rbx .rb .rake .gemspec, Rakefile, Gemfile |
shared canonical Ruby extractor; native domains require approved displacement and package ownership. Rails derived names require --derived-names |
de.h-dv.svelte |
.svelte |
the TEMPLATE half of a Svelte single-file component — markup, mustaches, {#snippet} and {@render}. It claims .svelte and nothing else: .svelte.ts and .svelte.js are runes modules the TypeScript and JavaScript packages already read correctly. One capability, same_file_candidate |
de.h-dv.vue |
.vue |
a Vue single-file component: the template's PascalCase component tags, @event handlers, bindings, v-for sources and {{ }} interpolations, and each <script> / <script setup> body handed to the package's TypeScript or JavaScript extractor (setup is file-scoped, a plain <script> a module). Any other <script lang> is reported unsupported. Capabilities same_file_candidate,exported_candidate |
de.h-dv.php |
.php .php3 .php4 .php5 .phtml |
canonical PHP extraction and package-owned Composer module layout |
de.h-dv.javascript |
.js .mjs .cjs .jsx |
separate JavaScript and JSX grammar lanes |
de.h-dv.typescript |
.ts .tsx |
separate TypeScript and TSX grammar lanes |
de.h-dv.csharp |
.cs |
canonical C# extraction, including granted extension methods; generated .Designer.cs and .g.cs files excluded |
de.h-dv.rust |
.rs |
canonical Rust extraction |
de.h-dv.python |
.py .pyi .pyw |
canonical Python extraction and explicit import/export metadata; fixture conformance needs --capabilities lexical_local,pytest_fixture |
To select a bundled language package, run code-index languages enable <lang> and approve its requested capabilities; auto selects it in release
builds. languages list and plugin status report the
selected owner and any refusal. Release builds use --no-default-features
and require installed, approved language packages. Cargo development builds
offer frozen native fallbacks as test oracles (only Ruby by default); individual
builtin-<lang> features select them explicitly.
Every release attaches <id>-<version>.cips beside each .cip, and this
release's publisher key is compiled into the binaries, so a pinned install
works on a machine that has anchored nobody. Download the .cips and keep
it beside the .cip — an install without it is refused with
signature_missing. If a package you
installed has stopped loading, read
_prdoc/guides/80-operator-recovery.md §1.2, which is the repair for
a package that really is unsigned: one you built yourself, or one whose
publisher ships no .cips.
EXPERIMENTAL: the plugin ABI and the
.cipformat are not stable across releases yet.
Updating a package
A missing .code-index.toml uses defaults. An invalid file refuses startup
in the CLI watcher, daemon and MCP server, and refuses an indexing walk before activation or row changes.
The shared error names the file, TOML position and accepted configuration keys.
Fix the file before restarting; invalid ignores never become a broader walk.
A walk policy that cannot load the config also blocks live file events.
Every index.extra_ignores entry is validated at load using gitignore syntax;
an invalid pattern is refused with its list position and value.
languages.enabled accepts well-formed language ids even before their package
is installed. Loading warns on stderr and doctor reports names no builtin or
installed package declares, with nearest known names: install the declaring
package or remove the entry. Other enabled languages continue indexing.
Malformed ids are refused. code-index languages enable still refuses unknown
names without editing the file. Unavailable languages have an ownership row
even when they have no indexed files.
Malformed trusted .code-index/languages.toml is reported as an error.
Routing fallbacks disable package enablement while that error persists.
Older configs may contain logging.level, logging.format, workspace.name
or plugins[].file. These keys remain accepted for compatibility and have no
effect. Loading emits one diagnostic per file and key set in a process; repeated loads
do not repeat it. The diagnostic names the keys; code-index doctor
reports them too. Remove them. Use --log (or CODE_INDEX_LOG) and --log-json
for logging, and code-index plugin add <file> to install a package.
Automatic first-party language plugins are enabled by default across all your projects. The indexer and daemon detect languages, install compatible signed first-party packages, enable them and update them with affected reindexing. Upgrading the core binaries together is sufficient; the installer needs no project list.
To turn maintenance off for all projects using your operator store:
code-index plugin update --automatic=false
An explicit operator policy lives outside repositories, in
$CODE_INDEX_HOME/automatic-plugins.toml (normally
~/.code-index/automatic-plugins.toml). Every project opened later uses it;
the installer does not need a project list. Run
code-index plugin update --automatic to turn maintenance back on, or
code-index plugin update --check to inspect candidates without applying them.
The daemon reconciles after startup and checks for new file types once a minute. Checks for the same file types are limited to once per day; a core upgrade or new file type triggers an earlier check. Failed checks retry after five minutes. Svelte/Vue detection also makes the JavaScript and TypeScript producers available. Installed and bundled packages are used first, so local packages remain usable offline. Downloads and conformance work are serialized across projects and package bytes are reused from the shared store. Signature, host compatibility and conformance checks precede activation; failed activation retains the working version.
With automatic maintenance enabled, a nonempty generated language list grows when files
need another verified first-party package, including packages such as Vue that
are offered by the registry rather than bundled. Each project updates its own
state and approval, even when projects share the package store. --check reports
missing language entries without changing them.
An explicit manifest language list is never widened. Empty language lists,
builtin owners, requested digest pins, per-package
update settings, publisher withdrawals and disabled packages take precedence.
To keep a nonempty list fixed, set automatic_plugins = false for the project.
Approved packages excluded by the list remain installed; plugin status and
project_overview identify that exclusion. Bare plugin enable <digest> keeps
an existing grant; --grant none explicitly withdraws its answering authority.
For a fully granted first-party plugin, an upgrade also grants newly requested
capabilities. Restricted grants and publisher changes still require approval.
To opt one project out, set automatic_plugins = false at the top level of
its .code-index.toml after upgrading that project's code-index; older
binaries reject unknown configuration keys. plugin status reports both the
machine policy and whether it applies to this project.
Core upgrades update bundled packages per project. When a project next
starts its daemon or MCP server, or runs code-index index/watch on its
default index, it checks the signed first-party packages
beside the new core. A newer bundled version replaces its approved version
after signature, host-compatibility and conformance checks. The installer
does not need to know the projects; reconciliation uses local bundle files
without fetching packages or running a timer.
Each project's approval record makes subsequent starts a no-op. A newer
installed package is kept when the bundle is older.
This preserves explicit builtin owners, other publishers' selected packages,
withdrawals and restricted grants. A bundle needing more authority is reported
instead of approved silently; plugin update <id> handles that decision.
An explicit rollback to an older approved digest is also retained on restart.
Changed extractor bytes may reindex the files they claim, as part of the core
upgrade. An explicit package owner permits upgrades of its existing approval.
Approvals are keyed by digest, so a new version of a package is a
package this project has not approved. plugin update is how that gets
answered for independently released packages without retyping four commands.
Read C1's not_graded disclosures before enabling a grant. In particular,
bridge_authority is not recorded or compared; see the
conformance contract.
That contract also explains why a narrower saved run yields
conformance_not_run, including when it failed, and when a covering failed
run yields conformance_failed. Run the exact check command in the witness.
You do not have to type this from memory. code-index plugin add <URL> --sha256 <digest> prints the stanza for the package it just
installed, with the id and the URL already filled in, on
update_config lines — that command is the one moment the tool holds
both halves, so it offers them there rather than in a paragraph. Where
an entry already exists it says so instead, with the auto_apply in
force, and it names both URLs when the configured source is not the one
you installed from. plugin add <FILE> prints no source — there is
no URL to put in one — and says that is why.
It is opt-in per package, in the project's own .code-index.toml:
The daemon checks configured entries in the background after becoming ready,
then once per day while it stays running. Each project's operator store records
the attempt, so restarting a daemon does not repeat a failed/offline fetch within
24 hours. Projects with no [update] entries make no per-package update requests;
automatic first-party maintenance follows its separate policy. The
matching code-index CLI must be installed beside the daemon; its existing
signature, conformance, grant and activation checks also govern background runs.
auto_apply = false records findings only. A changed extraction identity still
requires deliberate confirmation through code-index plugin update <id>.
plugin status and the MCP update notifications report completed findings;
the daemon log names the background command's detailed log in the operator
store. An explicit CLI update remains available during the cooldown.
[update."de.h-dv.xaml"]
source = "https://git.h-dv.de/api/packages/h-dv/generic/code-index/<release>/de.h-dv.xaml-<pkgver>.cip"
auto_apply = false # the default: CHECK AND NOTIFY ONLY
That is the real shape of the URL here — <release> is the code-index
version the package was published with and <pkgver> is the package's
own version, both as they appear in the release notes. <release>
carries NO leading v: the tag is v0.27.1 and the path segment is
0.27.1, because the publishing step strips it (PKG_VERSION="${TAG#v}").
Measured — .../code-index/v0.27.1/de.h-dv.xaml-0.2.0.cip answers 404
and .../code-index/0.27.1/de.h-dv.xaml-0.2.0.cip answers 200. The
same URL with .cips, .cip.sha256 and .cip.digest.txt appended
serves the signature, the checksum and the digest record; the
package_digest line of .cip.digest.txt is what --sha256 wants
elsewhere in this file (the other line, extraction_identity, is not).
The catalog is the OTHER door, and it is a separate key:
[update."de.h-dv.timeline"]
from = "registry" # and NO `source` — the catalog resolves the id
auto_apply = false
from says which door the stanza uses, and the two are mutually
exclusive: "url" — the default, and the state of an unstated field —
requires source, and "registry" requires source to be
absent. A stanza naming both is refused, quoting both values it read;
a from = "url" stanza with no source is refused too, and that
refusal names the other door. An operator who wrote both has not said
which they meant, and choosing for them would be this tool choosing
where your code comes from.
It is a key rather than a value inside source, for a measured
reason. The test used to be source.starts_with("registry") — and
source = "registry.example.com/pkg.cip" is a URL an operator may
really write. It was swallowed by that prefix and served the EMBEDDED
catalog instead of that server, silently. A sentinel living in a field
whose values are hostnames cannot be told apart from a hostname; two
keys cannot collide.
The registry update door resolves the package id through the signed catalog this
machine is configured with — COSI_REGISTRY_URL / COSI_REGISTRY_PATH,
or the live package channel — and a repository cannot name a
different catalog. It changes only WHERE the candidate bytes come from:
everything below is evaluated the same way, so a catalog offering an
older or equal version is stopped by the same clause a URL would be. It
does add one check the URL door has no way to make — the catalog names
the version's package_digest, and bytes whose digest is not that one
are refused as unreachable rather than assessed.
source is a URL the .cip is fetched from; the .cips beside it is
fetched too. There is no --sha256 here and that is deliberate — a pin
would have to be edited on every release, so the digest is not the
control. The control is that the AUTHORITY must be unchanged, and
plugin update applies a candidate only when all of the following hold
against the version whose grant is in force:
- the same publisher key, by fingerprint, out of your trust store — "the signature verifies" is not the test, "the same publisher" is;
- a grant that is a subset of the one already granted (no new
capability, no new bridge, no
derived_names); - no new
[[displaces]]row; - the same package id;
- the digest was not withdrawn here by
plugin disable; - the candidate's
[package] versionis strictly NEWER than the one in force, by semver precedence and not by string order —0.10.0is newer than0.9.0, and build metadata (1.2.3+a) is not part of the comparison at all; - and
extraction_identityunchanged — not an authority question but a cost one, since moving it reindexes every file the package claims.
Anything else is recorded, reported with the specific reason that
stopped it, and left for a human. The reasons are codes, not prose —
update_publisher_changed, update_grant_increase,
update_displacement_added, update_package_id_changed,
update_version_not_newer, update_version_not_comparable,
update_extraction_identity_changed, withdrawn_here, plus
update_deferred_by_check and update_apply_failed for the two
non-authority stops — and each carries a witness naming the actual
capability, bridge, fingerprint, id or version pair. They appear on plugin update,
on plugin status, on plugin doctor, and in project_overview's
package_updates block as blocked_by {reason, witness} pairs.
A version that is not newer is refused, and an EQUAL version is
refused too (#255 —
until it was fixed, an older package by the same publisher satisfied
every other condition and applied: measured at 0.0.1 over an installed
9.9.9, reported applied). Two codes, because they are two different
statements:
update_version_not_newer— measured. Either a downgrade (a rollback: the same publisher, the same grant, an older version) or an equal version with different bytes (a republish — the extractor changed without a version bump). Both refuse, because "newer" is the entire warrant for acting with no operator present, and the witness says which of the two it is.update_version_not_comparable— not measured. One of the two versions is not one this host can order, so no comparison happened. This cannot arise from a published.cip(the manifest validator is the version parser, so anything that parsed can be ordered), and it refuses rather than falling through precisely so that it never becomes an unattended apply nobody graded.
To take a republished or older version deliberately, plugin add is the
command — it asks.
Channel updates use the daemon's background updater. The operator's
automatic policy needs no per-package entries. Without that policy, configured
[update] entries are checked after startup and daily; auto_apply = true
permits application under the narrower per-package rules above, including the
extraction-identity stop. Projects with neither policy nor configured entries
make no update requests and report that no update check has run. An explicit
code-index plugin update runs immediately, independently of the cooldown.
To update manually instead, or to take a version update refused,
plugin add is the one command: it verifies, shows what the new grant
would do, and asks once.
code-index plugin update --check # what would happen, and why not
code-index plugin update # apply where the authority is unchanged
One package, from the signed package channel. First-party packages
are released on their own tags (pkg/<id>/v<version>), without a core
release, and listed in a rolling signed catalog — the package channel.
code-index plugin update <id> reads it and installs the newest version
whose requires_host admits this code-index release:
code-index plugin update de.h-dv.ruby --check # what is available
code-index plugin update de.h-dv.ruby # apply it
code-index plugin update de.h-dv.ruby --catalog ./mirror/registry.v2.json
- The catalog's
.sigis verified against your trust set, and so is the package's.cips; the fetched bytes must match the catalog's pin. - Where nothing about the authority changed it applies, exactly as an
auto_applyentry would. Where the grant grows, a displacement is added or the extraction identity moves, it asks — theplugin addconfirmation, with--grant/--yesas there. A different publisher, a different id, a withdrawn digest or a version that is not newer is refused by its code. - Atomic. Nothing is written until the candidate has been fetched and verified; C1 runs before the grant is written, and an activation that fails puts the approval record back. The version in force stays active.
- Approval writers for the same project run one at a time, including CLI
mutations, MCP
plugin_add, manifest/link grants and startup adoption. A competing command waits up to 120 seconds for the kernel lease, including while the holder makes progress. If still blocked,plugin_mutation_busynames the last observed holder's operation and PID and the time waited; retry after that writer finishes.plugin statusandplugin update --checkremain available, and other projects can update independently. - Without a running daemon, activation builds and resolves the index in
the foreground and the command waits for promotion. A large project or
full reindex may take several minutes;
activation_foregroundannounces this before the activation work starts. - Offline is a refusal, never "up to date": the catalog compiled into
the binary is never used here (it cannot know of a later release).
Point
--catalogat a local mirror (the.jsonand its.sig) to update without the network. Default source:COSI_REGISTRY_PATH, thenCOSI_REGISTRY_URL, then the package channel.
A package may declare which code-index releases it needs:
[package] requires_host = ">=0.33.0". A host outside the range refuses
it by name at install AND at activation (manifest.abi_unsupported,
naming the range and this host). After a code-index upgrade that moves
you out of an installed package's range, plugin update <id> fetches a
version that admits the new host.
The link subcommands manage the [[links]] array in .code-index.toml
without you having to hand-edit TOML. Comments and whitespace in the file
are preserved; writes are atomic; validation matches the MCP runtime
exactly (reserved name, duplicates, self-cycle, link-link path collisions
all caught at write time).
Auto-detected project root
If you don't pass --root, code-index walks up from your CWD looking for:
.code-index.toml— innermost wins. Drop one in any directory to pin that as the root regardless of any marker above it.code-index initcreates one for you..git/— innermost wins. Stops the walk: a workspace atrepo/with sub-crates atrepo/crates/foo/is correctly detected asrepo/even when you runcode-indexfrom inside a sub-crate. A stray~/.git(dotfiles repo) won't trip detection because a closer.git/always wins.- Outermost
Cargo.toml/package.json/pyproject.toml/composer.json/*.csproj/*.sln— only when no.git/is found anywhere up the chain. Catches non-git workspaces.
The walk is bounded at $HOME so a marker far above your home directory
can't poison detection.
Workspace links — querying multiple projects from one MCP entry
A customer install that extends a base product, a fork that needs to compare against upstream, an app that sits on a vendored library — common patterns where one Claude / Cursor / Cline session needs to query two indices at once.
Drop a [[links]] block into the primary project's
.code-index.toml:
[[links]]
name = "mainproject" # routing key for tool calls
path = "../../code/mainproject/24.0/source" # absolute, `~/…`, or relative to this file
relationship = "base" # base | dependency | fork_source | sibling | worktree
description = "mainproject 24.0 — base product"
A link declared in a COMMITTED manifest is a request, not a setting: a
repository you clone cannot make the daemon index and serve a directory
of yours by naming it. The server attaches it once you grant it —
code-index approve link:mainproject in the primary project (bound to the
path it points at now; re-pointing it asks again) — and until then says
so in the reply and in code-index doctor. code-index link add is you
typing the link, so it grants as it writes. The same rule holds for an
[index] max_file_size_mb above the built-in default
(code-index approve index.max_file_size_mb); lowering it needs nothing.
link add creates a missing config without recording a language decision;
adding or removing links preserves the enabled set and its source. With no
--relationship, another checkout sharing the primary's Git common directory
gets worktree; other targets get dependency. An explicit --relationship
always wins. Worktree links are queried by name and excluded from default
fan-out. Nested checkouts are excluded by the outer index's walk, so the
overlap warning applies to ordinary nested directories. Both add and remove
print the entry's name, path, relationship and config file; remove also reports
any approval it withdraws.
Then your existing MCP client config keeps one entry pointing at the primary root; the server attaches a daemon for each link at startup. Tools route across projects three different ways:
// (a) Name/phrase searches FAN OUT by default — primary + every
// available link. Each hit carries a `project` tag.
search_symbols { "query": "mainprojectService" }
// → [{ name: "mainprojectService", project: "primary", … },
// { name: "mainprojectService", project: "mainproject", … }]
// Pin a single project by passing its name:
search_symbols { "query": "mainprojectService", "project": "mainproject" }
// (b) Path-shaped tools (file_outline, read_code, get_dependencies)
// AUTO-ROUTE: an absolute path picks the owning project by
// longest root-prefix; relative paths default to primary.
file_outline { "path": "/abs/path/under/mainproject/foo.cs" } // → mainproject
file_outline { "path": "src/foo.rs" } // → primary
// (c) Symbol-id tools (get_symbol, find_references, find_callers,
// find_callees) are project-local: pass the `project` from
// the row that produced the id.
find_references { "symbol_id": 42, "project": "mainproject" }
project_overview on primary lists every linked project with its
index health, and the MCP info.instructions block names each
project + canonical root on initialize — so a fresh agent
discovers the topology without any extra calls.
What it does: fan-out + path auto-routing + isolation,
structured errors when a project name is unknown (project_not_found
with did_you_mean), configured-but-unavailable
(project_not_available — daemon failed to spawn or path was
unreachable), or a path lives outside every indexed root
(path_outside_known_roots with the known roots in did_you_mean).
What it doesn't do (yet): cross-project ref resolution
(find_callers on a mainproject symbol won't surface h-dv callers).
Cross-project definition, override and call-into-base queries remain design
proposals; they are not available through the current tool surface.
See _prdoc/missions/I011-workspace-links.md for the full design.
Architecture
See ARCHITECTURE.md for the design rationale and the
diagrams. See CONTRIBUTING.md to build, test, and
add a language plugin. Operator-facing guides — package authoring, the
extractor ABI, the capability model, grammar-build reproducibility,
recovery and the threat model — live under
_prdoc/guides/; specs and adaptation reports live
elsewhere under _prdoc/.
Status
Current release: v0.41.53 (changelog).
Per-release detail — what shipped, what it fixed, and the measurements —
lives in the release notes:
git.h-dv.de/h-dv/code-index/releases.
Design records for each mission are under _prdoc/missions/.
For resolved_by rule meanings and false-positive limits, read the
resolution guide
(MCP: code-index://docs/resolver-degradation).