Contributing · 3. Running locally

Three environment variables get you a useful local daemon, and one Docker overlay flag lets you edit console HTML without recompiling Rust. Those are the two facts that change how fast you work. Everything else in this chapter is detail around them.

This is a how-to. It assumes you have a binary from chapter 1.

3.1 The ports

SurfaceDefaultHost envPort env
HTTP (axum) plus the console127.0.0.1:14800CORECRUXD_HTTP_HOSTCORECRUXD_HTTP_PORT
MCP (Streamable HTTP)127.0.0.1:14801CORECRUXD_MCP_HOSTCORECRUXD_MCP_PORT
gRPC (tonic)127.0.0.1:4007CORECRUXD_GRPC_HOSTCORECRUXD_GRPC_PORT

Do not change the defaults in a pull request. Port 14800 is a fixed contract for every client in the ecosystem; the repository's CLAUDE.md marks it non-negotiable, and AGENTS.md:70 puts any default port change on the "Ask first" list. Overriding a port in your own shell for a single run is fine and is what the env vars are for.

The full listener configuration is in load_config (config.rs:793). The developer guide chapter 1 documents the runtime behaviour of all three planes.

3.2 The minimal dev environment

export CORECRUXD_AUTH_MODE=dev_scopes
export CORECRUXD_DATA_DIR=./data
export CORECRUXD_BUILD_CCXI=1
cargo run --bin corecruxd
VariableWhy you want it
CORECRUXD_AUTH_MODE=dev_scopesMandatory. There is no default; the daemon aborts without it. dev_scopes lets you pass scopes as a plain header
CORECRUXD_DATA_DIR=./dataKeeps the store inside your working copy so you can delete it. Without it the daemon resolves to ../CoreCruxData/v1
CORECRUXD_BUILD_CCXI=1Builds the .ccxi companion index at seal time. Without it, BM25 text search returns nothing and you will think retrieval is broken

With dev_scopes, an authenticated call is a header away:

curl -s http://localhost:14800/v1/console/summary -H 'X-Corecrux-Scopes: admin:read' | jq .

That exact shape is what the CI smoke step uses (ci.yml:248).

Delete ./data to start clean. It is a plain directory; nothing outside it holds state.

3.3 Auth modes

CORECRUXD_AUTH_MODE has no default and the daemon refuses to start without it (main.rs:307). The enum is at auth.rs:24; the strings are documented at config.example.env:14.

ValueMeaningUse for
offEvery scope check returns success immediatelyIntegration tests. The test harness pins this
dev_scopesScopes are read straight from the request, via X-Corecrux-Scopes or a bearer token parsed as a scope list (auth.rs:386)Local development. Testing only
jwt_hs256 (alias jwt)HS256 JWT verification. Requires CORECRUXD_JWT_HS256_SECRET, 32 bytes or moreRealistic auth locally
jwt_jwks (alias jwt_oidc)JWKS and OIDC verificationProduction

Mode strings parse leniently: dev, dev-scopes, jwt, jwks, oidc and several casings all resolve (auth.rs:58). An unrecognised value is not lenient; it aborts startup. That is deliberate: fail closed.

Binding a dev_scopes daemon to a non-loopback host additionally requires CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1. The Docker stack sets it (docker-compose.yml:29) because it binds 0.0.0.0 inside the container.

Do not assume runtime scope enforcement is on. The route-auth middleware defaults to shadow mode (route_auth.rs:565): a scope mismatch is logged as route_auth_shadow_mismatch and the request proceeds. The scope contract is enforced at test time by route_auth_matrix_is_complete (route_auth.rs:814), not at runtime, unless an operator sets CORECRUXD_ROUTE_AUTH=enforce. The per-handler require_http_scopes calls always apply regardless.

3.4 The full environment surface

config.example.env is the authoritative list of CORECRUXD_* variables, 436 lines, sectioned Required, Storage, Governance-tier local primitives, and so on. config.example.yaml is the file-config equivalent; CORECRUXD_CONFIG_PATH points at it. Environment always wins over file config, the canonical parse shape in load_config is env_string("CORECRUXD_X").or(file_config.section.field.clone()).unwrap_or(default).

Both files are release-boundary-required and their presence is asserted by scripts/assert-daemon-release-boundary.sh.

There is no CI check that config.example.env stays in sync with config.rs. Measured 2026-07-27: config.rs performs about 150 environment lookups; config.example.env documents about 98 distinct names. Every reference to the file from code or scripts asserts only that it exists and is copied into a release tarball. A new flag can ship undocumented and nothing notices. Writing that check is good first contribution number 7.

3.5 Cargo features

Four optional cargo features exist on corecruxd, all off by default: otel, wasm-extensions, dense-embed-model and hosted-surfaces (corecruxd/Cargo.toml). Counting every crate that carries a [features] block, corecrux-memory, corecrux-storage, corecruxctl and corecruxd, the workspace total is eight, which is the figure chapter 5 uses.

FeatureDefaultStatusAdds
hosted-surfacesoffFLAGThe Pro GPU compute bridge (/v1/gpu1/*) and GET /v1/cloud/access-contract. Compiled out of the default build, the routes 404 and the handler code is absent from the binary
wasm-extensionsoffFLAGThe wasmtime host for kind: wasm community extensions
oteloffFLAGOpenTelemetry OTLP export

hosted-surfaces has its own CI job, Test (hosted-surfaces) (ci.yml:291), which is not a required check. If you change code behind that feature, run it yourself:

cargo test -p corecruxd --features hosted-surfaces

Runtime environment flags such as CORECRUXD_CONTEXT_SURFACE and CORECRUXD_QUOTA are a different mechanism. Those routes are always compiled in and return 404 until the flag is set. Do not conflate the two: a cargo feature changes what is in the binary; an env flag changes what the binary does.

3.6 Docker Compose and the dev overlay

The base stack is docker-compose.yml:

docker compose up -d
AspectValue
Servicecrux, image cuecrux/crux-daemon:latest, build: .
Restart policyunless-stopped, so the console's "Restart daemon" button works via POST /v1/admin/restart (docker-compose.yml:8)
Published ports127.0.0.1:14800 and 127.0.0.1:14801
Volumecrux-data:/data
Healthcheckcurl -f http://localhost:14800/readyz every 10 s, 5 s timeout, 3 retries
Memory limit4G
AuthCORECRUXD_AUTH_MODE=dev_scopes plus CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1
LoggingCORECRUX_LOG_FORMAT=json

An optional Ollama sidecar sits behind a compose profile (docker-compose.yml:45). It starts Ollama on 127.0.0.1:11434 and pulls nomic-embed-text on first start:

docker compose --profile embeddings up -d

The dev overlay

This is the single highest-value dev-loop fact in the repository, and today it is only discoverable inside the file's own comments.

docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d

docker-compose.dev.yml:17 sets CORECRUXD_CONSOLE_DEV_PATH=/console-dev and bind-mounts crates/corecruxd/console read-only at that path. The file header states the effect verbatim: "edits to crates/corecruxd/console/index.html are served on the next browser refresh, no Rust rebuild, no container restart." The include_str! copy is still compiled into the binary; the dev path simply takes precedence at request time when the variable is set.

The overlay does three other things:

SettingEffect
CORECRUXD_WORKSPACE_PATH=/src with ./crates, ./Cargo.toml, ./Cargo.lock mounted read-onlyThe daemon builds its own context graph over this repository, modules, dependencies, stubs, dead code
CORECRUXD_SOURCE_ROOTS=/sources,/srcAllowlist for /v1/projects/{id}/planes/sync-layers; anything outside these prefixes is rejected with 400
Mounts ../PlanCrux at /sources/plancrux (docker-compose.dev.yml:33)This is a private monorepo path that you do not have. See below

The dev overlay is broken outside CueCrux. The ../PlanCrux bind mount points at a private repository. Depending on your Docker version, compose will either fail or silently mount an empty directory. The fix on your machine is to delete that one volume line locally, or to create an empty ../PlanCrux directory. Do not commit the deletion unless you are deliberately fixing this for everyone; it is a real bug worth an issue.

The Dockerfile

The builder image is rust:1.88-bookworm (Dockerfile:10). That is a deliberate exception to the house preference for Chainguard base images, documented at Dockerfile:5: the workspace pins its toolchain through rust-toolchain.toml and needs rustup to honour it, while the Chainguard Rust free tier is :latest-only. The build context excludes .git, so the git SHA is passed in as ARG GIT_SHA and consumed by crates/corecruxd/build.rs.

3.7 corecruxctl: the commands you actually need

The CLI has around 50 top-level subcommands. corecruxctl --help lists them all. These are the ones a contributor uses.

CommandDoes
corecruxctl startThe on-ramp: detect the daemon, sort out auth, wire MCP and Claude Code hooks, round-trip a fact
corecruxctl verify-store --strictPer-segment BLAKE3 re-derivation plus a chain walk
corecruxctl replay --strictRecompute and compare
corecruxctl inspect-receipt <file>Human-readable CROWN receipt breakdown
corecruxctl explain <receipt>The retrieval decision path for a receipt
corecruxctl gapsLow-coverage report
corecruxctl context exportExport the custody bundle
corecruxctl context verify --jsonVerify that bundle offline
corecruxctl audit-verifyVerify a bring-your-own audit bundle offline, with no daemon running
corecruxctl code-healthDead-code, stub and unused-dependency harvester

verify-store --strict and replay --strict are the two commands AGENTS.md:37 names as the way to verify the project's claims rather than believe them. See 4.9.

3.8 What breaks locally, and what it looks like

SymptomCauseFix
FATAL: CORECRUXD_AUTH_MODE is required on startupNo auth mode set. There is no defaultexport CORECRUXD_AUTH_MODE=dev_scopes
Startup aborts on an auth mode you thought was validThe value is unrecognised. Parsing is lenient about casing and aliases but fail-closed on nonsenseUse one of off, dev_scopes, jwt_hs256, jwt_jwks
address already in useAnother daemon holds 14800, 14801 or 4007Stop it, or override the port env vars for this run
401 with "hint": "set X-Corecrux-Scopes or Authorization: Bearer <scopes>"dev_scopes mode with no scopes on the requestAdd -H 'X-Corecrux-Scopes: admin:read'
403 with "code": "MISSING_SCOPE" and a missingScopes arrayThe route needs scopes you did not sendSend the scopes the body names
Text search returns nothingCORECRUXD_BUILD_CCXI is unset, so no .ccxi companion index was builtexport CORECRUXD_BUILD_CCXI=1 and re-ingest
Startup aborts complaining about the embedding selectionThe only cross-field validator, validate_embedding_selection (config.rs:609), rejected a mutually exclusive combinationRead the message; it names the conflict
docker compose -f ... -f docker-compose.dev.yml up fails on a missing pathThe ../PlanCrux private mount, section 3.6Remove that volume line locally
Console changes do not appearYou are on the base stack, not the dev overlayAdd -f docker-compose.dev.yml

Every failure path in the HTTP surface returns RFC 7807 with Content-Type: application/problem+json, and detail carries genuinely actionable text, often naming the exact environment variable to flip or the exact CLI command to run. Read it before you grep. The full error shape is in developer guide 1.5.

Sources