Resolve
sanctum-endpoints discovers live addresses and writes them to a generated file every service reads. The map is never hand-edited.

A hardcoded IP address is a time bomb with a long fuse. It works perfectly until the day the LAN shifts subnets, the router is swapped, or a Tailscale node re-authenticates with a new address — and then a dozen scripts fail at once, quietly, at 2 a.m., while everyone in the haus is asleep. We have been burned by this enough times that Qui-Gon, who owns infra, now treats a literal IP in code the way Windu treats a plaintext password: as a breach with a delay on it.
The doctrine is one sentence: every address a Sanctum service contacts is resolved at runtime from a single source of truth — never written as a literal in code or config. An address is only half of where a service lives; the port it answers on is the other half, and it gets the same treatment.
This is not theoretical hygiene. We paid tuition for it twice, both times in the dark, both times because a literal outlived the network it was written for.
192.168.1.x to a new subnet. Screen-time enforcement contacted a hardcoded gateway and silently went offline for 24+ hours. Nobody noticed until a kid did.10.0.0.0/24. Overnight the Mac Mini’s LAN IP drifted again (10.0.0.248 → 10.0.0.10). Every 192.168.1.x literal was already wrong; cert SANs asserted addresses the servers no longer bound to; a mDNS proxy was advertising a dead host. In both cases the failure was invisible because the literal was plausible — a real IP, just the wrong one. The resolver makes the wrong-but-plausible state impossible.
Resolve
sanctum-endpoints discovers live addresses and writes them to a generated file every service reads. The map is never hand-edited.
Enforce
The no-hardcoded-ip lint fails any commit that reintroduces a literal. Discovery without enforcement just rots again.
sanctum-endpoints~/.sanctum/bin/sanctum-endpoints reads instance.yaml — the same file the node topology is built from — for names and virtual anchors only, then resolves them to live addresses. It knows there is a manoir; it does not presume to know where manoir is standing today. Every source is dynamic:
| Address | How it’s resolved |
|---|---|
| Own LAN IP | the default-route interface (route get default → ipconfig getifaddr) — never a hardcoded interface |
| Firewalla box | the bridge’s own discovery endpoint (GET :1984/health → .discovery.ip) |
| Tailscale peers | tailscale status --json, matched by MagicDNS DNSName stem (not the messy display name), gated on Online — an offline node resolves to null, not a stale address |
| Loopback / co-resident services | 127.0.0.1 (stable by definition) |
| vmnet bridge / VM | the 10.10.10.x anchors from instance.yaml — addresses we assign on a private vmnet, so they never drift |
It writes two artifacts, atomically and content-hash idempotent (identical inputs produce a byte-identical file, so re-running never churns):
~/.sanctum/endpoints.json — structured, for Python / Rust / TypeScript consumers.~/.sanctum/endpoints.env — flat KEY=value, sourced by shell.Commands: generate (default), check (exit 1 on drift vs live), get <KEY>. A launchd job regenerates on network change (WatchPaths) and every ten minutes, so the map self-heals.
# shell — source the helper, resolve with a stable-anchor fallbacksource ~/.sanctum/lib/config.shVM=$(sanctum_endpoint SANCTUM_VM_BRIDGE) # 10.10.10.10 (ip-allow: doc example output)TS=$(sanctum_endpoint SANCTUM_MANOIR_TS) # 100.x, currentimport json, osep = json.load(open(os.path.expanduser("~/.sanctum/endpoints.json")))manoir_lan = ep["hosts"]["manoir"]["lan_ip"]Plists can’t call a resolver from a string argument, so a launch wrapper sources endpoints.env and execs the binary — the plist itself carries no literal. Agent prompts get a generated <!-- AUTO:network --> block so the council never reasons on a stale address.
no-hardcoded-ip~/.sanctum/sentinels/no-hardcoded-ip.py scans tracked files for infra-IP and MagicDNS literals (10.0.0.x, legacy 192.168.x, vmnet 10.10.10.x, Tailscale 100.64–127.x, *.tail*.ts.net) and exits non-zero on any violation, so it can gate pre-commit and CI. It is the half that makes this productized rather than a one-time cleanup.
What it does not flag, because those are correct by construction:
instance.yaml) and the generated endpoints.*ip-allow comment — or, for declarative formats like systemd units that forbid trailing comments, ip-allow on the preceding line<!-- AUTO:network --> block (machine-maintained from the SoT)A justified ip-allow (a stable vmnet anchor used as a documented example, a dated historical runbook) is legitimate. A stale 192.168.x never is — the lint treats those as drift, not exceptions.
Roughly 920 literal sites across the Sanctum repos were moved onto the resolver in a single pass, dispatched as parallel review-and-fix agents — one per file, each syntax-checked and reverted if it broke. Drift-prone LAN and Tailscale literals became resolver lookups; stable vmnet anchors became lookups-with-fallback or justified ip-allows; dated docs and runbooks were ip-allow’d rather than rewritten. No services were restarted during the sweep; the changes take effect the next time each consumer runs.
None of this is glamorous. The resolver has no dashboard, ships no metric anyone brags about, and on a good day produces a file byte-identical to yesterday’s. That is the intent. The measure of this system is a phone that does not ring: the subnet moves while the haus sleeps, the launchd job notices, the map regenerates, and every service wakes up already knowing where everyone lives now. Qui-Gon likes to say the best infra is the kind nobody remembers exists. The two nights we spent debugging plausible-but-wrong literals in the dark are the reason we finally believe him.
instance.yaml describes.