Once OpenClaw is installed on a rented Mac mini M4 in Canada, the next fork is not cosmetic: do you reach the local Gateway on TCP 18789 through an SSH tunnel, or do you treat the host as a small edge node and connect directly to the Gateway URL your provider publishes? That choice changes firewall rules, how you store gateway.remote.token, whether launchd must export a full PATH, and what shows up in logs when something breaks at 03:00 local time. This note is a compact decision guide plus a staged runbook and triage table you can paste into an internal wiki.
When an SSH tunnel wins—and when direct Gateway wins
SSH local port forward (for example ssh -L 18789:127.0.0.1:18789 user@canada-mac) keeps the Gateway bound to loopback on the server while your laptop speaks to localhost:18789. It is ideal for solo operators, short-lived debugging, or policies that forbid exposing agent ports on a routable interface. The trade-off is operational: tunnels die with the SSH session, reconnects can race with health checks, and you still need stable SSH itself—which is why team runbooks often pair tunnel workflows with broader SSH/VNC budgeting in Remote Mac team budget and performance in 2026: Canada for North America, trans-Pacific SSH/VNC, and M4 tiers.
Direct Gateway means your control plane or another service calls the Mac’s Gateway endpoint without wrapping it in SSH—typically over TLS in front of 18789, or on a provider-managed path. That path scales better for always-on agents, multiple clients, and automation that cannot hold an interactive shell open. It also pushes you to think like infrastructure: static egress expectations, whether the machine has a Physical Native IP: Why Mac Cloud Also Needs “One IP Per Machine” for allow-lists, and token rotation that does not depend on a developer’s laptop staying online.
launchd owns the process and you need 7×24 clients.
gateway.remote.token and where it must live
In current OpenClaw layouts, gateway.remote.token (or the equivalent key in your onboarded profile) is the shared secret between remote clients and the Gateway. It must be readable by the same user context that runs the Gateway—usually the account that completed openclaw onboard—and must not appear only in an interactive shell profile. Store it in the config file the daemon loads, or export it from a plist EnvironmentVariables dictionary if your security model allows that. After any change, verify from a non-login probe (for example a short script invoked by launchctl) that the process actually sees the variable; interactive SSH can mask missing plist env entirely.
Port 18789: bind address, firewall, and double listeners
Most builds still default the Gateway listener to 127.0.0.1:18789 until you explicitly widen the bind for remote access—which is correct for tunnel-first setups. If you switch to direct access, document whether the listener stays on loopback behind a reverse proxy or moves to a LAN address; either way, run lsof -iTCP:18789 -sTCP:LISTEN after reboot and after upgrades. If you see two processes, stop duplicates with openclaw gateway stop, remove stale LaunchAgents, then re-enable a single plist so health checks stop flapping.
PATH and launchd: step-by-step for headless Macs
launchd jobs inherit a minimal environment; openclaw may not resolve the same way it does in your Terminal.app session. Use this sequence on the Canada host:
1) Resolve the absolute path to the CLI with which openclaw in an SSH session that matches the daemon user.
2) In the LaunchAgent plist ProgramArguments, prefer the absolute binary path instead of a shell wrapper.
3) If you must wrap with /bin/zsh -lc, inline a PATH prefix in the command string, for example PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin;/opt/homebrew/bin/openclaw gateway start, so Node and helpers are found without relying on ~/.zshrc.
4) Load with launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/your.openclaw.gateway.plist (adjust domain if you use a different pattern), then confirm launchctl print gui/$(id -u)/your.label shows the job running.
5) Tail the log path your build documents while triggering a single client request; fix PATH or token errors before opening the listener to the public interface.
ssh -N -L 18789:127.0.0.1:18789 -i ~/.ssh/id_ed25519 [email protected]
Troubleshooting对照: symptom → likely cause → fix
| Symptom | Likely cause | First fix |
|---|---|---|
| Works in SSH shell, fails after reboot | PATH or token only in interactive profile | Absolute paths + plist EnvironmentVariables; reload LaunchAgent |
| Tunnel up, client gets reset | SSH idle timeout or TCP keepalive missing | ServerAliveInterval in SSH config; or move to direct Gateway |
| Direct mode: 401 from Gateway | gateway.remote.token mismatch or wrong header |
Align token in client and server config; rotate if leaked |
EADDRINUSE on 18789 |
Duplicate gateway or stray Node process | openclaw gateway stop; lsof; remove duplicate plist |
| Connection refused on public IP | Listener still on 127.0.0.1 only | Add reverse proxy or adjust bind per provider docs; do not blindly expose raw port |
Summary
On a 2026 OpenClaw stack in Canada, pick SSH tunneling when you need a temporary, loopback-safe pipe to 127.0.0.1:18789, and migrate to direct Gateway with explicit tokens and network policy when automation must survive sleep, reboots, and multiple callers. Wire gateway.remote.token into the same context launchd uses, keep PATH boring and absolute, and treat the triage table above as the first page of your on-call runbook.