Castle Wall macOS Install And Arm Reference
This is the command-level reference behind the supported agent-guided flow. A
normal operator should give a shell-capable agent the task and have it loop on
sanctuary install --profile full --harness hermes --json; the operator should
not have to execute this recipe. This reference targets Sanctuary v1.8.4 and
the Sanctuary-CastleWall.app.zip asset attached to that release.
The npm package installs the cooperative Sanctuary CLI, dashboard, keys, policy gates, and audit trail. The operating-system wall is a separate signed and notarized macOS app with a system extension, a signer helper, and a root boot service.
Scope
- macOS 13 or newer with an interactive console session. The first System Settings approvals require a GUI login session; SSH cannot complete them.
- Node.js 22 or newer and npm 10 or newer.
- Admin credentials for
sudo. - One operator account that owns the fortress, usually your normal macOS account.
- One protected agent macOS account. The protected account must have a real UID at or above the default ceiling of
500. - The proven public macOS bound is per-UID allow/deny plus attended reboot survival on one host and one OS version. Unattended reboot survival remains unproven. Linux has a separate narrowed Ubuntu 24.04 (x86-64) Assurance Matrix claim; Windows is roadmapped.
1. Install the Sanctuary CLI and fortress
Install the pinned CLI:
npm install -g @sanctuary-framework/mcp-server@1.8.4
Record the CLI’s absolute path for the later commands that run through sudo:
SANCTUARY_CLI="$(command -v sanctuary)"
test -n "$SANCTUARY_CLI" && test -x "$SANCTUARY_CLI" && echo "$SANCTUARY_CLI"
Wrap the harness you already use. For Claude Code:
sanctuary protect --claude-code
Substitute --openclaw, --hermes, --cursor, --cline, --mastra, or --wrap <path-to-config> for another harness.
Back up the credential this machine holds for the fortress:
sanctuary export-passphrase
It prints the stored fortress passphrase when one unlocks this fortress, and otherwise the OS-keyring custody factor the fortress was created with. That factor is machine-resident: the portable credential is the recovery key captured at creation, so keep that one too.
Failure mode: this step does not install or arm the macOS wall. It prepares the fortress, cooperative gates, dashboard, audit trail, and harness config.
2. Download and verify the Castle Wall app
Download the v1.8.4 release asset:
APP_ZIP="$HOME/Downloads/Sanctuary-CastleWall.app.zip"
curl -L -o "$APP_ZIP" \
"https://github.com/eriknewton/sanctuary-framework/releases/download/v1.8.4/Sanctuary-CastleWall.app.zip"
Verify the SHA-256 digest:
shasum -a 256 "$APP_ZIP"
Expected first field:
a19b71e8a5cb25070f8efff6df43cbb77c451da8b0f41bcb47680193553081c7
The GitHub release asset metadata and the release body publish this digest. The release does not publish a separate .sha256 sidecar file for the app zip. If your digest differs, stop and discard the download.
3. Install and launch the app
Extract the zip and place the app where the CLI auto-discovers it:
ditto -x -k "$APP_ZIP" "$HOME/Downloads"
sudo ditto "$HOME/Downloads/Sanctuary-CastleWall.app" \
"/Applications/Sanctuary-CastleWall.app"
open "/Applications/Sanctuary-CastleWall.app"
Failure mode: the CLI searches /Applications/Sanctuary-CastleWall.app and ~/Applications/Sanctuary-CastleWall.app by default. If the app stays in another directory, set SANCTUARY_CASTLE_HOSTAPP to the app’s Contents/MacOS/CastleWallHostApp binary and set SANCTUARY_CASTLE_SIGNER_CLIENT to the bundled Contents/MacOS/castle-wall-signer-client shim.
4. Approve the macOS security prompts
Run the shared-directory preflight, then use the app at the console:
sudo "$SANCTUARY_CLI" castle-wall setup-shared-dir
open "/Applications/Sanctuary-CastleWall.app"
Approve the macOS prompts in this order:
- If the app shows “Approve Sanctuary background helper”, click “Open Settings”, then enable
Sanctuary-CastleWallunder System Settings > General > Login Items & Extensions > Allow in the Background. - Quit
Sanctuary-CastleWall.app, relaunch it withopen "/Applications/Sanctuary-CastleWall.app", then expect the system-extension approval prompt. - If macOS asks for system extension approval, open System Settings > Privacy & Security and approve the Castle Wall system extension. If no prompt appears, click the app’s Arm button; the manual Arm action re-submits activation.
- On macOS Tahoe, open System Settings > General > Login Items & Extensions > Network Extensions and switch Castle Wall on.
- If a later arm attempt says content-filter consent is missing, launch
Sanctuary-CastleWall.appat the console, click Allow on the content-filter prompt, then retry the arm command.
Failure modes:
- A remote shell cannot complete the first GUI approvals. Use the Mac console, Screen Sharing, or another interactive macOS login session for this step.
sudo: sanctuary: command not foundmeans root’s PATH cannot find the CLI even when your user PATH can. Usesudo "$SANCTUARY_CLI" ...after confirmingecho "$SANCTUARY_CLI"prints the absolute path.
5. Re-pin to the signer helper
Put the enforcement trust anchor in the hands of the approved root signer helper. This is the only command that changes this Mac’s trust anchor; creating a vault never does. It is not, by itself, the step that puts the vault you created on the Castle Wall: a vault is on the wall only when this Mac’s anchor is consistent with the signer helper’s key AND that vault has its own arm evidence. Re-pin supplies the first condition. Arming, later in this guide, supplies the second.
sanctuary castle-wall re-pin
Expected stderr announcement starts with:
Re-pinning trust anchor for fortress: <fortress-path>
The command then asks you to confirm, and continues only when you type the word it names. It asks twice, and both asks want the same word: the second one comes from the small native helper client the CLI hands the request to, which is separately executable and therefore does its own check for a person at a terminal. Typing the word once and walking away leaves the command waiting at the second prompt. It runs in a terminal you are sitting at: with no terminal either ask refuses, and there is no flag or environment variable that skips them. The master key is derived after the confirmation, so a normal passphrase or custody prompt can still appear before it completes.
Failure mode: Refusing: castle-wall re-pin requires an interactive terminal. means the command was started by something that has no terminal (a script, a scheduled job, or an agent). Run it yourself from a local Terminal window. A message from castle-wall-signer-client saying re-pin requires an interactive terminal means the same thing, one layer down.
Check helper readiness:
SANCTUARY_CASTLE_SIGNER_CLIENT="/Applications/Sanctuary-CastleWall.app/Contents/MacOS/castle-wall-signer-client" \
sanctuary castle-wall signer-helper status
Expected ready output has four passing checks and this final line:
Signer helper: READY (boot-started, reachable, pin-consistent).
Failure modes:
launchd_job_visiblefails: approve the Background Item in System Settings > General > Login Items & Extensions, then reboot or rerun the check.xpc_reachablefails: confirm the Background Item approval and the signer-client path.pin_matchfails: rerunsanctuary castle-wall re-pin.custody_directoryfails: rerunsudo "$SANCTUARY_CLI" castle-wall setup-shared-dir.
6. Install the root boot service
The boot service keeps the Castle Wall daemon alive after boot in safe mode. It snapshots the signed app’s embedded Node and safe-mode daemon into root-owned custody; the invoking npm CLI is never retained as a root executable. Use an absolute Node path to avoid launchd-style minimal-PATH failures while invoking the installer itself:
SANCTUARY_CLI="$(realpath "$(command -v sanctuary)")"
NODE_BIN="$(realpath "$(command -v node)")"
test -f "$SANCTUARY_CLI" && test -x "$NODE_BIN"
sudo "$NODE_BIN" "$SANCTUARY_CLI" castle-wall install-boot \
--signer-client "/Applications/Sanctuary-CastleWall.app/Contents/MacOS/castle-wall-signer-client"
Expected success output begins like this:
Castle Wall safe-mode boot service installed and running (pid <pid>): /Library/LaunchDaemons/ai.sanctuaryprotocol.castle-wall.daemon.plist
Runs as root at boot in SAFE MODE (boot token only, never the master key); fortress <fortress-path>; KeepAlive on.
On first install it also prints:
Boot token minted: /Library/Application Support/Sanctuary/castle-wall-boot-token.bin (root-owned 0600).
Failure mode: if the command cannot resolve the CLI path, reinstall with npm install -g @sanctuary-framework/mcp-server@1.8.4 and confirm command -v sanctuary prints an absolute path. sudo: sanctuary: command not found means root’s PATH cannot find the CLI even when your user PATH can. If bootstrap is accepted and the service does not stay running, inspect the two paths the command prints: sudo launchctl print system/ai.sanctuaryprotocol.castle-wall.daemon and /var/log/castle-wall-daemon.err.log.
7. Choose the protected agent UID
Pick a dedicated macOS account for the agent. If the account does not exist yet, create it in System Settings > Users & Groups > Add Account and choose Standard rather than Administrator; a freshly created macOS account gets a UID at or above 501, which satisfies the default ceiling.
Get its UID:
id -u <agent-account>
Use the printed integer as <agent-uid>. With the default ceiling, valid protected agent UIDs are 500 or higher. Root 0, malformed values, and UIDs below the ceiling are rejected before arming.
Failure mode: the UID is never auto-derived. If you choose the wrong UID, you protect the wrong account. Re-run id -u <agent-account> immediately before arming.
8. Arm a verified deny-all quarantine
This first arm proves the wall is actually enforcing for the protected UID. It deliberately starts with zero agent egress, so the agent is confined until you promote allow rules.
Run this in the same terminal immediately before enable. The deny-all smoke shells out to sudo -n -u '#<uid>' /usr/bin/curl ..., so it needs either a warm sudo credential or an explicitly configured NOPASSWD grant:
sudo -v
sanctuary castle-wall enable \
--agent-uid=<agent-uid> \
--ceiling=500 \
--no-ttl \
--allow-no-egress
Expected success output includes:
Agent origin configured: mode=uid agent_uid=<agent-uid> ceiling=500
Deny-all quarantine smoke passed: uid <agent-uid> could not reach example.com:443 on the direct --noproxy path.
Castle Wall armed: content filter enabled (verified via host-app status, system extension state, and enforcement availability).
Use --ttl 15m instead of --no-ttl for a bounded drill window. TTL values use forms like 30s, 5m, or 1h.
Failure modes:
Refusing to arm: a non-interactive sudo credential for the arm probe is unavailable for uid <agent-uid>.This is the usual outcome when yoursudotimestamp is cold. The wall is not armed, so nodisableis needed. Runsudo -v(or configure a NOPASSWD sudoers grant for the probe), then re-runenable. The arm now preflights sudo before arming, so a missing credential is caught here rather than after the wall is armed.Refusing to arm: no agent-origin descriptor is set for this fortress.Re-runenablewith--agent-uid=<agent-uid> --ceiling=500.Refusing to arm: this fortress has ZERO agent-matchable allow rules. Use--allow-no-egressfor the initial deny-all proof, or promote allow rules first.The one-time macOS content-filter consent has not been granted on this machine.LaunchSanctuary-CastleWall.appat the console, click Allow, then retry.The Castle Wall system extension is installed but toggled OFF.Open System Settings > General > Login Items & Extensions > Network Extensions and switch Castle Wall on.Castle Wall arm saved by the host app, but enforcement availability is not live.Treat the wall as unarmed, runsanctuary castle-wall status, fix the named availability reason, then re-runenable.- If the as-uid smoke cannot prove it reached curl (a rarer case now that the pre-arm sudo preflight catches a cold credential first, for example if the credential expires between the preflight and the smoke), the refusal is:
Castle Wall arm saved by the host app, but the deny-all quarantine smoke could not verify the direct as-uid path.
Expected uid <agent-uid> to be unable to reach example.com:443 with --noproxy '*', but the probe itself was inconclusive.
Treat the quarantine as unverified; run 'sanctuary castle-wall disable' before continuing.
This is expected recovery. Run sanctuary castle-wall disable, warm sudo with sudo -v, then re-run enable; the install does not need to be restarted.
- If the smoke proves the protected UID could still reach the negative-control host, the refusal is:
Castle Wall arm saved by the host app, but the deny-all quarantine smoke FAILED.
uid <agent-uid> reached example.com:443 on the direct --noproxy path despite ZERO agent-matchable allow rules.
Treat this as fail-open for the confined uid; run 'sanctuary castle-wall disable' before continuing.
This is also expected recovery. Run sanctuary castle-wall disable before making any allow-rule changes; the install does not need to be restarted.
9. Confirm the wall is armed
Run:
sanctuary castle-wall status
Expected armed lines:
Castle Wall sysext: [activated enabled]
Content filter: enabled
Enforcement availability: live (<reason>; observed=<timestamp>; active_connections=<count>)
The enable success line is the stronger proof for the arm attempt. status is the ongoing diagnostic. If status prints Content filter: disabled, Castle Wall sysext: not loaded, Castle Wall sysext: [activated disabled], or Enforcement availability: unavailable, treat the wall as unarmed until the named condition is fixed.
10. Move from quarantine to useful allow rules
Turn on observe mode while the quarantine is armed:
sanctuary castle-wall observe start
Run the protected agent under the protected account so denied destinations are recorded. Then review candidates:
sanctuary castle-wall observe candidates
Promote the destinations you approve:
sanctuary castle-wall observe promote --destination <host:port>
Promotion is Tier-1 approved. Approved rules are re-signed and published to the rule source the enforcement daemons read. Follow the command’s final instruction: either run sanctuary castle-wall reload for a running direct wall, or re-arm if the command says the fortress uses exclusive routing.
Failure mode: Skipping EXFIL-RISK destination <host:port> (pass --include-risky to promote it deliberately). means the destination stayed pending because it was classified as an exfil risk. Re-run with --include-risky only if you deliberately approve that destination.
Teardown
Use the convergent uninstall command rather than manually reordering the safety steps:
sanctuary uninstall --fortress "$HOME/.sanctuary"
sudo sanctuary uninstall --fortress "$HOME/.sanctuary"
The first run disarms through the logged-in app context. The elevated rerun can remove the root boot service, harness daemon, provisioned egress rules, and request system-extension deactivation. Both runs preserve the fortress, keys, recovery material, and audit log. The command refuses to report a clean uninstall while any authoritative probe is unknown or installed residue remains.
macOS may report that system-extension deactivation will complete after reboot.
In that case, reboot and rerun sanctuary uninstall; only an observed-absent
system extension closes teardown. Removing the app bundle is optional ordinary
app cleanup after the command reports the installed footprint removed, and is
not a substitute for disarming or deactivating the extension.
Restore and recovery
If you restore a fortress from Time Machine, backup, dotfile sync, or a cloned machine, Sanctuary can freeze trust-bearing writes until you acknowledge the restore:
sanctuary restore-attest --fortress "$HOME/.sanctuary"
Enter the fortress passphrase when prompted, or set SANCTUARY_PASSPHRASE for noninteractive recovery.
Expected success output:
Attested: custody epoch re-baselined to <epoch>.
Trust-bearing writes are UNFROZEN.
A permanent audit entry (custody_restore_attested) records this restore.
If you did not intentionally restore anything, treat the freeze as suspicious and rotate the master before attesting.