The Gateway
The Gateway is OpenClaw's core: one long-running Node.js process per host that owns the messaging channels, runs agent turns, and serves a WebSocket + HTTP server on one port (default 127.0.0.1:18789). The CLI, the Control UI, the desktop and mobile apps, and paired nodes are all clients of it.
Official references: Gateway runbook, Gateway architecture, Gateway protocol.
Running the Gateway
# Foreground (logs to stdout)
openclaw gateway run
# Install and start as a service (launchd, systemd user unit, or Windows Scheduled Task)
openclaw gateway install
openclaw gateway status
openclaw gateway restart
openclaw gateway stop
# Foreground on another port, replacing anything bound to it
openclaw gateway run --port 19000 --force
# Debug logging (--log-level is a global flag, so it goes before the command)
openclaw --log-level debug gateway run
openclaw onboard --install-daemon installs the service during setup. The Gateway only starts when gateway.mode is "local"; onboarding sets this. --bind, --auth, --token and --tailscale override the matching config for one run; see openclaw gateway.
The Gateway watches openclaw.json and hot-applies most changes. Some, such as gateway.auth.mode, need a restart (Config hot reload). An invalid edit is skipped and the running config stays in place.
Wire Protocol
Clients talk to the Gateway over WebSocket using JSON text frames, at protocol version 4. For anything you build, prefer the published client packages @openclaw/gateway-client and @openclaw/gateway-protocol over hand-rolled frames (Building a Gateway client).
Frame types
| Type | Direction | Structure | Purpose |
|---|---|---|---|
| Request | Client → Gateway | {type: "req", id, method, params} | RPC calls |
| Response | Gateway → Client | {type: "res", id, ok, payload|error} | RPC replies |
| Event | Gateway → Client | {type: "event", event, payload, seq?, stateVersion?} | Push notifications |
Connection handshake
- Client opens a WebSocket to
ws://127.0.0.1:18789. - Gateway sends a
connect.challengeevent with anonceandts. - Client sends a
connectrequest as its first frame: protocol range (minProtocol/maxProtocol4), client info,role(operatorornode), scopes,auth(for example{ token }), and adeviceblock that signs the challenge nonce. - Gateway replies with
hello-ok, which carries the negotiated protocol, policy limits, and a presence/health snapshot.
A first frame that is not JSON or not connect is a hard close. Events are not replayed, so clients refresh state after a gap. Details: Handshake and roles.
Payload limits
| Phase | Max size |
|---|---|
| Pre-handshake frames | 64 KiB |
Post-handshake (hello-ok.policy.maxPayload) | 25 MiB |
Per-connection send buffer (maxBufferedBytes) | 50 MiB |
Common RPC methods
The protocol has many methods; hello-ok.features.methods lists what a Gateway advertises. Commonly used ones:
| Area | Methods |
|---|---|
| Chat and sessions | chat.send, chat.abort, chat.history, sessions.create, sessions.send, sessions.steer |
| Agent runs | agent, agent.wait |
| Tools | tools.catalog, tools.effective, tools.invoke |
| Exec approvals | exec.approval.request, exec.approval.resolve |
| Heartbeat | last-heartbeat, set-heartbeats, wake |
| Memory | doctor.memory.status |
| Agents | agents.list, agents.create |
| Config | config.get, config.set, config.patch, config.apply |
| Plugins | plugins.list, plugins.install |
| Devices | device.pair.list, device.pair.approve, device.pair.reject, device.pair.remove |
| Talk | talk.session.create, talk.session.close |
| Nodes | node.list, node.describe, node.invoke |
The full list is in RPC methods. From a shell, openclaw gateway call <method> --params '<json>' calls one method with your configured auth:
openclaw gateway call health
openclaw gateway call status --json
Idempotency
Side-effecting methods such as chat.send and agent require an idempotencyKey. The Gateway keeps a short-lived dedupe cache so a retried request is not processed twice.
Example: sending a chat message
This is the shape of a chat.send request, sent after a successful connect. chat.send is a closed object: it takes sessionKey, message and idempotencyKey (plus optional fields), and rejects unknown keys such as text.
ws.send(JSON.stringify({
type: 'req',
id: 'msg-001',
method: 'chat.send',
params: {
sessionKey: 'agent:main:main',
message: 'What files are in my home directory?',
idempotencyKey: 'idem-abc123',
},
}));
// The Gateway acknowledges with {type: "res", id: "msg-001", ok: true, payload: {...}}
// and then streams progress as `chat` and `agent` events
// (assistant deltas, tool events, lifecycle start/end).
Authentication
Gateway auth is required by default, and onboarding generates a token. The Gateway refuses to bind to a non-loopback address without auth.
gateway.auth.mode | How it works | Use case |
|---|---|---|
token | Shared token sent in connect.params.auth.token | Default |
password | Shared password sent in connect.params.auth.password | Human-typed secret |
trusted-proxy | Identity headers from an identity-aware reverse proxy listed in gateway.trustedProxies | Behind an auth proxy (Trusted proxy auth) |
none | No shared-secret auth | Trusted loopback only; never on a reachable interface |
Tailscale is not an auth mode. gateway.tailscale.mode: "serve" plus gateway.auth.allowTailscale lets Tailscale Serve identity satisfy Control UI and WebSocket auth. Send the token in the connect frame, never in a URL query string.
Device identity
Every client also presents an Ed25519 device identity on connect and signs the connect.challenge nonce. Signature payload v3 binds platform and deviceFamily. The allowed clock skew for signedAt is 2 minutes. After the first approval the Gateway issues a device token for later connects (Protocol auth).
Device pairing
- Trusted local (loopback) connections, including SSH port forwards, are approved automatically.
- LAN and tailnet devices need explicit approval:
openclaw devices list, thenopenclaw devices approve <requestId>. openclaw pairing approve <channel> <code>is different: it approves DM pairing for a chat sender, not a device. DM pairing caps pending requests at 3 per channel account (DM pairing).
See Device pairing.
Do not expose the Gateway on a public interface. Keep gateway.bind: "loopback" (the default) and use Tailscale, a VPN, or an SSH tunnel for remote access. If you must use bind: "lan", keep token auth on and firewall the port. Exposed instances have been mass-scanned; see Exposed Instances.
Internal components
Command queue
Agent runs are serialized per session key (a "session lane"), optionally through a global lane as well. Only one run touches a given session at a time, so simultaneous messages from several channels or a scheduled turn cannot race. Channels choose how a message that arrives mid-run is handled (steer, follow-up, collect, interrupt). See Command queue.
Exec approvals
When tools.exec.mode is ask or auto, a command that is not allowlisted becomes an approval record with its exact argv and cwd. Approval clients (CLI, Control UI, apps) resolve it with exec.approval.resolve. The default mode is full, which runs host commands with no prompt. See Agent runtime and tools.
Nodes
Nodes connect to the same WebSocket with role: "node" and declare their commands. The Gateway routes node.invoke calls to them. Nodes are peripherals: channel messages land on the Gateway, not on nodes (Nodes).
Logs and health
- Log file:
/tmp/openclaw/openclaw-YYYY-MM-DD.log(per-profile:openclaw-<profile>-YYYY-MM-DD.log). There is no PID file. See Logging. - Tail logs:
openclaw logs --follow. Other flags:--limit,--json,--plain,--max-bytes. There is no--filterflag, so pipe togrep. - Health:
openclaw health,openclaw gateway status, and the HTTP probes/healthz(liveness),/startupzand/readyz(readiness).
openclaw gateway status
openclaw logs --follow
openclaw logs --limit 500 --plain | grep -i heartbeat
curl -fsS http://127.0.0.1:18789/healthz
Configuration
A validated starting point. The token comes from the OPENCLAW_GATEWAY_TOKEN environment variable (put it in ~/.openclaw/.env). A missing variable stays unresolved and the token counts as unavailable.
{
gateway: {
mode: "local",
port: 18789,
bind: "loopback", // auto | loopback | lan | tailnet | custom
auth: {
mode: "token",
token: "${OPENCLAW_GATEWAY_TOKEN}",
},
trustedProxies: [],
},
logging: { level: "info" },
}
Port precedence: --port > OPENCLAW_GATEWAY_PORT > gateway.port > 18789. For bind: "custom", set gateway.customBindHost. All gateway keys: Configuration — gateway.
Transport security
- Plaintext
ws://is accepted only for loopback, private (RFC 1918), link-local and CGNAT addresses,.localhostnames, and Tailnet*.ts.nethosts. - Everything else requires
wss://(TLS). - Browser SSRF to private networks is blocked by default.
See also
- Architecture Overview: component map
- Gateway API: protocol and HTTP endpoint summary
- API & Webhooks: integration patterns
- Security Hardening: bind, auth, reverse proxy
- Configuration Reference: how the config file works