Skip to main content

Troubleshooting

Every entry here reflects a real check Circinus runs, a real command, or a real defect found and fixed during MVP hardening — not a speculative FAQ.

Start with circinus doctor (or circinus config check for the config-relevant subset) before reading further — it runs the same checks this guide references and states impact plus a remediation for every WARN/FAIL line.

uv run circinus doctor # human-readable PASS/WARN/FAIL + remediation
uv run circinus doctor --json # same checks, stable machine-readable schema

Exit codes: 0 everything passed, 1 only warnings, 2 at least one failure.

Daemon / socket not reachable​

circinus doctor's daemon_reachable check pings the unix socket the daemon listens on. WARN: not running (or unreachable) at <path> means either the daemon was never started, or it crashed.

  • Start it: circinus start.
  • A stale socket file (present on disk but nothing answers on it) is reported separately by the stale_pid_socket check — see "Stale state / PID / queue" below; circinus repair clears it.

The enforcement hot path never depends on the daemon being reachable for a decision — hooks fail open (shadow mode) or closed (enforce mode) explicitly and log the gap rather than staying silent — but a down daemon does mean nothing is being ingested or analyzed while it is down.

Hooks not firing / Claude Code compatibility warning​

Two separate things can be wrong, and circinus doctor / circinus status distinguish them:

  1. Hooks not registered — circinus doctor's hooks_installed check: not configured (never installed), unhealthy (entries recorded in the install manifest are missing from settings.json), or a hook contract version drift WARN (the installed manifest's hook-spec version doesn't match this binary's). Fix: circinus install claude-code again, or circinus repair (which reconnects hooks from an existing manifest without re-running the full install).
  2. Hooks registered but Claude Code itself may not match what Circinus validated against — circinus status's compatibility: section reports the detected claude --version, classifies it against the last-validated baseline (known_compatible / newer_unknown / older_unsupported / not_detected), whether the static payload-contract probe passed, and per-event hook connectivity. A warning here does not necessarily mean anything is broken — newer_unknown/older_unsupported only warns if the contract probe also fails.
What Circinus can and cannot see here, stated plainly

It can tell you whether hooks are configured correctly in settings.json. It cannot tell you whether Claude Code silently drops a hook invocation before it ever reaches the circinus hook process — a config-correct-but-non-firing host looks identical to a healthy one until an event is actually observed. If circinus status/doctor report everything healthy but you see no new decisions after real tool calls, that silent-drop case — not a Circinus bug your own diagnosis can surface — is the remaining possibility.

Prerequisite this misdiagnoses as "hooks not firing": circinus not on PATH​

The hook commands Circinus writes into settings.json are the bare word circinus (e.g. circinus hook pre-tool-use), not uv run circinus .... uv run only puts circinus on PATH for the duration of the command you ran it with — not for the separate process Claude Code spawns later to fire the hook. Confirm with sh -c "circinus hook pre-tool-use" in the same shell environment Claude Code's hook subprocess inherits; if that fails, install the entrypoint on PATH (e.g. uv tool install --editable ., or an activated .venv) before relying on hooks — hooks_installed checks that the settings.json entries exist, not that the circinus command they invoke actually resolves.

Semantic provider configured but not used / disclosure gate disabled​

circinus doctor's semantic_provider_configured check has three states, and "configured but disabled" is the common one to be surprised by:

  • UNSUPPORTED — no OPENAI_API_KEY set; the deterministic (local-only) analyzer is in use. Nothing to fix unless you want semantic analysis.
  • WARN, configured but disabled — OPENAI_API_KEY is set, but CIRCINUS_ANALYSIS_SEND_CONTENT is not "true", so no content is sent to the provider. This is the safe default, not a bug: BYOK means disclosure is opt-in. Set CIRCINUS_ANALYSIS_SEND_CONTENT=true only if you deliberately intend to send content to OpenAI.
  • PASS — provider configured and disclosure enabled.

The inverse misconfiguration is caught by a separate check, configuration_conflicts: CIRCINUS_ANALYSIS_SEND_CONTENT=true set with no OPENAI_API_KEY present has no effect — WARN, remediation is to set the key or unset the flag. The same check also WARNs when either flag is set to a non-empty value that isn't a recognized boolean spelling (only "true" / "false", case-sensitive) — a malformed value always resolves to the safe/off state, the WARN just makes the typo visible instead of it silently not taking effect.

circinus config show prints both settings under analysis.* — a credential-shaped value like OPENAI_API_KEY is always rendered as configured/missing, never as its actual value.

Wait-budget expired / provider failure​

An irreversible action whose relevant artifact's verdict is not ready yet does not wait forever — it waits up to the active security profile's bounded budget: realtime 0 ms (never waits), balanced 150 ms, strict/critical 500 ms. If evidence still is not ready when the budget expires, circinus explain shows rule_id: wait_budget_expired. Every profile resolves this the same explicit way every time — review for realtime/balanced/strict, block for critical (the only profile that fails closed without asking a human). This is expected behavior under a slow or unreachable analysis provider, not a bug — pending evidence is never silently treated as permission.

A provider call failure is a separate, narrower thing from a slow provider: the orchestrator retries only a transient failure (timeout, connection error, 429/5xx) and never retries a permanent one (an invalid or revoked API key) — a permanent provider failure fails fast without wasting the retry budget, and circinus explain shows the resulting decision was made without deep evidence rather than hanging.

Stale state / PID / queue​

circinus doctor runs two checks for this:

  • stale_pid_socket — a pidfile naming a process that is no longer running, an unreadable pidfile, or a socket file present but unreachable. WARN with remediation circinus repair, then circinus start.
  • analysis_queue_health — any task stuck queued/leased for over 15 minutes. WARN, with the most common cause being a stopped daemon; check circinus status and restart with circinus start.

circinus repair (optionally --dry-run to preview) fixes only what is narrow, deterministic, and reversible: a stale pidfile, a stale daemon socket, or hooks that fell out of settings.json while the install manifest still exists. It deliberately never touches the provenance database and never changes mode — a corrupt or behind-schema database is diagnosed by doctor (db_integrity, db_schema_version), never silently rewritten by repair.

circinus explain / decision lookup​

circinus explain --artifact <id> | --decision <id>|latest [--verbose] [--json] reconstructs one decision's source → artifact → transform → evidence → decision chain in plain language. circinus explain recent [--limit N] lists recent decisions to pick an id from. See the CLI Reference for example output.

Two things worth knowing before you go looking for a decision that "isn't there":

  • Every analysis finding is durably recorded in the local SQLite store and survives a daemon or process restart — re-running circinus explain --decision <id> after a restart reconstructs the identical decision from disk, it does not need the daemon to still be up.
  • Without --verbose, a long argument value is shown truncated by design (Circinus never prints a full shell command, path, or other raw argument text by default) — use --verbose if you need the untruncated value, not a sign that the value was lost.

Install / upgrade / uninstall / reinstall​

uv run circinus install claude-code # register hooks (project scope by default)
uv run circinus install claude-code --scope user # ~/.claude/settings.json instead
uv run circinus install claude-code --print # preview the change, write nothing
uv run circinus uninstall claude-code # remove only Circinus's own hook entries
uv run circinus uninstall claude-code --purge # also delete the local state directory

Install and uninstall are both idempotent — re-running either is safe and touches only the entries/state Circinus itself owns, preserving every other key already in settings.json. --purge refuses to run while the daemon is still reachable, precisely so it can never delete a database a live process has open; stop the daemon first (circinus stop).

Local state location and safe backup / purge​

Durable local state (the provenance database, install manifest, logs) lives under the state directory shown by circinus doctor's state_dir_access check and circinus config show's paths.state_dir/paths.runtime_dir rows. It defaults to a per-machine location (~/.local/state/circinus on the platforms Circinus supports), not per-project — concurrent or sibling installs on the same machine share one daemon/database/manifest by default; set XDG_STATE_HOME before running any circinus command to isolate a checkout.

  • Backup: the state directory is a plain filesystem tree — stop the daemon first (circinus stop), then copy it. circinus doctor's privacy_permissions check confirms the directory is private to the current user (WARN + chmod 700 <dir> remediation if not) — a database containing local provenance data.
  • Purge: circinus uninstall claude-code --purge deletes the whole state directory. It refuses while the daemon is reachable (see above), which is what makes it safe to run without first double-checking nothing is using the database.
  • Circinus never auto-repairs a corrupt database (db_integrity FAIL remediation is explicitly "restore from a backup") — an automated repair that edits a corrupt database to make a check go green would risk silently discarding evidence.

Collecting a privacy-safe diagnostic bundle​

There is no separate diagnostics subsystem to install or run — circinus doctor --json is the safe diagnostic bundle mechanism, by design:

uv run circinus doctor --json > circinus-doctor.json

Every check in the registry reports only non-sensitive metadata (booleans, counts, versions, paths, PASS/WARN/FAIL/UNSUPPORTED status) — never a secret, a raw prompt, tool arguments, or protected artifact content. A credential-shaped setting is always summarized as configured/missing; circinus config show --json/circinus config diff --json follow the same rule and are safe to attach alongside the doctor output for a config-focused report.

If you also need to show why a decision was made, circinus explain --decision <id> --json is safe to attach without --verbose — the default view never includes a full raw argument value, raw artifact content, or secrets; only add --verbose output to a bundle you are sharing if you have reviewed it yourself first, since --verbose intentionally reveals the untruncated argument values it otherwise withholds.

Do not attach the provenance database itself or its --verbose explain output to a bug report by default — those can contain content Circinus observed from your own tool calls, which is exactly the boundary the checks above are designed to stay on the safe side of.