docs/gateway/remote.md
This repo supports “remote over SSH” by keeping a single Gateway (the master) running on a dedicated host (desktop/server) and connecting clients to it.
Think of the Gateway host as where the agent lives. It owns sessions, auth profiles, channels, and state. Your laptop, desktop, and nodes connect to that host.
Run the Gateway on a persistent host (VPS or home server) and reach it via Tailscale or SSH.
gateway.bind: "loopback" and use Tailscale Serve for the Control UI.Ideal when your laptop sleeps often but you want the agent always-on.
The laptop does not run the agent. It connects remotely:
Runbook: macOS remote access.
Keep the Gateway local but expose it safely:
Guides: Tailscale and Web overview.
One gateway service owns state + channels. Nodes are peripherals.
Flow example (Telegram → node):
node.* RPC).Notes:
Create a local tunnel to the remote Gateway WS:
ssh -N -L 18789:127.0.0.1:18789 user@host
With the tunnel up:
openclaw health and openclaw status --deep now reach the remote gateway via ws://127.0.0.1:18789.openclaw gateway status, openclaw gateway health, openclaw gateway probe, and openclaw gateway call can also target the forwarded URL via --url when needed.You can persist a remote target so CLI commands use it by default:
{
gateway: {
mode: "remote",
remote: {
url: "ws://127.0.0.1:18789",
token: "your-token",
},
},
}
When the gateway is loopback-only, keep the URL at ws://127.0.0.1:18789 and open the SSH tunnel first.
In the macOS app’s SSH tunnel transport, discovered gateway hostnames belong in
gateway.remote.sshTarget; gateway.remote.url remains the local tunnel URL.
Gateway credential resolution follows one shared contract across call/probe/status paths and Discord exec-approval monitoring. Node-host uses the same base contract with one local-mode exception (it intentionally ignores gateway.remote.*):
--token, --password, or tool gatewayToken) always win on call paths that accept explicit auth.--url) never reuse implicit config/env credentials.OPENCLAW_GATEWAY_URL) may use env credentials only (OPENCLAW_GATEWAY_TOKEN / OPENCLAW_GATEWAY_PASSWORD).OPENCLAW_GATEWAY_TOKEN -> gateway.auth.token -> gateway.remote.token (remote fallback applies only when local auth token input is unset)OPENCLAW_GATEWAY_PASSWORD -> gateway.auth.password -> gateway.remote.password (remote fallback applies only when local auth password input is unset)gateway.remote.token -> OPENCLAW_GATEWAY_TOKEN -> gateway.auth.tokenOPENCLAW_GATEWAY_PASSWORD -> gateway.remote.password -> gateway.auth.passwordgateway.remote.token / gateway.remote.password are ignored.gateway.remote.token only (no local token fallback) when targeting remote mode.OPENCLAW_GATEWAY_* only.WebChat no longer uses a separate HTTP port. The SwiftUI chat UI connects directly to the Gateway WebSocket.
18789 over SSH (see above), then connect clients to ws://127.0.0.1:18789.The macOS menu bar app can drive the same setup end-to-end (remote status checks, WebChat, and Voice Wake forwarding).
Runbook: macOS remote access.
Short version: keep the Gateway loopback-only unless you’re sure you need a bind.
ws:// is loopback-only by default. For trusted private networks,
set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 on the client process as
break-glass. There is no openclaw.json equivalent; this must be process
environment for the client making the WebSocket connection.lan/tailnet/custom, or auto when loopback is unavailable) must use gateway auth: token, password, or an identity-aware reverse proxy with gateway.auth.mode: "trusted-proxy".gateway.remote.token / .password are client credential sources. They do not configure server auth by themselves.gateway.remote.* as fallback only when gateway.auth.* is unset.gateway.auth.token / gateway.auth.password is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).gateway.remote.tlsFingerprint pins the remote TLS cert when using wss://.gateway.auth.allowTailscale: true; HTTP API endpoints do not
use that Tailscale header auth and instead follow the gateway's normal HTTP
auth mode. This tokenless flow assumes the gateway host is trusted. Set it to
false if you want shared-secret auth everywhere.gateway.auth.trustedProxy.allowLoopback = true.Deep dive: Security.
For macOS clients connecting to a remote gateway, the easiest persistent setup uses an SSH LocalForward config entry plus a LaunchAgent to keep the tunnel alive across reboots and crashes.
Edit ~/.ssh/config:
Host remote-gateway
HostName <REMOTE_IP>
User <REMOTE_USER>
LocalForward 18789 127.0.0.1:18789
IdentityFile ~/.ssh/id_rsa
Replace <REMOTE_IP> and <REMOTE_USER> with your values.
ssh-copy-id -i ~/.ssh/id_rsa <REMOTE_USER>@<REMOTE_IP>
Store the token in config so it persists across restarts:
openclaw config set gateway.remote.token "<your-token>"
Save this as ~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.openclaw.ssh-tunnel</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh</string>
<string>-N</string>
<string>remote-gateway</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist
The tunnel will start automatically at login, restart on crash, and keep the forwarded port live.
<Note> If you have a leftover `com.openclaw.ssh-tunnel` LaunchAgent from an older setup, unload and delete it. </Note>Check if the tunnel is running:
ps aux | grep "ssh -N remote-gateway" | grep -v grep
lsof -i :18789
Restart the tunnel:
launchctl kickstart -k gui/$UID/ai.openclaw.ssh-tunnel
Stop the tunnel:
launchctl bootout gui/$UID/ai.openclaw.ssh-tunnel
| Config entry | What it does |
|---|---|
LocalForward 18789 127.0.0.1:18789 | Forwards local port 18789 to remote port 18789 |
ssh -N | SSH without executing remote commands (port-forwarding only) |
KeepAlive | Automatically restarts the tunnel if it crashes |
RunAtLoad | Starts the tunnel when the LaunchAgent loads at login |