Skip to content

sanctum-cli config (the cli block)

sanctum-cli config — five intricate hand-forged brass keys on a single ring with a worn leather strap, each key uniquely cut, an amber halo from above. One ring, one block, every key earning its place.

Open ~/.sanctum/instance.yaml expecting the usual config sprawl and the file is almost rude about how little it asks of you: two required keys and a lot of white space. That one file (instance.yaml) describes the entire constellation — networking, node topology, every service — but the sanctum CLI only ever reads two slices of it. The loader pulls out the instance: and cli: keys, validates those, and leaves every other tool to own the rest without complaint. This page is the second slice: cli:, the block the CLI itself reads.

The block is optional, which is the point. A fresh install runs entirely on defaults; you add a cli: block only when you want to steer routing, pick a provider, or point backups somewhere. sanctum onboard scaffolds a minimal instance.yaml (just instance: { name, slug }) when none exists, then walks its numbered steps — it never opens an editor. And when a key is wrong, sanctum config validate names the offender out loud, before anything downstream gets the chance to fail more interestingly. The whole design assumes you will get it wrong at least once; it just refuses to be quiet about it.

The smallest input the CLI accepts is just the instance: metadata — no cli: block at all, every setting on its default:

instance:
name: "Manoir Sanctum"
slug: "manoir-sanctum"
timezone: "America/Montreal"

Only instance.name and instance.slug are required; timezone is optional. Everything else in this reference is a key you add under cli: to override a default. Skip the cli: block entirely and you get Claude routing, telemetry on, and the built-in family and operator backup recipes — which, after watching one long cloud outage in real time, is a defensible place to start.


The CLI pulls three fields out of the instance: block; the rest of that block (users, network, services, nodes) belongs to other tools and the CLI ignores it without complaint. slug is the URL-safe identifier; name shows up in sanctum status and alerts.

KeyTypeRequiredDescription
namestringYesHuman-readable instance name. Surfaced in sanctum status and alert subjects.
slugstringYesURL-safe identifier. Lowercase, no spaces — used in paths and hostnames.
timezonestringNoIANA zone, e.g. America/Montreal. Used for schedule and report timestamps.
instance:
name: "Manoir Sanctum"
slug: "manoir-sanctum"
timezone: "America/Montreal"

Which model seat answers by default, and the rules that override it. default_provider is the floor; routing.rules are the ordered exceptions (first match wins); routing.fallback catches what no rule matched or what is unreachable.

KeyTypeDefaultDescription
default_providerclaude | gemini | mlx_localclaudeProvider used when no routing rule matches.
routing.ruleslist[]Ordered when/then rules. First match wins.
routing.fallbackclaude | gemini | mlx_localclaudeProvider used if no rule matched or the primary is down.
cli:
default_provider: claude
routing:
rules:
- when: { offline: true }
then: mlx_local
fallback: claude

A then: value names a seatclaude, gemini, or mlx_local — not a specific model. Which model sits behind each seat lives in the provider block below, so you can swap the model without rewriting every rule. It is the same seat vocabulary the Smart Router speaks one layer up.


The three model backends, each with its own endpoint, model, and — where it talks to a paid API — a Keychain pointer. Secrets never live in this file; only a {service, account} tuple that the CLI resolves at runtime.

ProviderKeyDefaultDescription
claudeviaproxyproxy shells through the local claude CLI (bills the Max subscription); direct uses the Anthropic SDK (bills the API key).
claudeendpointhttp://127.0.0.1:2001Local OpenAI-compatible proxy address for via: proxy.
claudekeychain{service: anthropic-api-key, account: sanctum}Keychain entry for via: direct.
geminimodelgemini-2.5-proGoogle model; key at {service: google-ai-api-key, account: sanctum}.
mlx_localendpointhttp://127.0.0.1:8900Local cathedral. always_available: true — no key, never the bottleneck.
cli:
providers:
claude:
via: proxy
keychain: { service: "anthropic-api-key", account: "sanctum" }
mlx_local:
endpoint: "http://127.0.0.1:8900"
model: "council-secure"

The canonical Keychain lookup carries both halves of the tuple — account and service. The CLI shells exactly this (sanctum_cli/keychain.py):

Terminal window
security find-generic-password -a sanctum -s anthropic-api-key -w

A local, append-only CLI usage log. On by default, prompts redacted by default, never leaves the machine.

KeyTypeDefaultDescription
enabledbooltrueWrite a JSONL line per invocation.
pathstring~/.sanctum/telemetry/cli.jsonlWhere the log lands.
redact_promptsbooltrueStrip prompt text before writing.
aggregate_window_daysint7Rolling window for status aggregates (1–365).

Restic/borg/kopia repo pointers for sanctum backup: a primary repo, an optional secondary, and a retention policy. This is the one cli: sub-block configured on this machine; everything else runs on defaults.

KeyTypeDefaultDescription
primary.kindrestic | borg | kopiaresticBackup engine.
primary.repostringRepo path or rclone remote.
primary.keychain{service, account}Keychain entry holding the repo password.
retention.keep_dailyint7Daily snapshots to keep.
cli:
cloud_backup:
primary:
kind: restic
repo: "/Volumes/T9/sanctum-restic"
keychain: { service: "sanctum-backup-key", account: "sanctum-backup" }
secondary:
kind: restic
repo: "rclone:gdrive-sanctum:sanctum-restic"
keychain: { service: "sanctum-backup-key", account: "sanctum-backup" }
retention: { keep_daily: 7, keep_weekly: 4, keep_monthly: 12 }

Recipes are named bundles of backup sources/excludes for an audience (family, operator); ui is cosmetic. Both ship sane built-ins, so most operators never touch either.

KeyTypeDefaultDescription
recipesmapbuilt-insNamed source/exclude bundles. Override family/operator or add your own.
default_recipestringunsetRecipe used when sanctum backup is called bare.
ui.colorauto | always | neverautoColor output policy.
ui.json_defaultboolfalseEmit JSON without passing --json.

A per-module enable/disable map, keyed by module name. The only field today is enabled (YAGNI — the rest is intentionally absent). Driven by the sanctum module list / status / install / uninstall command group.

KeyTypeDefaultDescription
modules.<name>.enabledbooltrueWhether the named module is active.
cli:
modules:
backup:
enabled: false

The env vars the CLI actually honors at runtime, grepped straight from sanctum_cli/. Useful for tests, sandboxes, and one-off runs. None are needed for normal operation.

VariableOverridesNotes
SANCTUM_INSTANCE_FILEPath to instance.yamlDefault: ~/.sanctum/instance.yaml
SANCTUM_DEVICES_FILEDevices file pathRead by sanctum devices
SANCTUM_MODULES_DIRInstalled-modules directoryWhere sanctum module looks
SANCTUM_PROVIDERcli.default_providerForce a provider for one invocation
SANCTUM_PROVIDERS_CLAUDE_ENDPOINTcli.providers.claude.endpointDirect-SDK endpoint override
SANCTUM_PROVIDERS_CLAUDE_PROXY_ENDPOINTClaude proxy endpointThe via: proxy address
SANCTUM_BRIDGE_URLFirewalla bridge URLLocal bridge address
SANCTUM_PREFIXInstall prefixWhere the CLI looks for its own files
SANCTUM_SPLASHStartup splash toggleCosmetic

That is the whole cli: block: five keys on one ring, each earning its place, and not a single secret riding along on any of them — the tokens stay in Keychain, exactly as the secrets trifecta demands. When something downstream misbehaves, this file is where you start, because sanctum config validate reads it first and will tell you which key is lying. The rest of the constellation waits next door in instance.yaml; it just no longer has an opinion about how the CLI is run.