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
| Surface | Default | Host env | Port env |
|---|---|---|---|
| HTTP (axum) plus the console | 127.0.0.1:14800 | CORECRUXD_HTTP_HOST | CORECRUXD_HTTP_PORT |
| MCP (Streamable HTTP) | 127.0.0.1:14801 | CORECRUXD_MCP_HOST | CORECRUXD_MCP_PORT |
| gRPC (tonic) | 127.0.0.1:4007 | CORECRUXD_GRPC_HOST | CORECRUXD_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
| Variable | Why you want it |
|---|---|
CORECRUXD_AUTH_MODE=dev_scopes | Mandatory. There is no default; the daemon aborts without it. dev_scopes lets you pass scopes as a plain header |
CORECRUXD_DATA_DIR=./data | Keeps the store inside your working copy so you can delete it. Without it the daemon resolves to ../CoreCruxData/v1 |
CORECRUXD_BUILD_CCXI=1 | Builds 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.
| Value | Meaning | Use for |
|---|---|---|
off | Every scope check returns success immediately | Integration tests. The test harness pins this |
dev_scopes | Scopes 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 more | Realistic auth locally |
jwt_jwks (alias jwt_oidc) | JWKS and OIDC verification | Production |
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
shadowmode (route_auth.rs:565): a scope mismatch is logged asroute_auth_shadow_mismatchand the request proceeds. The scope contract is enforced at test time byroute_auth_matrix_is_complete(route_auth.rs:814), not at runtime, unless an operator setsCORECRUXD_ROUTE_AUTH=enforce. The per-handlerrequire_http_scopescalls 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.envstays in sync withconfig.rs. Measured 2026-07-27:config.rsperforms about 150 environment lookups;config.example.envdocuments 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.
| Feature | Default | Status | Adds |
|---|---|---|---|
hosted-surfaces | off | FLAG | The 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-extensions | off | FLAG | The wasmtime host for kind: wasm community extensions |
otel | off | FLAG | OpenTelemetry 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
| Aspect | Value |
|---|---|
| Service | crux, image cuecrux/crux-daemon:latest, build: . |
| Restart policy | unless-stopped, so the console's "Restart daemon" button works via POST /v1/admin/restart (docker-compose.yml:8) |
| Published ports | 127.0.0.1:14800 and 127.0.0.1:14801 |
| Volume | crux-data:/data |
| Healthcheck | curl -f http://localhost:14800/readyz every 10 s, 5 s timeout, 3 retries |
| Memory limit | 4G |
| Auth | CORECRUXD_AUTH_MODE=dev_scopes plus CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1 |
| Logging | CORECRUX_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:
| Setting | Effect |
|---|---|
CORECRUXD_WORKSPACE_PATH=/src with ./crates, ./Cargo.toml, ./Cargo.lock mounted read-only | The daemon builds its own context graph over this repository, modules, dependencies, stubs, dead code |
CORECRUXD_SOURCE_ROOTS=/sources,/src | Allowlist 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
../PlanCruxbind 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../PlanCruxdirectory. 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.
| Command | Does |
|---|---|
corecruxctl start | The on-ramp: detect the daemon, sort out auth, wire MCP and Claude Code hooks, round-trip a fact |
corecruxctl verify-store --strict | Per-segment BLAKE3 re-derivation plus a chain walk |
corecruxctl replay --strict | Recompute and compare |
corecruxctl inspect-receipt <file> | Human-readable CROWN receipt breakdown |
corecruxctl explain <receipt> | The retrieval decision path for a receipt |
corecruxctl gaps | Low-coverage report |
corecruxctl context export | Export the custody bundle |
corecruxctl context verify --json | Verify that bundle offline |
corecruxctl audit-verify | Verify a bring-your-own audit bundle offline, with no daemon running |
corecruxctl code-health | Dead-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
| Symptom | Cause | Fix |
|---|---|---|
FATAL: CORECRUXD_AUTH_MODE is required on startup | No auth mode set. There is no default | export CORECRUXD_AUTH_MODE=dev_scopes |
| Startup aborts on an auth mode you thought was valid | The value is unrecognised. Parsing is lenient about casing and aliases but fail-closed on nonsense | Use one of off, dev_scopes, jwt_hs256, jwt_jwks |
address already in use | Another daemon holds 14800, 14801 or 4007 | Stop 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 request | Add -H 'X-Corecrux-Scopes: admin:read' |
403 with "code": "MISSING_SCOPE" and a missingScopes array | The route needs scopes you did not send | Send the scopes the body names |
| Text search returns nothing | CORECRUXD_BUILD_CCXI is unset, so no .ccxi companion index was built | export CORECRUXD_BUILD_CCXI=1 and re-ingest |
| Startup aborts complaining about the embedding selection | The only cross-field validator, validate_embedding_selection (config.rs:609), rejected a mutually exclusive combination | Read the message; it names the conflict |
docker compose -f ... -f docker-compose.dev.yml up fails on a missing path | The ../PlanCrux private mount, section 3.6 | Remove that volume line locally |
| Console changes do not appear | You are on the base stack, not the dev overlay | Add -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
- crates/corecruxd/src/config.rs:793,
load_config - crates/corecruxd/src/config.rs:609,
validate_embedding_selection, the only cross-field validator - crates/corecruxd/src/main.rs:307, auth-mode-required abort
- crates/corecruxd/src/auth.rs:24, the
AuthModeenum - crates/corecruxd/src/auth.rs:58, lenient mode-string parsing
- crates/corecruxd/src/auth.rs:386, bearer-token-as-scope-list parsing
- crates/corecruxd/src/http/route_auth.rs:565,
RouteAuthMode, defaultshadow - crates/corecruxd/src/http/route_auth.rs:814,
route_auth_matrix_is_complete - config.example.env:14, the auth-mode strings
- docker-compose.yml:8, the restart-policy rationale
- docker-compose.yml:45, the Ollama profile
- docker-compose.dev.yml:17,
CORECRUXD_CONSOLE_DEV_PATH - docker-compose.dev.yml:33, the private
../PlanCruxmount - Dockerfile:5, the documented Chainguard exception
- .github/workflows/ci.yml:248, the console-summary smoke probe
- AGENTS.md:37, verify the claims yourself

