Remote Access to the OpenClaw Gateway
Learn how to access the OpenClaw Gateway remotely via WebSocket, SSH tunnels, or Tailscale. This guide is for operators and node devices needing secure remote connections.
Read this when
- Running or troubleshooting remote gateway setups
OpenClaw designates a single Gateway (the master) on one host, with all other components connecting to it as clients. The Gateway is responsible for sessions, auth profiles, channels, and state; everything else acts purely as a client.
- Operators (you, or the macOS app): a direct WebSocket over LAN or Tailnet is the simplest option when the Gateway is accessible; SSH tunneling serves as the universal backup.
- Nodes (iOS/Android and other devices): reach the Gateway via WebSocket (over LAN/tailnet or through an SSH tunnel).
Remote clients can resume a conversation owned by the Gateway using a URL or short reference. For details, see Session synchronization and attachment.
The core idea
By default, the Gateway WebSocket listens on loopback at port 18789 (gateway.port). To use it remotely, either expose it via Tailscale Serve or a trusted LAN-Tailnet bind, or forward the loopback port through SSH.
Topology options
| Setup | Where the Gateway runs | Best for |
|---|---|---|
| Always-on Gateway in your tailnet | Persistent host (VPS or home server), reached via Tailscale or SSH | Laptops that sleep often but need the agent always-on. See exe.dev (easy VM) or Hetzner (production VPS). |
| Home desktop | Desktop; laptop connects remotely via the macOS app's remote mode (Settings → Connection → OpenClaw runs) | Keeping the agent on hardware that stays powered on. Runbook: macOS remote access. |
| Laptop | Laptop, exposed safely via SSH tunnel or Tailscale Serve (keep gateway.bind: "loopback") | Single-machine setups. See Tailscale and Web. |
For the always-on and laptop setups, prefer keeping gateway.bind: "loopback" and using Tailscale Serve for the Control UI, or a trusted LAN/Tailnet bind with gateway.remote.transport: "direct". SSH tunnel is the fallback that works from any machine.
Command flow (what runs where)
State and channels live on a single Gateway; nodes are peripheral devices. Example (Telegram message routed to a node tool):
- Telegram message arrives at the Gateway.
- Gateway runs the agent, which decides whether to call a node tool.
- Gateway calls the node over the Gateway WebSocket (
node.invokeRPC). - Node returns the result; Gateway replies to Telegram.
Nodes do not run the Gateway service. Only one Gateway should run per host unless you intentionally run isolated profiles (see Multiple gateways). macOS app "node mode" is just a node client over the Gateway WebSocket.
SSH tunnel (CLI + tools)
ssh -N -L 18789:127.0.0.1:18789 user@gateway-host
Once the tunnel is active, openclaw health and openclaw status --deep can access the remote Gateway through ws://127.0.0.1:18789. Additionally, openclaw gateway status, openclaw gateway health, openclaw gateway probe, and openclaw gateway call can point to a forwarded URL using --url.
To avoid setting up a separate SSH tunnel for each client and instead use one private wss:// endpoint while the Gateway stays on loopback, check Give your Gateway a stable HTTPS URL.
Note
Swap
18789with your configuredgateway.port(or--port/OPENCLAW_GATEWAY_PORT).
Warning
--urlwill not automatically read credentials from config or environment variables. You must supply--tokenor--password; if they are absent, the client sends no credentials and the connection fails when the target Gateway requires authentication.
CLI remote defaults
To make CLI commands default to a remote target, persist it:
{
gateway: {
mode: "remote",
remote: {
url: "ws://127.0.0.1:18789",
token: "your-token",
},
},
}
If the Gateway is loopback-only, keep the URL at ws://127.0.0.1:18789 and establish the SSH tunnel first. In the macOS app's SSH-tunnel transport, the discovered Gateway hostname goes in gateway.remote.sshTarget (user@host or user@host:port); gateway.remote.url remains the local tunnel URL. When the remote port differs from the local one, configure gateway.remote.remotePort.
Host-key verification is strict by default (gateway.remote.sshHostKeyPolicy: "strict"). Switch it to "openssh" to rely on your effective OpenSSH config instead; check your user and system SSH settings before enabling this.
For a Gateway already reachable on a trusted LAN or Tailnet, use direct mode:
{
gateway: {
mode: "remote",
remote: {
transport: "direct",
url: "ws://192.168.0.202:18789",
token: "your-token",
},
},
}
Gateway behind an identity-aware proxy
To set up this deployment from the ground up, including the tunnel, Access application, Gateway trusted-proxy auth, and node routes, see Cloudflare Tunnel and Access. This section addresses only the client side: how a CLI, TUI, or app authenticates to that edge.
Use gateway.remote.edgeAuth when an identity-aware proxy must authenticate the WebSocket upgrade before traffic reaches the Gateway. Header values are SecretInput fields, so they can come from env, file, exec, or store secret providers without placing credentials directly in the config.
For Cloudflare Access, a generic exec secret provider can obtain a short-lived application token from an operator-installed cloudflared binary:
{
secrets: {
providers: {
"cloudflare-access": {
source: "exec",
command: "/usr/local/bin/cloudflared",
args: ["access", "token", "-app=https://gateway.example"],
jsonOnly: false,
passEnv: ["HOME"],
trustedDirs: ["/usr/local/bin"],
},
},
},
gateway: {
mode: "remote",
remote: {
url: "wss://gateway.example",
edgeAuth: {
"Cf-Access-Token": {
source: "exec",
provider: "cloudflare-access",
id: "token",
},
},
},
},
}
secrets.providers.*.command must be an absolute path; replace /usr/local/bin/cloudflared with the real, non-symlink install location on your host, such as the resolved executable under a Homebrew prefix, and keep trustedDirs pointing at the directory that actually holds it.
Exec providers operate within a sanitized environment. The cached application token is read by cloudflared from the user's home directory, which makes passEnv: ["HOME"] a prerequisite; if it is missing, the provider exits with a non-zero code and no header gets generated. Execute cloudflared access login <gateway-url> once beforehand so that a token is available for reading.
When using a Cloudflare Access service token, supply the two static headers from any supported secret provider. The following example pulls them from environment-backed SecretRefs:
{
secrets: {
providers: {
default: { source: "env" },
},
},
gateway: {
mode: "remote",
remote: {
url: "wss://gateway.example",
edgeAuth: {
"CF-Access-Client-Id": {
source: "env",
provider: "default",
id: "CF_ACCESS_CLIENT_ID",
},
"CF-Access-Client-Secret": {
source: "env",
provider: "default",
id: "CF_ACCESS_CLIENT_SECRET",
},
},
},
},
}
The Gateway connection code within OpenClaw never invokes cloudflared directly, nor does it include any Cloudflare dependency or login procedure. Only the generic exec secret provider runs the precise command that an operator sets up. Resolved edge-auth headers are transmitted solely when the destination falls within the configured gateway.remote.url scope, exclusively over wss://, and are never forwarded across redirects.
Credential precedence
A single shared contract governs credential resolution for the Gateway across call, probe, and status paths, as well as Discord exec-approval monitoring. Node-host adheres to the same contract, with one local-mode exception: it disregards gateway.remote.*.
- On call paths that permit explicit authentication, explicit credentials (
--token,--password, or a tool'sgatewayToken) take precedence. - URL override safety:
- Implicit config or env credentials are never reused by CLI
--url. - Env
OPENCLAW_GATEWAY_URLis restricted to env credentials only (OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD).
- Implicit config or env credentials are never reused by CLI
- Local mode defaults:
- token:
gateway.auth.token->OPENCLAW_GATEWAY_TOKEN->gateway.remote.token(remote fallback applies only when the local token is absent) - password:
gateway.auth.password->OPENCLAW_GATEWAY_PASSWORD->gateway.remote.password(remote fallback applies only when the local password is absent)
- token:
- Remote mode defaults:
- token:
gateway.remote.token->OPENCLAW_GATEWAY_TOKEN->gateway.auth.token - password:
OPENCLAW_GATEWAY_PASSWORD->gateway.remote.password->gateway.auth.password
- token:
- Node-host local-mode exception: environment credentials remain primary, while
gateway.remote.tokenandgateway.remote.passwordare skipped because node commands point to a specific host and port. - Remote startup, status, and wizard probes that support SecretRefs treat configured
gateway.remote.tokenandgateway.remote.passwordas definitive for the specified target. Ambient environment credentials come into play only when neither remote credential is set. If a configured remote SecretRef cannot be resolved, the probe issues a warning and does not revert to environment credentials; a separately configured sibling credential that resolves successfully continues to work. - Gateway env overrides rely exclusively on
OPENCLAW_GATEWAY_*.
Chat UI remote access
WebChat does not use a separate HTTP port; the SwiftUI chat interface links directly to the Gateway WebSocket.
- Tunnel
18789over SSH (as described above), then point clients tows://127.0.0.1:18789. - For LAN or Tailnet direct mode, direct clients to the configured private
ws://or securewss://URL. - On macOS, remote mode in the app handles transport selection automatically.
macOS app remote mode
The macOS menu bar app drives the entire setup process from start to finish: remote status checks, WebChat, and Voice Wake forwarding. See the runbook: macOS remote access.
Security rules (remote/VPN)
Leave the Gateway bound to loopback-only unless a bind is genuinely required.
- Loopback + SSH/Tailscale Serve offers the strongest safety by default, since nothing is exposed publicly.
- Plaintext
ws://is permitted for loopback, private/LAN (RFC 1918), link-local, CGNAT,.local, and.ts.nethosts. For public remote hosts,wss://is mandatory. - When binding to non-loopback addresses (
lan/tailnet/custom, orautoif loopback isn't available), Gateway auth is required: either a token, password, or an identity-aware reverse proxy configured withgateway.auth.mode: "trusted-proxy". - Client credentials come from
gateway.remote.token/.password; these do not, on their own, set up server-side authentication. - Local call paths may rely on
gateway.remote.*as a fallback, but only whengateway.auth.*remains unset. - If
gateway.auth.token/gateway.auth.passwordis set explicitly through SecretRef and cannot be resolved, the resolution fails closed, so no remote fallback can hide the issue. gateway.remote.tlsFingerprintfixes the remote TLS certificate forwss://, covering operator/control traffic and the companion node in macOS direct mode. Without a stored pin, macOS only pins on first use after standard system trust checks pass; Gateways using self-signed or private-CA certificates need an explicit fingerprint or Remote over SSH.- Tailscale Serve can authenticate Control UI/WebSocket traffic through identity headers when
gateway.auth.allowTailscale: true. HTTP API endpoints skip that header-based auth and rely on the Gateway's standard HTTP auth mode instead. This tokenless approach assumes the Gateway host is trustworthy; switch it tofalseto enforce shared-secret auth across the board. - Trusted-proxy auth defaults to expecting a non-loopback identity-aware proxy. For a reverse proxy on the same host over loopback, you must explicitly set
gateway.auth.trustedProxy.allowLoopback = true. - Browser control should be handled like operator access: restricted to the tailnet and requiring deliberate node pairing.
Deep dive: Security.
macOS: persistent SSH tunnel via LaunchAgent
On macOS, the most straightforward persistent setup involves an SSH LocalForward config entry combined with a LaunchAgent that maintains the tunnel across reboots and crashes.
Step 1: add SSH config
Open ~/.ssh/config for editing:
Host remote-gateway
HostName <REMOTE_IP>
User <REMOTE_USER>
LocalForward 18789 127.0.0.1:18789
IdentityFile ~/.ssh/id_rsa
Substitute your own values for <REMOTE_IP> and <REMOTE_USER>.
Step 2: copy SSH key (one-time)
ssh-copy-id -i ~/.ssh/id_rsa <REMOTE_USER>@<REMOTE_IP>
Step 3: configure the gateway token
openclaw config set gateway.remote.token "<your-token>"
If the remote Gateway authenticates with a password, use gateway.remote.password instead. While OPENCLAW_GATEWAY_TOKEN still works as a shell-level override, the lasting remote-client configuration relies on gateway.remote.token / gateway.remote.password.
Step 4: create the LaunchAgent
Store it 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>
Step 5: load the LaunchAgent
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist
The tunnel launches automatically at login, restarts after a crash, and keeps the forwarded port active.
Note
If an older setup left a
com.openclaw.ssh-tunnelLaunchAgent behind, unload it and remove the file.
Troubleshooting
# 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 | Restarts the tunnel automatically if it crashes |
RunAtLoad | Starts the tunnel when the LaunchAgent loads at login |