Install
openclaw skills install hybrid-gatewaySet up and troubleshoot a hybrid OpenClaw architecture where the gateway runs on a cloud VPS and a local machine (Mac Mini, desktop, Raspberry Pi, etc.) acts as a node. Covers Tailscale networking, gateway bind modes, node pairing, LaunchAgent/systemd auto-start, exec routing, SSH fallback, and the common gotchas that break this setup. Use when connecting a local node to a remote gateway, debugging node connectivity, or planning a VPS + local hardware split.
openclaw skills install hybrid-gatewayRun your OpenClaw gateway on a cloud VPS for reliability, and connect a local machine as a node for hardware capabilities the VPS lacks: residential IP, GPU/ML inference, browser automation, local models, macOS-only tools, etc.
| VPS (Gateway) | Local Node | |
|---|---|---|
| Always online | ✅ Static IP, no ISP outages | ❌ Power/network dependent |
| Messaging | ✅ Handles Telegram, Discord, etc. | ❌ Not its job |
| Agent brain | ✅ Runs models, routes tools | ❌ Peripheral only |
| GPU / ML | ❌ Most VPS have no GPU | ✅ Apple Silicon, NVIDIA, etc. |
| Browser automation | ⚠️ Headless only, cloud IP | ✅ Real browser, residential IP |
| Local models | ❌ No hardware for it | ✅ Ollama, whisper, etc. |
| macOS tools | ❌ Linux VPS | ✅ Native macOS (if using Mac) |
Before starting, you need:
tailscale status on either machine should show both devicesopenclaw gateway status should show runningIf you don't have Tailscale set up yet, do that first. The rest of this guide assumes both machines are on the same tailnet.
By default, the gateway binds to loopback (127.0.0.1 only). Your node can't reach that from another machine.
Recommended: lan bind (listens on all interfaces)
# On the VPS
openclaw config set gateway.bind lan
This listens on 0.0.0.0 — both 127.0.0.1 (local agents) and your Tailscale IP (remote node) will work.
Alternative: tailnet bind (Tailscale IP only)
openclaw config set gateway.bind tailnet
⚠️ Warning: tailnet bind breaks local agent-to-agent sessions. Local tools try ws://127.0.0.1:18789 but the gateway only listens on the Tailscale IP. If you use multi-agent workflows, use lan instead.
Ensure auth is configured (required for any non-loopback bind):
# Check current auth
openclaw config get gateway.auth.mode
openclaw config get gateway.auth.token
# Set token auth if not configured
openclaw config set gateway.auth.mode token
openclaw config set gateway.auth.token "<your-token>"
# Add rate limiting (recommended)
openclaw config set gateway.auth.rateLimit.maxAttempts 10
openclaw config set gateway.auth.rateLimit.windowMs 60000
openclaw config set gateway.auth.rateLimit.lockoutMs 300000
Restart the gateway after config changes:
openclaw gateway restart
Verify:
openclaw gateway status
# Should show: bind=lan (0.0.0.0) and RPC probe: ok
On the local machine (Mac Mini, desktop, etc.):
# Get your VPS Tailscale IP (run on VPS)
tailscale ip -4
# Example output: 100.x.y.z
# On the local machine, set the gateway token
export OPENCLAW_GATEWAY_TOKEN="<your-token>"
# Start the node (foreground, for testing)
openclaw node run --host <VPS_TAILSCALE_IP> --port 18789 --display-name "My Node"
If it connects, you'll see it register. If not, see Troubleshooting below.
On the VPS:
openclaw devices list
# Find the pending request from your node
openclaw devices approve <requestId>
# Verify
openclaw nodes status
# Should show your node as paired and connected
You want the node to survive reboots and run headless.
openclaw node install --host <VPS_TAILSCALE_IP> --port 18789 --display-name "My Node"
This creates a LaunchAgent plist that auto-starts on login.
Important: If the gateway requires ws:// over a private network (not wss://), you may need to set an environment variable in the LaunchAgent plist:
Add these environment variables to the plist's EnvironmentVariables dict:
OPENCLAW_GATEWAY_TOKEN — your existing gateway auth token (the same one from openclaw config get gateway.auth.token on the VPS)OPENCLAW_ALLOW_INSECURE_PRIVATE_WS = 1 — allows ws:// over Tailscale (safe — traffic is WireGuard-encrypted at the network layer)See the OpenClaw node host docs for the full plist reference.
The insecure WS override is only needed because OpenClaw blocks plaintext ws:// to non-loopback addresses by default. On Tailscale this is safe since all traffic is encrypted by WireGuard.
The LaunchAgent plist location: ~/Library/LaunchAgents/ai.openclaw.node.plist
Load/unload manually:
launchctl load ~/Library/LaunchAgents/ai.openclaw.node.plist
launchctl unload ~/Library/LaunchAgents/ai.openclaw.node.plist
Check logs: ~/.openclaw/logs/node.log
openclaw node install --host <VPS_TAILSCALE_IP> --port 18789 --display-name "My Node"
Or create a systemd user service manually:
In the systemd service [Service] section, add:
Environment=OPENCLAW_GATEWAY_TOKEN=<your-gateway-token> — same token from your VPS gateway configEnvironment=OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 — only if not using wss:// via Tailscale ServeSee the OpenClaw node host docs for the full service file reference.
systemctl --user daemon-reload
systemctl --user enable openclaw-node
systemctl --user start openclaw-node
Tell the gateway to route exec host=node commands to your node:
# On the VPS
openclaw config set tools.exec.node "My Node"
Now agents can run commands on your local machine:
exec host=node command="uname -a"
The node enforces its own allowlist. Add commands you want to permit:
# On the VPS (manages node approvals remotely)
openclaw approvals allowlist add --node "My Node" "/usr/bin/uname"
openclaw approvals allowlist add --node "My Node" "/bin/bash"
openclaw approvals allowlist add --node "My Node" "/usr/local/bin/node"
Or edit ~/.openclaw/exec-approvals.json on the node directly.
The node protocol handles command execution, but SSH is still needed for:
.bashrc/.zshrc — nvm, homebrew PATH, env vars)Set up SSH key-based access between the VPS and the node:
1. Generate an ed25519 key pair on the VPS
2. Copy the public key to the node's `authorized_keys`
3. Create an SSH config alias (e.g. `Host my-node`) pointing to the node's Tailscale IP
4. Test with `ssh my-node` to confirm passwordless access
See the [GitHub SSH key guide](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) for detailed steps — the same process applies to any SSH host.
| Task | Use |
|---|---|
| Run a command | exec host=node (node protocol) |
| Transfer files | scp my-node:/path/to/file . (SSH) |
| Commands needing shell env | ssh my-node "source ~/.zshrc; command" (SSH) |
| Node disconnected | SSH fallback for everything |
The node is trying to connect via ws:// (not wss://) to a non-loopback address. This is blocked by default.
Fix: Set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 in the node's environment. This is safe on Tailscale (WireGuard-encrypted). Add it to your LaunchAgent plist or systemd service.
Better fix (long-term): Use Tailscale Serve to get proper wss:// on the gateway. Then you don't need the env override.
If you changed gateway.bind from loopback to tailnet, local agent-to-agent communication breaks. Local sessions try ws://127.0.0.1 but the gateway only listens on the Tailscale IP.
Fix: Use gateway.bind: "lan" instead. This listens on 0.0.0.0 — both loopback and Tailscale interfaces.
Network route is working, auth is fine, but the device hasn't been approved yet.
# On the VPS
openclaw devices list
openclaw devices approve <requestId>
The setup code or pairing token is stale.
Fix: Generate a fresh one and reconnect:
openclaw qr --json
openclaw nodes status
tools.exec.node points to the right node:
openclaw config get tools.exec.node
tailscale status~/.openclaw/logs/node.logNode system.run uses a minimal PATH. Homebrew, nvm, and other tools that modify PATH via shell config won't be available.
Fix: Use full binary paths:
exec host=node command="/opt/homebrew/bin/ollama run llama3"
Or fall back to SSH when you need the full shell environment:
ssh my-node "source ~/.zshrc; ollama run llama3"
┌─────────────────────────────────────────────┐
│ CLOUD VPS │
│ │
│ ┌──────────────────────────────────┐ │
│ │ OpenClaw Gateway │ │
│ │ (bind: lan / 0.0.0.0) │ │
│ │ port: 18789 │ │
│ │ │ │
│ │ ┌─────────┐ ┌──────────────┐ │ │
│ │ │ Telegram │ │ Discord │ │ │
│ │ │ Bot │ │ Bot │ │ │
│ │ └─────────┘ └──────────────┘ │ │
│ │ │ │
│ │ ┌─────────┐ ┌──────────────┐ │ │
│ │ │ Agents │ │ Models │ │ │
│ │ │ (main, │ │ (API) │ │ │
│ │ │ workers)│ │ │ │ │
│ │ └─────────┘ └──────────────┘ │ │
│ └──────────────────────────────────┘ │
│ │ │
│ Tailscale VPN │
│ (WireGuard encrypted) │
└────────────────────┼────────────────────────┘
│
│ ws:// (safe — encrypted by Tailscale)
│
┌────────────────────┼────────────────────────┐
│ LOCAL NODE │
│ │
│ ┌──────────────────────────────────┐ │
│ │ OpenClaw Node │ │
│ │ (LaunchAgent / systemd) │ │
│ │ │ │
│ │ exec host=node → system.run │ │
│ │ │ │
│ │ ┌─────────┐ ┌──────────────┐ │ │
│ │ │ Ollama │ │ Lighthouse │ │ │
│ │ │ (local │ │ (browser │ │ │
│ │ │ models) │ │ automation) │ │ │
│ │ └─────────┘ └──────────────┘ │ │
│ │ │ │
│ │ ┌─────────┐ ┌──────────────┐ │ │
│ │ │ Whisper │ │ Playwright │ │ │
│ │ │ (speech │ │ (headless │ │ │
│ │ │ to text)│ │ browser) │ │ │
│ │ └─────────┘ └──────────────┘ │ │
│ └──────────────────────────────────┘ │
│ │
│ Also accessible via SSH (file transfer, │
│ full shell env, fallback) │
└──────────────────────────────────────────────┘
| Task | Command |
|---|---|
| Check gateway bind | openclaw config get gateway.bind |
| Check node status | openclaw nodes status |
| List pending pairings | openclaw devices list |
| Approve a node | openclaw devices approve <id> |
| Run command on node | exec host=node command="..." |
| Check Tailscale | tailscale status |
| Restart gateway | openclaw gateway restart |
| Node logs | ~/.openclaw/logs/node.log |
Should work with any VPS provider and any local machine that can run OpenClaw + Tailscale.
MIT