Skip to content

instance.yaml Reference

One YAML to rule them all — a single configuration file radiating control to every service in the constellation

The central configuration file for a Sanctum instance lives at ~/.sanctum/instance.yaml. Every instance-specific value is defined here — services, networking, paths, family members, node topology, and secrets references.

One file. Everything your haus knows about itself is in this file. The name, the network layout, which services run, who lives there. If this file is wrong, everything downstream is wrong. If this file is right, everything downstream has a fighting chance.

A JSON cache is auto-regenerated at ~/.sanctum/.instance.json whenever the YAML changes, via lib/yaml2json.py.

The absolute minimum to get Sanctum to acknowledge your existence:

instance:
name: Sanctum Hub
slug: sanctum-hub
timezone: America/Montreal
users:
mac:
username: neo
home: /Users/neo
vm:
username: ubuntu
home: /home/ubuntu
network:
mac_bridge_ip: 10.0.0.1
vm_ip: 10.0.0.10
mac_lan_ip: 192.0.2.10
bridge_interface: bridge100
vm_ssh_alias: openclaw

Top-level identity for this Sanctum deployment. Who you are. Where you are. When you are.

KeyTypeRequiredDescription
slugstringYesURL-safe identifier used in hostnames, paths, and DNS. Example: sanctum-hub
namestringYesHuman-readable display name. Example: Sanctum Hub
timezonestringYesIANA timezone for scheduling and logs. Example: America/Montreal
instance:
slug: sanctum-hub
name: Sanctum Hub
timezone: America/Montreal

OS-level identities on the Mac host and the VM. Unglamorous but load-bearing — every SSH command, every file path, every LaunchAgent plist resolves through one of these. Get them wrong and nothing errors cleanly; things just silently don’t work, which is worse. Each side is a map of a username and a home directory, not a bare string — sanctum_home() and sanctum_expand() read the home path straight off users.mac.home.

KeyTypeRequiredDescription
mac.usernamestringYesmacOS username on the host machine
mac.homestringYesmacOS home directory (absolute)
vm.usernamestringYesLinux username inside the VM
vm.homestringYesLinux home directory inside the VM
users:
mac:
username: neo
home: /Users/neo
vm:
username: ubuntu
home: /home/ubuntu

The plumbing. Nobody admires plumbing until it breaks, and then it’s the only thing anyone can talk about. This section defines how the Mac, the VM, and the LAN find each other — a tiny private internet inside your haus, inside your actual internet.

KeyTypeRequiredDescription
mac_bridge_ipstringYesMac-side IP on the bridge interface
vm_ipstringYesStatic IP of the VM on the host-only network
mac_lan_ipstringYesMac Mini IP on the local network
bridge_interfacestringYesmacOS bridge interface name (e.g., bridge100)
bridge_subnetstringNoCIDR for the Mac↔VM bridge network
lan_subnetstringNoCIDR for the LAN
vm_ssh_aliasstringYesSSH config alias for the VM (e.g., openclaw)
network:
mac_bridge_ip: 10.0.0.1
vm_ip: 10.0.0.10
mac_lan_ip: 192.0.2.10
bridge_interface: bridge100
bridge_subnet: 10.0.0.0/24
lan_subnet: 192.0.2.0/24
vm_ssh_alias: openclaw

Where things live on disk. Every backup script, every log rotation, every skill loader reads from this section. Think of it as the haus’s filing cabinet — except the filing cabinet is also load-bearing, so don’t move it.

KeyTypeRequiredDescription
openclaw_configstringYesAgent config directory. Default: ~/.openclaw
openclaw_skillsstringYesShared skills repo checkout
logsstringYesCentralized log directory
projectsstringYesProjects root (Mac side)
backupsstringYesBackup destination directory
paths:
openclaw_config: ~/.openclaw
openclaw_skills: ~/Projects/openclaw-skills
logs: ~/.openclaw/logs
projects: ~/Projects
backups: ~/Backups

Here’s where it gets real. Every service your haus runs — the agent gateway, the voice engine, the haus automation hub, the offline library — each one gets an enabled boolean and, if it listens on a port, a port key. Flip the boolean, regenerate plists, and a freshly enabled service gets its LaunchAgent. Configuration as incantation.

The enabled flag controls whether generate-plists.sh renders the corresponding LaunchAgent. Today a disabled service is simply skipped during generation — its plist isn’t rewritten. If it was already loaded, it keeps running until you launchctl bootout it by hand. “Disabled” means “not regenerated,” not yet “torn down.” Honest beats incantation.

ServiceKeyDefault Port(s)Description
Agent GatewaydenchOpenClaw/DenchClaw agent gateway (runs in the VM; reached on the host via the Lima-forwarded 127.0.0.1:1977)
Home Assistanthome_assistant8123Home automation hub (Docker)
Dashboarddashboard3333, 1111Command center web (3333) + API (1111); the Holocron UI on 3344 has since become the front door
Firewalla Bridgefirewalla_bridge1984Firewalla P2P bridge
VMvmUbuntu 24.04 guest under Lima
Voice Agentvoice_agent1138Yoda voice interaction agent
Qwen3-TTSqwen3_tts8008Text-to-speech server (mlx-audio backend; XTTS-v2 retired 2026-04-19)
Council MLXcouncil_mlx1337Council MLX model server (Qwen3.6-35B-A3B, mTLS)
Cloudflare TunnelcloudflareCloudflare Zero Trust tunnel
iCloud Filericloud_filerAutomatic iCloud filing daemon
Health Centerhealth_center2222Health monitoring dashboard
TailscaletailscaleMesh VPN
Memory Vaultmemory_vault42069Cross-session message store
WatchdogwatchdogService health monitoring
Kiwixkiwix8888Offline library server
Signal Proxysignal_proxy5150Signal messaging bridge (host-forwarded from the VM)
Outlineoutline3100Self-hosted wiki

Seventeen of the services are shown above. The full catalogue is 28 keys under services:. On a Mac Mini. Under a desk. Running a haushold.

services:
dench:
enabled: true
home_assistant:
port: 8123
homekit_port: 21063
dashboard:
port: 3333
backend_port: 1111
firewalla_bridge:
enabled: true
port: 1984
vm:
enabled: true
voice_agent:
enabled: true
port: 1138
qwen3_tts:
enabled: true
port: 8008
council_mlx:
enabled: true
port: 1337
cloudflare:
enabled: true
icloud_filer:
enabled: true
health_center:
enabled: true
port: 2222
tailscale:
enabled: true
memory_vault:
enabled: true
port: 42069
watchdog:
enabled: true
interval: 600
triage:
enabled: true
interval: 30
kiwix:
enabled: true
port: 8888
signal_proxy:
enabled: true
port: 5150
host: vm
outline:
enabled: true
port: 3100

From the shell library:

Terminal window
source ~/.sanctum/lib/config.sh
if sanctum_enabled voice_agent; then
echo "Voice agent is enabled on port $(sanctum_get services.voice_agent.port)"
fi

sanctum_enabled takes the bare service name — it prepends services. and appends .enabled itself. Pass it services.voice_agent and it dutifully looks up services.services.voice_agent.enabled, finds nothing, and reports disabled. sanctum_get, by contrast, wants the full dotted path. The asymmetry is a footgun; the rule of thumb is “name the service for enabled, name the path for get.”

From TypeScript:

import { isEnabled, get } from './lib/config.js';
if (isEnabled('voice_agent')) {
const port = get('services.voice_agent.port');
}

The only section of this file that’s about what isn’t here. Sanctum never stores secrets in instance.yaml directly — the config tells you where secrets live, not what they are. A treasure map that says “the chest is buried under the oak tree” without ever containing the treasure. Three secret stores, three levels of paranoia, all of them justified.

KeyTypeDescription
keychain_accountstringmacOS Keychain account name for stored tokens
op_vaultstring1Password vault name for credentials
op_item_prefixstringPrefix for 1Password item names (e.g. Sanctum - Home Assistant)
sops_filestringPath to the SOPS-encrypted secrets file
secrets:
keychain_account: sanctum
op_vault: Private
op_item_prefix: Sanctum
sops_file: ~/.openclaw/secrets.enc.yaml

Three stores, three trust models, zero plaintext on this disk. The treasure map can be committed to git. The chests cannot.


Where the smart home meets reality. This section doesn’t configure Home Assistant itself — that’s configuration.yaml’s job. This section tells Sanctum what HA needs to know about the physical world: which speakers exist, which cameras are watching, which thermostat controls which zone. The kind of inventory you never think to write down until the third time you troubleshoot from memory.

KeyTypeDescription
camerasmapCamera entity IDs keyed by friendly name
sonos_speakerslistHA media_player.* entity IDs for the Sonos fleet
hvac_zoneslistThermostat zones (id, name, temp + humidity sensors)
ecobeemapEcobee climate + sensor entity IDs
floor_zoneslistIn-floor heating zones
carrier_sensorsmapCarrier furnace/airflow sensor entity IDs
home_assistant:
cameras:
front_door: camera.front_door_live_view
sonos_speakers:
- media_player.dining_room
- media_player.living_room
- media_player.master_bedroom
hvac_zones:
- id: climate.main_floor
name: Ground Floor
temp_sensor: sensor.main_floor_temperature
humidity_sensor: sensor.main_floor_humidity
ecobee:
climate: climate.main_floor
occupancy: binary_sensor.main_floor_occupancy

Your haus should know who lives in it. This section is what lets the agents greet a kid by name, route an alert to their bedroom speaker, and know which devices are theirs when curfew lands. A short list with outsized consequences — and the screen-time enforcer reads it directly.

KeyTypeDescription
childrenlistList of family member objects
children[].namestringDisplay name
children[].sonos_speakerstringHA media_player.* entity to address this person
children[].deviceslistPer-device {mac, name, icon} entries (for presence + screen time)
children[].screen_timemapWake/block schedule (wake_hour, block_hour, school_days)
family:
children:
- name: Kid One
sonos_speaker: media_player.vr_room
devices:
- mac: AA:BB:CC:DD:EE:XX
name: Kid One's iPad
icon: phone
screen_time:
wake_hour: 8
block_hour: 21

Multi-site node topology. Each node represents a physical location running Sanctum infrastructure. Because one haus wasn’t enough — you needed a distributed system. The hub runs everything; satellites run what they can; mobile devices check in when they feel like it. It’s federation for people who own property in more than one postal code.

KeyTypeDescription
<node_id>mapStable short node identifier (e.g., hub, satellite)
.typestringNode type: hub, satellite, mobile, or sensor
.display_namestringHuman-readable label shown in the dashboard
.locationstringLogical location tag (e.g., home, chalet)
.hoststringLAN hostname or IP (empty if unreachable from the hub)
.tailscale_ipstringTailscale mesh IP
.tailscale_namestringTailscale device name
.ssh_userstringSSH username on this node (read by sanctum_node_user)
.onlineboolLast-known reachability
.vmmapHub only: nested host / ssh_user / ssh_alias for the guest
.servicesmapOptional per-node service overrides (satellites)
nodes:
hub:
type: hub
display_name: Sanctum Hub
location: home
host: 192.0.2.10
tailscale_ip: 100.0.0.20
tailscale_name: hub
ssh_user: neo
online: true
vm:
host: 10.0.0.10
ssh_user: ubuntu
ssh_alias: openclaw
satellite:
type: satellite
display_name: Chalet
location: chalet
host: ''
tailscale_ip: 100.0.0.30
tailscale_name: satellite
ssh_user: neo
online: false
TypeDescription
hubPrimary site with full infrastructure (VM, all services)
satelliteSecondary site with reduced stack (no VM, lighter services)
mobileLaptop or portable device
sensorHeadless monitoring device

The satellite knows it’s not the hub. It doesn’t try to be. It runs what it needs, phones haus over Tailscale, and keeps the lights on when the internet goes out. Self-aware infrastructure is underrated.


An annotated example is available at ~/.sanctum/instance.yaml.example and can be used as a starting point for new instances.


The YAML config is automatically converted to a JSON cache at ~/.sanctum/.instance.json — a nested object tree mirroring the YAML, which the libraries walk with dotted-path lookups (services.voice_agent.port). Nothing is flattened; the dots in the query are traversal, not key names. The shell and TypeScript helpers both read this file for fast lookups. If you edit instance.yaml manually, regenerate the cache:

Terminal window
python3 ~/.sanctum/lib/yaml2json.py