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_socketcheck — see "Stale state / PID / queue" below;circinus repairclears 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:
- Hooks not registered —
circinus doctor'shooks_installedcheck:not configured(never installed),unhealthy(entries recorded in the install manifest are missing fromsettings.json), or ahook contract version driftWARN (the installed manifest's hook-spec version doesn't match this binary's). Fix:circinus install claude-codeagain, orcircinus repair(which reconnects hooks from an existing manifest without re-running the full install). - Hooks registered but Claude Code itself may not match what Circinus
validated against —
circinus status'scompatibility:section reports the detectedclaude --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_unsupportedonly warns if the contract probe also fails.
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— noOPENAI_API_KEYset; the deterministic (local-only) analyzer is in use. Nothing to fix unless you want semantic analysis.WARN,configured but disabled—OPENAI_API_KEYis set, butCIRCINUS_ANALYSIS_SEND_CONTENTis not"true", so no content is sent to the provider. This is the safe default, not a bug: BYOK means disclosure is opt-in. SetCIRCINUS_ANALYSIS_SEND_CONTENT=trueonly 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 remediationcircinus repair, thencircinus start.analysis_queue_health— any task stuckqueued/leasedfor over 15 minutes. WARN, with the most common cause being a stopped daemon; checkcircinus statusand restart withcircinus 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--verboseif 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'sprivacy_permissionscheck 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 --purgedeletes 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_integrityFAIL 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.