Operations · 8. Troubleshooting
Start with the symptom you can see, not the subsystem you suspect. This chapter is a how-to. Each entry gives the check that discriminates, the cause, and the fix. They are ordered by how often each actually happens.
Two commands answer more incidents than everything else here combined:
df -h /path/to/data_dir
curl -s http://127.0.0.1:14800/readyz
Run both before forming a theory.
8.1 The daemon will not start
It exits immediately with a message and no log lines
Check: read the message on stderr. That is the whole diagnosis.
Cause: you are in the pre-tracing part of boot. Nothing has been logged yet, by design.
Fix: match the message against the 25 refusal conditions in chapter 4 of the internals set §4.3. Each row carries the exact operator fix.
CORECRUXD_AUTH_MODE must be set explicitly
Cause: there is no default. This is intentional.
Fix: set the variable, or set daemon.auth_mode in the config file. If you believe you set it in a file, read the next entry; that failure is silent.
Configuration in a YAML file appears to be ignored
Check: set CORECRUXD_CONFIG_PATH explicitly and validate the YAML separately.
Cause: three possibilities, all silent. XDG_CONFIG_HOME is unset so no file is read at all, and there is no ~/.config fallback. Or the file has a syntax error and the parse failure is discarded. Or the path is wrong, which looks identical to no file.
Fix: name the path explicitly and check the YAML parses.
auth mode Off may not bind to non-loopback addresses…
Cause: the bind-posture rail. A dev auth mode cannot bind a non-loopback address.
Fix: switch to a JWT mode, keep the bind on loopback, or set CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1 knowingly. A stock Helm install trips this, see chapter 16 B2.
A boolean flag you set has no effect
Cause: nine incompatible parsing rules across the flag surface, and six flags do not trim whitespace. A trailing newline from a systemd EnvironmentFile= or a Kubernetes configMapKeyRef reads as off.
Fix: use =1 and =0. They work under every rule.
A command-line flag does nothing
Cause: corecruxd accepts no runtime configuration flags and silently ignores unrecognised ones. corecruxd --data-dir /x starts normally and ignores it.
Fix: use environment variables.
8.2 /readyz returns 503
The body names a gate
Check: read checks[]. Nine gates exist (health.rs:276).
| Failing gate | Cause | Fix |
|---|---|---|
data_dir_capacity | Free ratio below the emergency threshold, 0.10 by default | Free disk. See §8.3 |
data_dir_lock_held | The lock is not held | Another daemon holds it, or the path changed |
routing_loaded | The shard map did not load | Check meta/routing/ |
replicated_commit_dataplane | ReplicatedCommit selected on a build with no dataplane | This gate cannot pass in this edition. Do not select that mode |
replicated_commit_topology | Replication topology invalid | Same |
read_retry_failed_threshold | Read retries past threshold | Storage-level problem |
projection_snapshots_valid | A projection snapshot is invalid | The response samples the offending ids |
corruption_state_clear | corecrux_segment_corrupt_total incremented | Requires operator clearance after investigation |
control_evidence_ok | The CONTROL.json evidence check failed | Read CONTROL.json |
Everything unrelated is failing and the error is unhelpful
Check: df -h on the data partition. Do this first, always.
Cause: the data_dir_capacity gate takes an otherwise-healthy daemon out of rotation below 10% free. Downstream tooling reports that as a bare timeout with empty stderr, every test failing at once, across unrelated changes, with no message.
Fix: free space. Reap shards/*/quarantine/ and observations/*.jsonl; neither has any GC path.
The failure body has no per-gate breakdown
Cause: CORECRUXD_PUBLIC_PROBES_MINIMAL=1 strips it, along with routing and valve detail from /healthz.
Fix: unset it, or read the gate from metrics instead. Note the flag does not cover /metrics.
8.3 Storage and growth
Ingest is paused and you did not pause it
Check: read CONTROL.json, field valves.pauseIngest.actor.
Cause: capacity_guard means the background guard paused ingest on disk pressure.
Fix: free space. It resumes at CORECRUXD_CAPACITY_RESUME_FREE_RATIO (config.rs:1112). It will not stomp an operator-set pause.
facts.jsonl is growing unexpectedly
Check: Memory › Facts. Compare visible against stored, then turn the reserved __\* toggle on and look at the session-binding prefix.
Cause: the most common one is a stateless MCP bridge re-initialising on every poll, each initialisation writing one durable session-binding fact. Unbounded without the ephemeral GC.
Fix: CORECRUXD_EPHEMERAL_GC=1, then restart; it is read once at boot. It caps session bindings at 32 per passport and drops reverify receipts older than 30 days, and it never touches user facts (ephemeral_gc.rs:23).
Something is growing and nothing reclaims it
Cause: six artefacts have no automatic reclamation path: observation journals, shard quarantine directories, the routing shard-map history, the integrations audit journal, the activity journal and the cost report journal.
Fix: CORECRUXD_OBS_RETENTION_DAYS archives observation sessions hourly, unset means retain forever. The rest are manual. See chapter 17 §17.9.
8.4 The console
The console is blank: no rail, no content
Check, in order:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:14800/console
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:14800/console-v2/api.js
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:14800/console-v2/render.js
| Result | Cause | Fix |
|---|---|---|
/console 404 | CORECRUXD_CONSOLE_ENABLED=0; it removes every console route (console.rs:294) | Unset it |
/console 200, a module 404 | The asset name is not one of the seven served (console.rs:109) | A proxy is rewriting paths |
| Everything 200, page still blank | A script threw. Open the browser console | The renderer prints Renderer module unavailable when a module is missing |
| Blank only on one origin | The CORS allowlist | See the next entry |
The console loads no external resource of any kind. If your browser reports a blocked external request, something in front of the daemon is injecting it.
The console works on one host and not another
Check: CORECRUXD_CONSOLE_ALLOWED_ORIGINS.
Cause: the console asset routes use an explicit allowlist, never a wildcard. Unset, three defaults apply: https://crux.cuecrux.com, http://100.70.12.73, http://crux (console.rs:60).
Fix: set it to your real origin, comma-separated. Entries that are not a real http(s)://host are dropped, including * and null, and if nothing usable survives, the defaults apply (console.rs:248).
A red banner: "Your session has expired"
Cause: an origin-wide session gate bounced the boot read to a sign-in page, or returned 401/403 (shell.html:4864).
Fix: click Sign in, the link carries a return URL back to the exact view.
Caveat: detection runs once, at boot. A session that expires mid-visit is not caught until the next load, and presents as panels quietly failing.
The status pill says Degraded
Cause: the daemon answered but GET /v1/console/summary returned no usable data. Almost always a scope problem; your identity lacks console:read.
Fix: check the token's scopes. Navigation still works; only the live panels are empty.
Controls I expected are missing
Check: the account badge at the bottom of the rail. It reads Operator or Customer.
Cause: the posture probe. Operator requires either auth_mode: off or a 200 from GET /v1/admin/version. Anything else, including a probe that fails, times out, or is blocked by a proxy, is customer, and every mutating control is hidden (render.js:62).
Fix: grant the admin scope. Note that a reverse proxy which strips or rejects that route will silently demote you.
A whole destination is missing from the rail
Check: System › Settings, the Runtime capabilities card.
Cause: the destination is capability-gated and the daemon's plan does not report it available. Link graph is the shipped example.
Fix: configure the upstream, for Link graph, CORECRUXD_CORECRUX_GRAPH_BASE_URL (http/console.rs:761). The validation is strict: a malformed or incomplete descriptor resolves to unavailable with a reason code, so read the reason (render.js:531).
A page says "disabled: set CORECRUXD_…"
Cause: a feature flag is off. This is the console reporting accurately, not failing.
Fix: the flag is named in the message. Defaults are tabulated in chapter 1 §1.6.
A write button refuses with an Article 14 message
Cause: no bound passport. Every gated write requires one (render.js:648).
Fix: Trust › Pending mints, the Bound approver panel. Choose a passport and click Bind. The binding is browser-local, stored under crux-console-bound-passport.
A button is visible but permanently disabled
Cause: it is one of the controls gated for a stated, grounded reason, not a milestone. The reason is in its tooltip.
| Control | Reason |
|---|---|
| Add repo, Set as planning repo | No project id and no real repository, no groundable request body (pages.js:1668) |
| Queue ingest | The only real route is a synchronous ingest needing a documents[] payload; shape mismatch |
| Install (Extensions) | The route wants a full manifest object; the form supplies a URL. Installing from the signed catalogue by id is wired, in Studio › Library |
| Apply defaults to all tenants | No bulk route; "all" would be an unbounded client loop |
| Run sweep now | No HTTP route, an MCP dry-run tool plus a background timer |
| Export audit bundle | The real route is a read needing a session id and CORECRUXD_OBSERVE |
| Send (Raw JSON-RPC) | It would dispatch an arbitrary MCP tools/call, an arbitrary-write surface. Permanent |
| Every Settings select and toggle | read-only in the console, set on the daemon host (pages.js:792) |
The Rings view shows plans I do not recognise
Check: the data-src attribute on the rings root. live means real; absent means snapshot.
Cause: the ring only swaps its embedded snapshot for your board when the feed returns at least 50 ExecPlan items with provenance timestamps in in_progress, complete or blocked (render.js:15119).
Fix: none needed; it is a deliberate fallback so the page is never empty. Below the threshold, read the work board instead.
The ExecPlan board is empty but plans exist
Cause: CRUX_EXECPLANS_ROOT is not set in the daemon's process environment. Without it the read-time projection has no files to read, and /v1/work returns only the kanban table (work_execplans.rs:58).
Fix: set it on the daemon, not in your shell, and restart.
Meters is empty
Two distinct empty states, and the page distinguishes them:
| Message | Cause | Fix |
|---|---|---|
cost lens off or unreachable | CORECRUXD_FEATURE_COST_LENS off | Enable it |
/v1/cost/report: reachable, empty | Flag on, nothing posted | Run corecruxctl session cost --post |
The Activity log is empty
Cause: CORECRUXD_FEATURE_ACTIVITY_LOG off, a warning banner names it (render.js:12026).
Fix: enable it. The flag gates both GET /v1/activity and the live stream.
"Go live" shows "Live (reconnecting)"
Cause: the EventSource on /v1/events/stream dropped. Usually a proxy buffering or timing out server-sent events.
Fix: configure the proxy to pass SSE unbuffered. The backfill and Reload paths are unaffected.
Studio › Library is empty
Cause: the daemon caches the catalogue index; nothing fetches it automatically. There is deliberately no refresh button, because there is no fetch route.
Fix: corecruxctl studio sync (render.js:8691).
8.5 Data and identity
A tenant vanished
Check, in order:
- Memory › Memory, is hide system tenants on? Tenants prefixed
__are hidden by default (pages.js:218). - Memory › Facts, turn reserved __\* on and search the tenant id.
- Memory › Facts, set as of to a time before it disappeared. If it is there, the facts were retired, not lost.
- Turn superseded on. A consolidation supersedes target facts; they stop resolving but remain in the store.
- Confirm you are on the same data directory. The default
data_diris relative, so two starts from different working directories give two divergent stores.
The most likely cause is (1) or (5). A tenant does not disappear from the store on its own.
The daemon reports a different identity and receipts stop verifying
Cause: passport.key is missing and a new one was generated silently.
Fix: there is none. It cannot be recovered. It also encrypts stored integration credentials through a derived subkey, so those are gone too. This is why chapter 9 exists.
A second daemon started against the same data
Cause: the LOCK flock is per resolved path. Two starts from different working directories against the default relative data_dir do not collide; they diverge.
Fix: always set an absolute CORECRUXD_DATA_DIR.
A repo scan is marked failed: "daemon restarted before scan completed"
Cause: expected restart recovery, not corruption.
Fix: re-run the scan.
8.6 Network and API
An MCP client cannot connect after the session handshake
Cause: channels.mcp from POST /session points at port 14800, where no /mcp route exists.
Fix: use port 14801. See chapter 16 B3.
A gRPC call returns unimplemented
Cause: every registered RPC does, in every build from this repository.
Fix: use HTTP. See chapter 16 B5.
POST /v1/admin/append returns a platform-upgrade response
Cause: it always does. The dataplane pool is hard-coded to None. It is not a subscription problem and no flag enables it.
Fix: use the fact and local-ingest surfaces.
A route you expected to be blocked is not
Cause: route-auth defaults to shadow mode; it logs route_auth_shadow_mismatch and continues.
Fix: CORECRUXD_ROUTE_AUTH=enforce. Expect console routes to start returning 403 when you do, and soak it first.
8.7 Logging
Logs are not JSON despite setting CORECRUX_LOG_FORMAT=json
Cause: no code reads that name. Every shipped manifest sets it anyway.
Fix: LOG_FORMAT=json.
Log level changes have no effect
Cause: RUST_LOG overrides CORECRUXD_LOG_LEVEL entirely when set.
Fix: unset RUST_LOG, or set the level there.
8.8 When you have fixed it
Two things, both cheap.
Do not retry blindly. Read the logs, journalctl -u corecruxd, or the container logs, before the next attempt. A retry against a full disk produces a second identical failure and no information.
Record it. A one-paragraph incident note with the symptom, the cause, the fixing change and the reproduction steps is the difference between an incident you solve once and one you solve every quarter.
Sources
- crates/corecruxd/src/http/health.rs:276, the nine readiness gates
- crates/corecruxd/src/console.rs:294, the console routes and their enable flag
- crates/corecruxd/src/console.rs:248, the CORS allowlist parser
- crates/corecruxd/src/config.rs:1112, the capacity resume ratio
- crates/corecruxd/src/work_execplans.rs:58,
CRUX_EXECPLANS_ROOT - crates/corecruxd/console/v2/render.js:62, the posture derivation
- crates/corecruxd/console/v2/render.js:531, the fail-closed capability validation
- crates/corecruxd/console/v2/render.js:15119, the ring's 50-item live threshold
- crates/corecruxd/console/v2/pages.js:1667, the grounded reason for each permanently-gated control

