This guide is for the case where Nerve is already installed and working, and you want to add private remote access afterward.
Use one of these two paths:
- Tailnet IP: quickest path, Nerve listens on the Tailscale IP and you open
http://100.x.y.z:3080 - Tailscale Serve: better default for phones and voice input, Nerve stays on
127.0.0.1and Tailscale exposeshttps://<node>.tail<id>.ts.net
If you are starting from scratch, use the normal installer/setup flow first, then come back here only if you need to retrofit Tailscale onto an existing machine.
Make sure all of this is already true:
- Nerve starts locally and
curl http://127.0.0.1:3080/healthworks - OpenClaw gateway is healthy and
openclaw gateway statusworks - Tailscale is installed on the Nerve machine
- Tailscale is logged in on the Nerve machine and on the client device you want to use
- You know where your Nerve install lives, default is usually
~/nerve
Back up your current config first:
cd ~/nerve
cp .env .env.before-tailscale.bak
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.before-tailscale.bakChoose Tailnet IP if:
- you want the simplest possible setup
- plain HTTP on the tailnet is fine
- you are okay with Nerve binding to
0.0.0.0
Choose Tailscale Serve if:
- you want Nerve to stay private on localhost
- you want an HTTPS URL for phone access
- you want the least surprising path for microphone access on mobile browsers
This exposes Nerve on the machine's Tailscale IP and patches both Nerve and the gateway to allow that origin.
tailscale ip -4Example output:
100.64.0.42Save that value, this guide calls it <tailscale-ip> below.
Open ~/nerve/.env and make sure these values are set:
HOST=0.0.0.0
ALLOWED_ORIGINS=http://<tailscale-ip>:3080
CSP_CONNECT_EXTRA=http://<tailscale-ip>:3080 ws://<tailscale-ip>:3080
WS_ALLOWED_HOSTS=<tailscale-ip>
NERVE_AUTH=trueNotes:
HOST=0.0.0.0is required for direct tailnet-IP accessNERVE_AUTH=trueis strongly recommended whenever Nerve is reachable over the network, including Tailscale- if you do not already have a password hash configured, Nerve accepts the
GATEWAY_TOKENas a fallback password - if
ALLOWED_ORIGINSorCSP_CONNECT_EXTRAalready contains other values you still need, append instead of replacing
Add the same origin to ~/.openclaw/openclaw.json:
ORIGIN="http://<tailscale-ip>:3080" node - <<'NODE'
const fs = require('fs');
const path = `${process.env.HOME}/.openclaw/openclaw.json`;
const origin = process.env.ORIGIN;
const cfg = JSON.parse(fs.readFileSync(path, 'utf8'));
cfg.gateway ??= {};
cfg.gateway.controlUi ??= {};
const existing = cfg.gateway.controlUi.allowedOrigins || [];
cfg.gateway.controlUi.allowedOrigins = [...new Set([...existing, origin])];
fs.writeFileSync(path, `${JSON.stringify(cfg, null, 2)}\n`);
console.log(`Added ${origin} to ${path}`);
NODEsudo systemctl restart nerve.service
openclaw gateway restartOn the Nerve machine:
curl -fsS http://127.0.0.1:3080/health
openclaw gateway statusFrom another Tailscale-connected device, open:
http://<tailscale-ip>:3080
Expected result:
- the page loads
- login works
- sessions load
- chat connects without origin errors
This keeps Nerve on localhost and lets Tailscale publish a private HTTPS URL.
On the Nerve machine:
tailscale serve --bg http://127.0.0.1:3080tailscale serve status --json | node - <<'NODE'
let text = '';
process.stdin.on('data', chunk => text += chunk);
process.stdin.on('end', () => {
const data = JSON.parse(text || '{}');
const key = Object.keys(data.Web || {})[0];
if (!key) {
console.error('No Tailscale Serve web origin found');
process.exit(1);
}
const host = key.replace(/:\d+$/, '');
console.log(`https://${host}`);
});
NODEExample output:
https://example-node.tail0000.ts.net
Save that value, this guide calls it <serve-origin> below.
Open ~/nerve/.env and make sure these values are set:
HOST=127.0.0.1
ALLOWED_ORIGINS=<serve-origin>
CSP_CONNECT_EXTRA=<serve-origin> wss://<serve-host>
NERVE_AUTH=trueWhere <serve-host> is the hostname without https://.
Example:
HOST=127.0.0.1
ALLOWED_ORIGINS=https://example-node.tail0000.ts.net
CSP_CONNECT_EXTRA=https://example-node.tail0000.ts.net wss://example-node.tail0000.ts.net
NERVE_AUTH=trueNotes:
- if
HOSTis missing entirely, Nerve defaults to localhost, which is also fine - remove stale
WS_ALLOWED_HOSTSif you previously used tailnet-IP mode and are switching to Serve-only access NERVE_AUTH=trueis still recommended, even though Serve is private by default
Add the same Serve origin to ~/.openclaw/openclaw.json:
ORIGIN="<serve-origin>" node - <<'NODE'
const fs = require('fs');
const path = `${process.env.HOME}/.openclaw/openclaw.json`;
const origin = process.env.ORIGIN;
const cfg = JSON.parse(fs.readFileSync(path, 'utf8'));
cfg.gateway ??= {};
cfg.gateway.controlUi ??= {};
const existing = cfg.gateway.controlUi.allowedOrigins || [];
cfg.gateway.controlUi.allowedOrigins = [...new Set([...existing, origin])];
fs.writeFileSync(path, `${JSON.stringify(cfg, null, 2)}\n`);
console.log(`Added ${origin} to ${path}`);
NODEsudo systemctl restart nerve.service
openclaw gateway restartOn the Nerve machine:
curl -fsS http://127.0.0.1:3080/health
openclaw gateway status
tailscale serve statusFrom another Tailscale-connected device, open:
<serve-origin>
Expected result:
- the page loads over HTTPS
- login works
- chat connects without
origin not allowed - phone access works without exposing Nerve directly on
0.0.0.0
If you switch modes later, update both layers:
- Nerve
.env - OpenClaw
gateway.controlUi.allowedOrigins
Common cleanup when switching to Serve:
- change
HOSTback to127.0.0.1 - replace IP-based
ALLOWED_ORIGINS - replace IP-based
CSP_CONNECT_EXTRA - remove
WS_ALLOWED_HOSTSif you no longer need direct IP access
Common cleanup when switching to Tailnet IP:
- set
HOST=0.0.0.0 - replace
ALLOWED_ORIGINSwith the IP origin - replace
CSP_CONNECT_EXTRAwith the IP origin +ws://... - set
WS_ALLOWED_HOSTS=<tailscale-ip>
Cause:
- the Serve or tailnet origin is missing from
gateway.controlUi.allowedOrigins
Fix:
- patch
~/.openclaw/openclaw.json - restart the gateway
Cause:
- the browser origin is missing from
ALLOWED_ORIGINS - or you kept stale
WS_ALLOWED_HOSTS/HOSTvalues from the other mode
Fix:
- clean up
.envso it matches the mode you actually want - restart Nerve
Use Tailscale Serve, not plain http://<tailscale-ip>:3080.
Mobile browsers are much happier with HTTPS for microphone access.
- Do not expose OpenClaw gateway port
18789publicly just because Nerve is on Tailscale - Keep
NERVE_AUTH=truefor any non-localhost access - If you shared gateway tokens while debugging, rotate them afterward
If you only need one answer:
- use Tailnet IP for the fastest manual retrofit
- use Tailscale Serve for the cleanest long-term remote setup, especially on phone