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

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:

Failure modes:

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:

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:

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.

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.