Config System
Sanctum follows a single-source-of-truth principle: one YAML file defines the entire instance. Every service, script, dashboard, and LaunchAgent reads its configuration from this file — either directly or through generated artifacts. Secrets are stored separately in macOS Keychain and never appear in configuration files.

This is either an act of elegant simplicity or a magnificent dare. One file. One indentation error away from a very quiet haus.
Configuration Hierarchy
Section titled “Configuration Hierarchy”Everything flows down from that YAML file at the top. If you’re looking for the single point of failure, you found it. If you’re looking for the single source of truth, you also found it. Schrodinger’s architecture decision: genius and catastrophe in the same file, and you won’t know which until you open it.
instance.yaml
Section titled “instance.yaml”The central configuration file lives at ~/.sanctum/instance.yaml. It contains every instance-specific value: network addresses, service ports, node definitions, feature flags, and integration settings.
instance: name: Manoir Neo slug: manoir-neo timezone: America/New_York
network: mac_bridge_ip: 10.0.0.1 vm_ip: 10.0.0.10 mac_lan_ip: 192.0.2.10 bridge_subnet: 10.0.0.0/24 lan_subnet: 192.0.2.0/24
services: home_assistant: port: 8123 homekit_port: 21063 dashboard: port: 3333 backend_port: 1111 council_mlx: enabled: true port: 1337 # sanctum-mlx serving Qwen3.6-35B-A3B-4bit (mTLS) model_path: ~/Projects/mlx-finetune/models/Qwen3.6-35B-A3B-4bit-text voice_agent: enabled: true port: 1138 # ... additional services
# The coder seat (Codestral-22B at :3301) is NOT a services.* block --# it's a router backend. The router owns where prompts get sent:router: backends: coder: url: http://127.0.0.1:3301/v1 # sanctum-mlx-codestral, plain HTTP default_model: mlx-community/Codestral-22B-v0.1-4bit
nodes: hub: type: hub host: 192.0.2.10 tailscale_ip: 100.0.0.20 ssh_user: operator services: [home_assistant, dashboard, voice_agent, council_mlx] satellite: type: satellite host: null # set during on-site install tailscale_ip: 100.0.0.30 ssh_user: operator services: [home_assistant]JSON Cache
Section titled “JSON Cache”The JSON cache at ~/.sanctum/.instance.json is an auto-generated derivative of the YAML file. It exists so that shell scripts and lightweight tools can parse configuration without a YAML library.
# Regenerate the cache manually (normally automatic)python3 ~/.sanctum/lib/yaml2json.pyThe cache is regenerated automatically whenever configuration libraries detect the YAML file is newer than the JSON. Never edit .instance.json by hand — changes will be overwritten. This is not a suggestion. The file is named with a dot prefix for a reason. It is trying to hide from you.
Shell Library
Section titled “Shell Library”Source config.sh in any Bash script to get access to configuration values:
source ~/.sanctum/lib/config.shAvailable Functions
Section titled “Available Functions”| Function | Description | Example |
|---|---|---|
sanctum_get <path> | Read a value by dotted path | sanctum_get services.voice_agent.port |
sanctum_slug | Return the instance slug | manoir-neo |
sanctum_home | Return the Mac home directory | /Users/neo |
sanctum_vm_ssh | Return the VM SSH target | ubuntu@10.0.0.10 |
sanctum_enabled <service> | Check if a service is enabled | sanctum_enabled voice_agent |
sanctum_expand <template> | Expand {{PLACEHOLDER}} strings | See below |
sanctum_whoami | Return this node’s identity | hub |
sanctum_node_ts_ip <node> | Read a typed field from a node | sanctum_node_ts_ip satellite |
There’s a typed helper for each node field that gets read often — sanctum_node_type, sanctum_node_host, sanctum_node_user, sanctum_node_ts_ip — so you’re not reaching for sanctum_get nodes.X.field every time. They’re thin wrappers around exactly that, named so the call site reads like a sentence.
Usage Example
Section titled “Usage Example”#!/bin/bashsource ~/.sanctum/lib/config.sh
if sanctum_enabled voice_agent; then PORT=$(sanctum_get services.voice_agent.port) echo "Voice agent running on port $PORT"fi
VM=$(sanctum_vm_ssh)ssh "$VM" 'systemctl --user status openclaw-gateway'TypeScript Library
Section titled “TypeScript Library”The TypeScript library provides the same capabilities for Node.js services like the command center dashboard and gateway plugins.
import { get, isEnabled, expand, vmSsh, whoami, nodeGet, getNodesByType } from './lib/config';Available Functions
Section titled “Available Functions”| Function | Description | Return Type |
|---|---|---|
get(path) | Read a value by dotted path | string | number | boolean | object |
isEnabled(service) | Check if a service is enabled | boolean |
expand(template) | Replace {{PLACEHOLDER}} tokens | string |
vmSsh() | Return the VM SSH connection string | string |
whoami() | Return this node’s identity | string |
nodeGet(node, field) | Read a value from a specific node | T (defaults to string) |
getNodesByType(type) | List nodes of a given type | Array<{ id: string } & object> |
Usage Example
Section titled “Usage Example”import { get, isEnabled } from './lib/config';
const port = get('services.dashboard.port') as number; // 3333
if (isEnabled('home_assistant')) { const haPort = get('services.home_assistant.port'); // 8123 console.log(`HA available at http://localhost:${haPort}`);}Plist Templates
Section titled “Plist Templates”LaunchAgent plist files are generated from templates rather than hand-maintained. This ensures every plist stays in sync with instance.yaml and that secrets are injected at generation time from macOS Keychain.
If you’ve ever manually edited 11 plist files, changed a port number, missed one, and then spent 45 minutes watching logs from the wrong service — this system exists because of you. Well. Because of us.
Templates live in ~/.sanctum/templates/launchagents/ and use a {{PLACEHOLDER}} syntax:
<!-- Template: com.sanctum.council-mlx.plist --><?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>Label</key> <string>com.sanctum.council-mlx</string> <key>ProgramArguments</key> <array> <string>{{MAC_HOME}}/.sanctum/bin/sanctum-council-mlx</string> <string>--model</string> <string>{{MLX_MODEL_PATH}}</string> <string>--host</string> <string>{{MAC_BRIDGE_IP}}</string> <string>--port</string> <string>{{MLX_PORT}}</string> </array> <key>StandardOutPath</key> <string>{{LOGS_DIR}}/mlx-server.log</string> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <true/></dict></plist>Generating Plists
Section titled “Generating Plists”The generate-plists.sh script renders all templates:
- Reads
instance.yamlfor service ports, paths, and feature flags. - Pulls tokens from macOS Keychain (e.g.,
GATEWAY_TOKEN). - Skips templates for disabled services (
enabled: false). - Writes rendered plists to
~/Library/LaunchAgents/.
# Preview what would be generated~/.sanctum/generate-plists.sh --dry-run
# Generate and install all plists~/.sanctum/generate-plists.shSecrets Management
Section titled “Secrets Management”Secrets never appear in instance.yaml or any configuration file. They live in two stores, split by what they protect: per-service tokens in the Keychain, the gateway config under SOPS.
Tokens and API keys are stored in the macOS login Keychain. The plist generator and shell scripts retrieve them using the security command:
security find-generic-password -a "sanctum" -s "gateway-token" -wSecrets are rotated monthly by the com.sanctum.rotate-secrets LaunchAgent, which runs on the 1st of each month at 3:30 AM. Nothing like waking up to fresh tokens you didn’t have to think about.
The OpenClaw gateway config is encrypted at rest with SOPS and age. The encrypted file is the source of truth and lives in git; the plaintext is regenerated on every launch and never committed:
# Encrypted source of truth (git-tracked)~/.openclaw/openclaw.json.enc
# Decrypt wrapper, wired into ai.openclaw.gateway as ProgramArguments[0]~/.openclaw/scripts/sops-start.sh
# age private key (never leaves the host)~/.config/sops/age/keys.txtAt launch, sops-start.sh decrypts openclaw.json.enc into openclaw.json (mode 0600, gitignored), then execs SanctumBridge. One guardrail worth knowing: if you edit openclaw.json directly and it’s newer than the encrypted source, the wrapper skips the decrypt rather than clobber your edits — and quietly reminds you to re-encrypt. The plaintext is disposable; the .enc is the one you protect.
Adding a New Service
Section titled “Adding a New Service”To add a new service to the configuration layer:
- Add a
services.<name>block toinstance.yamlwith at leastenabledandport. - If the service needs a LaunchAgent, create a template in
templates/launchagents/. - If the service needs secrets, store them in Keychain and reference them as
{{PLACEHOLDER}}in the template. - Run
generate-plists.shto render the new plist. - Add the service to the appropriate node’s
serviceslist ininstance.yaml. - Update the watchdog configuration if the service should be health-checked.