- default:
http://<host>:18789/ - optional prefix: set
gateway.controlUi.basePath(e.g./openclaw)
Quick open (local)
If the Gateway is running on the same computer, open http://127.0.0.1:18789/ (or http://localhost:18789/). If the page fails to load, start the Gateway first:openclaw gateway.
On native Windows LAN binds, Windows Firewall or organization-managed Group Policy can still block the advertised LAN URL even when
127.0.0.1 works on the Gateway host. Run openclaw gateway status --deep on the Windows host; it reports likely-blocked ports, profile mismatches, and local firewall rules that policy may ignore.connect.params.auth.tokenconnect.params.auth.password- Tailscale Serve identity headers when
gateway.auth.allowTailscale: true - trusted-proxy identity headers when
gateway.auth.mode: "trusted-proxy"
gateway.auth.mode is "password".
Device pairing (first connection)
Connecting from a new browser or device usually requires a one-time pairing approval, shown asdisconnected (1008): pairing required.
If the browser retries pairing with changed auth details (role/scopes/public key), the previous pending request is superseded and a new requestId is created; re-run openclaw devices list before approving.
Switching an already-paired browser from read access to write/admin access is treated as an approval upgrade, not a silent reconnect: OpenClaw keeps the old approval active, blocks the broader reconnect, and asks you to approve the new scope set explicitly.
Once approved, the device is remembered and won’t require re-approval unless you revoke it with openclaw devices revoke --device <id> --role <role>. See Devices CLI for token rotation, revocation, and the Paperclip / openclaw_gateway first-run approval flow.
- Direct local loopback browser connections (
127.0.0.1/localhost) are auto-approved. - Tailscale Serve can skip the pairing round trip for Control UI operator sessions when
gateway.auth.allowTailscale: true, Tailscale identity verifies, and the browser presents its device identity. Device-less browsers and node-role connections still follow the normal device checks. - Direct Tailnet binds, LAN browser connects, and browser profiles without device identity still require explicit approval.
- Each browser profile generates a unique device ID, so switching browsers or clearing browser data requires re-pairing.
Pair a mobile device
An already paired administrator can create the iOS/Android connection QR without opening a terminal:Connect the phone
In the OpenClaw mobile app, open Settings → Gateway and scan the QR code. You can copy and paste the setup code instead.
operator.admin; the button is disabled for sessions without it. A setup code contains a short-lived bootstrap credential, so treat the QR and copied code like a password while they are valid. For remote pairing, the Gateway must resolve to wss:// (for example, through Tailscale Serve/Funnel); plain ws:// is limited to loopback and private LAN addresses. See Pairing for the full security and fallback details.
Personal identity (browser-local)
The Control UI supports a per-browser personal identity (display name and avatar) attached to outgoing messages, for attribution in shared sessions. It lives in browser storage, scoped to the current browser profile, and is not synced to other devices or persisted server-side beyond the normal transcript authorship metadata on messages you send. Clearing site data or switching browsers resets it to empty. The assistant avatar override follows the same browser-local pattern: uploaded overrides overlay the gateway-resolved identity locally and never round-trip throughconfig.patch. The shared ui.assistant.avatar config field is still available for non-UI clients that write the field directly.
Runtime config endpoint
The Control UI fetches its runtime settings from/control-ui-config.json, resolved relative to the gateway’s Control UI base path (for example /__openclaw__/control-ui-config.json under base path /__openclaw__/). That endpoint is gated by the same gateway auth as the rest of the HTTP surface: unauthenticated browsers cannot fetch it, and a successful fetch requires a valid gateway token/password, Tailscale Serve identity, or a trusted-proxy identity.
Gateway host status
Open Settings in Simple view to see the Gateway Host card with the Gateway machine, LAN address, operating system, runtime, uptime, CPU load, memory, and state-volume disk space. The card refreshes every 10 seconds while visible through thesystem.info Gateway RPC, which requires the operator.read scope. Older Gateways and connections without that scope omit the card.
Language support
The Control UI localizes itself on first load based on your browser locale. To override it later, open Overview -> Gateway Access -> Language (the picker lives in the Gateway Access card, not under Appearance).- Supported locales:
en,ar,de,es,fa,fr,hi,id,it,ja-JP,ko,nl,pl,pt-BR,ru,th,tr,uk,vi,zh-CN,zh-TW - Non-English translations are lazy-loaded in the browser.
- The selected locale is saved in browser storage and reused on future visits.
- Missing translation keys fall back to English.
th) and Persian (fa) docs are still generated in the publish repo; they may not appear in that picker until Mintlify supports those codes.
Appearance themes
The Appearance panel has the built-in Claw, Knot, and Dash themes (Claw is default), plus one browser-local tweakcn import slot. To import a theme, open the tweakcn editor, choose or create a theme, click Share, and paste the copied link into Appearance. The importer also acceptshttps://tweakcn.com/r/themes/<id> registry URLs, editor URLs like https://tweakcn.com/editor/theme?theme=amethyst-haze, relative /themes/<id> paths, raw theme IDs, and default theme names such as amethyst-haze.
Imported themes are stored only in the current browser profile; they are not written to gateway config and do not sync across devices. Replacing the imported theme updates the one local slot; clearing it switches back to Claw if the imported theme was active.
Appearance also has a browser-local Text size setting, stored with the rest of Control UI preferences. It applies to chat text, composer text, tool cards, and chat sidebars, and keeps text inputs at least 16px so mobile Safari does not auto-zoom on focus.
Sidebar navigation
The sidebar pins navigation above a scrollable recent-session list split into Pinned, one section per custom group (the sessioncategory, sorted alphabetically), and Ungrouped for the rest. Every pinned session stays visible, while unpinned sessions keep an independent nine-item recent budget. Opening a visible session moves the selection highlight without reordering the rows; an off-list deep link is surfaced at the top. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. Each session row has a context menu (kebab button or right-click) with Pin/Unpin, Mark as unread/read, Rename, Fork, Move to group (including New group and Remove from group), Archive, and Delete; touch layouts keep the direct pin and menu controls visible. Group headers have their own menu (kebab button or right-click) with Rename group, New group, and Delete group; renaming or deleting a group updates every member session, including archived ones, and deleting a group keeps its sessions and moves them back to Ungrouped. Groups created from the header start empty and stay visible as move targets. The sort control in the session list header also has a Group by toggle: Custom groups (default) or None for one flat list (Pinned stays separate); the choice is stored in the current browser profile. Multi-agent setups show a compact scope control in the Ungrouped header. Overview is the only destination pinned by default; expand More to reach every other destination. Select Customize sidebar under More, or right-click the navigation area, to pin or unpin destinations and restore the defaults. The pinned set and More expansion state are stored in the current browser profile and survive reloads.
The compact footer keeps connection status, Settings, Docs, mobile pairing, and the sidebar collapse toggle together. Collapsing shrinks the sidebar to an icon rail with the expand button at the top of the footer stack. At drawer breakpoints, the topbar hamburger button replaces that control.
What it can do (today)
Chat and Talk
Chat and Talk
- Chat with the model via Gateway WS (
chat.history,chat.send,chat.abort,chat.inject). - Chat history refreshes request a bounded recent window with per-message text caps, so large sessions do not force the browser to render a full transcript payload before chat becomes usable.
- Hovering or keyboard-focusing a public GitHub issue or pull request link shows its state, title, author, recent activity, comments, and change statistics. The connected Gateway fetches and caches public metadata without changing the link target, including when the UI uses a remote Gateway. The Gateway uses
GH_TOKENorGITHUB_TOKENwhen available, after confirming the repository is public; otherwise it uses GitHub’s anonymous API with a longer cache. - Talk through browser realtime sessions. OpenAI uses direct WebRTC, Google Live uses a constrained one-use browser token over WebSocket, and backend-only realtime voice plugins use the Gateway relay transport. Client-owned provider sessions start with
talk.client.create; Gateway relay sessions start withtalk.session.create. The relay keeps provider credentials on the Gateway while the browser streams microphone PCM throughtalk.session.appendAudio, forwardsopenclaw_agent_consultprovider tool calls throughtalk.client.toolCallfor Gateway policy and the larger configured OpenClaw model, and routes active-run voice steering throughtalk.client.steerortalk.session.steer. - Stream tool calls and live tool output cards in Chat (agent events).
- Activity tab with browser-local, redaction-first summaries of live tool activity from existing
session.tool/ tool event delivery.
Channels, instances, sessions, dreams
Channels, instances, sessions, dreams
- Channels: built-in plus bundled/external plugin channels status, QR login, and per-channel config (
channels.status,web.login.*,config.patch). - Channel probe refreshes keep the previous snapshot visible while slow provider checks finish, and label partial snapshots when a probe or audit exceeds its UI budget.
- Instances: presence list and refresh (
system-presence). - Sessions: list configured-agent sessions by default, pin frequent sessions, rename them, archive or restore inactive sessions, fall back from stale unconfigured agent session keys, and apply per-session model/thinking/fast/verbose/trace/reasoning overrides (
sessions.list,sessions.patch). Pinned sessions sort above recent unpinned sessions; archived sessions live in the Sessions page’s archived view and keep their transcripts. Rows show an unread dot for sessions with activity since their last read, with mark-unread/mark-read actions (sessions.patch { unread }), and a Fork action that branches the transcript into a new session (sessions.create { parentSessionKey, fork: true }). - Session grouping: a Group by control organizes the sessions table into sections by custom groups, channel, kind, agent, or date. Custom groups persist per session via
sessions.patch(category), so sessions started from message channels (Discord, Telegram, WhatsApp, …) can be categorized too; assign groups by dragging rows onto a section, or with the per-row group selector, and create groups with the New group action. - Dreams: dreaming status, enable/disable toggle, and Dream Diary reader (
doctor.memory.status,doctor.memory.dreamDiary,config.patch).
Cron, tasks, skills, nodes, exec approvals
Cron, tasks, skills, nodes, exec approvals
- Cron jobs: list/add/edit/run/enable/disable plus run history (
cron.*). - Tasks: live active and recent background task ledger with linked sessions and cancellation (
tasks.*). - Skills: status, enable/disable, install, API key updates (
skills.*). - Nodes: list plus caps (
node.list), create mobile setup codes, and approve device pairing (device.pair.*). - Exec approvals: edit gateway or node allowlists and ask policy for
exec host=gateway/node(exec.approvals.*).
Config
Config
- View/edit
~/.openclaw/openclaw.json(config.get,config.set). - MCP has a dedicated settings page for configured servers, enablement, OAuth/filter/parallel summaries, common operator commands, and the scoped
mcpconfig editor. - Apply and restart with validation (
config.apply), then wake the last active session. - Writes include a base-hash guard to prevent clobbering concurrent edits.
- Writes (
config.set/config.apply/config.patch) preflight active SecretRef resolution for refs in the submitted config payload; unresolved active submitted refs are rejected before write. - Form saves discard stale redacted placeholders that cannot be restored from the saved config, while preserving redacted values that still map to saved secrets.
- Schema and form rendering come from
config.schema/config.schema.lookup, including fieldtitle/description, matched UI hints, immediate child summaries, docs metadata on nested object/wildcard/array/composition nodes, plus plugin and channel schemas when available. Raw JSON editor is available only when the snapshot has a safe raw round-trip; otherwise Control UI forces Form mode. - Raw JSON editor “Reset to saved” preserves the raw-authored shape (formatting, comments,
$includelayout) instead of re-rendering a flattened snapshot, so external edits survive a reset when the snapshot can safely round-trip. - Structured SecretRef object values render read-only in form text inputs, to prevent accidental object-to-string corruption.
Usage
Usage
- Session-derived token and estimated-cost analysis stays separate from provider billing.
- Provider cards call
usage.statusand show live plan names, quota windows, balances, spend, and budgets reported by configured provider plugins. - A provider usage failure does not block the session/cost dashboard; unavailable provider cards show their own error state.
Debug, logs, update
Debug, logs, update
- Debug: status/health/models snapshots, event log, and manual RPC calls (
status,health,models.list). - The event log includes Control UI refresh/RPC timings, slow chat/config render timings, and browser responsiveness entries for long animation frames or long tasks when the browser exposes those PerformanceObserver entry types.
- Logs: live tail of gateway file logs with filter/export (
logs.tail). - Update: run a package/git update plus restart (
update.run) with a restart report, then pollupdate.statusafter reconnect to verify the running gateway version.
Cron jobs panel notes
Cron jobs panel notes
- For isolated jobs, delivery defaults to announce summary; switch to none for internal-only runs.
- Channel/target fields appear when announce is selected.
- Webhook mode uses
delivery.mode = "webhook"withdelivery.toset to a valid HTTP(S) webhook URL. - For main-session jobs, webhook and none delivery modes are available.
- Advanced edit controls include delete-after-run, clear agent override, cron exact/stagger options, agent model/thinking overrides, and best-effort delivery toggles.
- Form validation is inline with field-level errors; invalid values disable the save button until fixed.
- Set
cron.webhookTokento send a dedicated bearer token; if omitted, the webhook is sent without an auth header. cron.webhookis a deprecated legacy fallback: runopenclaw doctor --fixto migrate stored jobs that still usenotify: trueto explicit per-job webhook or completion delivery.
MCP page
The dedicated MCP page is an operator view for OpenClaw-managed MCP servers undermcp.servers. It does not start MCP transports by itself; use it to inspect and edit saved config, then use openclaw mcp doctor --probe when you need live server proof.
Typical workflow:
- Open MCP from the sidebar.
- Check the summary cards for total, enabled, OAuth, and filtered server counts.
- Review each server row for transport, enablement, auth, filters, timeouts, and command hints.
- Toggle enablement when a server should remain configured but stay out of runtime discovery.
- Edit the scoped
mcpconfig section for server definitions, headers, TLS/mTLS paths, OAuth metadata, tool filters, and Codex projection metadata. - Use Save for a config write, or Save & Publish when the running Gateway should apply the changed config.
- Run
openclaw mcp status --verbose,openclaw mcp doctor --probe, oropenclaw mcp reloadfrom a terminal for static diagnostics, live proof, or cached-runtime disposal.
Activity tab
The Activity tab is an ephemeral browser-local observer for live tool activity, derived from the same Gatewaysession.tool / tool event stream that powers Chat tool cards. It does not add another Gateway event family, endpoint, durable activity store, metrics feed, or external observer stream.
Activity entries keep only sanitized summaries and redacted, truncated output previews. Tool argument values are not stored in Activity state; the UI shows that arguments are hidden and records only the argument field count. The in-memory list follows the current browser tab, survives navigation within the Control UI, and resets on page reload, session switch, or Clear.
Operator terminal
The dockable operator terminal is disabled by default. To enable it, setgateway.terminal.enabled: true and restart the Gateway. The terminal requires an operator.admin connection and opens a host PTY in the active agent workspace. New tabs follow the currently selected chat agent.
Use Ctrl + backtick to toggle the dock. The layout supports bottom and right docking, resizes with the browser viewport, and keeps multiple shell tabs. See Gateway configuration for gateway.terminal.enabled and the optional gateway.terminal.shell override.
Sessions survive disconnects: a page reload, laptop sleep, or network blip detaches the session on the Gateway instead of killing it, and the same browser tab reattaches on reconnect with recent output replayed. Detached sessions are killed after gateway.terminal.detachedSessionTimeoutSeconds (default 300 seconds; 0 restores kill-on-disconnect). terminal.list shows attachable sessions, terminal.attach adopts one (tmux-style take-over), and terminal.text reads a session’s recent output as plain text without attaching - an agent/tooling affordance.
The terminal is also available as a full-screen, terminal-only document at /?view=terminal. The iOS and Android apps embed this page in their Terminal screens, reusing the stored gateway credentials; availability follows the same gateway.terminal.enabled and operator.admin gate, and the page shows a notice when the connected Gateway does not offer the terminal.
Chat behavior
Send and history semantics
Send and history semantics
chat.sendis non-blocking: it acks immediately with{ runId, status: "started" }and the response streams viachatevents. Trusted Control UI clients may also receive optional ACK timing metadata for local diagnostics.- Chat uploads accept images plus non-video files. Images keep the native image path; other files are stored as managed media and shown in history as attachment links.
- Re-sending with the same
idempotencyKeyreturns{ status: "in_flight" }while running, and{ status: "ok" }after completion. chat.historyresponses are size-bounded for UI safety. When transcript entries are too large, Gateway may truncate long text fields, omit heavy metadata blocks, and replace oversized messages with a placeholder ([chat.history omitted: message too large]).- When a visible assistant message was truncated in
chat.history, the side reader can fetch the full display-normalized transcript entry on demand throughchat.message.getbysessionKey, activeagentIdwhen needed, and transcriptmessageId. If the Gateway still cannot return more, the reader shows an explicit unavailable state instead of silently repeating the truncated preview. - Assistant/generated images are persisted as managed media references and served back through authenticated Gateway media URLs, so reloads do not depend on raw base64 image payloads staying in the chat history response.
- When rendering
chat.history, the Control UI strips display-only inline directive tags from visible assistant text (for example[[reply_to_*]]and[[audio_as_voice]]), plain-text tool-call XML payloads (including<tool_call>...</tool_call>,<function_call>...</function_call>,<tool_calls>...</tool_calls>,<function_calls>...</function_calls>, and truncated tool-call blocks), and leaked ASCII/full-width model control tokens. It omits assistant entries whose whole visible text is only the exact silent tokenNO_REPLY/no_replyor the heartbeat acknowledgement tokenHEARTBEAT_OK. - During an active send and the final history refresh, the chat view keeps local optimistic user/assistant messages visible if
chat.historybriefly returns an older snapshot; the canonical transcript replaces those local messages once the Gateway history catches up. - Live
chatevents are delivery state, whilechat.historyis rebuilt from the durable session transcript. After tool-final events the Control UI reloads history and merges only a small optimistic tail; the transcript boundary is documented in WebChat. chat.injectappends an assistant note to the session transcript and broadcasts achatevent for UI-only updates (no agent run, no channel delivery).- The sidebar lists recent sessions by pinned/custom/ungrouped section with a New Session action and an All Sessions link. Pinned sessions always stay visible; unpinned sessions keep a nine-item budget and a stable recency order, so opening a visible row moves only the highlight. A new dashboard session asynchronously gets a concise generated title from its first non-command message; explicit names are never replaced. Set
agents.defaults.utilityModel(oragents.list[].utilityModel) to route this separate model call to a lower-cost model. Switching the compact agent scope shows only sessions tied to that agent and falls back to that agent’s main session when it has no saved dashboard sessions yet. - Session search lives in the command palette (⌘K, or the search button in the top bar): typing a query follows a bounded number of matching pages across agents, filters internal child/cron rows, and lists visible matches next to navigation commands. The All Sessions page keeps the exhaustive searchable list with filters.
- Each sidebar row keeps direct pin access plus a full context menu for unread state, rename, fork, grouping, archive, and delete. An active run and an agent’s main session cannot be archived. Archiving or deleting the currently selected session switches Chat back to that agent’s main session.
- In the macOS app, the OpenClaw mark uses the otherwise-empty native titlebar strip next to the window controls instead of consuming a sidebar row.
- On desktop widths, chat controls stay on one compact row and collapse while scrolling down the transcript; scrolling up, returning to the top, or reaching the bottom restores the controls.
- Consecutive duplicate text-only messages render as one bubble with a count badge. Messages that carry images, attachments, tool output, or canvas previews are left uncollapsed.
- The chat header model and thinking pickers patch the active session immediately through
sessions.patch; they are persistent session overrides, not one-turn-only send options. - Split view: open it from the composer controls, then split any pane right or down for as many panes as fit. Each pane has its own session, transcript, composer, and tool stream.
- Drag a session from the sidebar into chat to open it in a pane. An animated drop preview glides between zones and labels the outcome — “Split” over the exact half a new pane will occupy, “Open here” over a whole pane — and drops also work from single-pane mode.
- The active split pane drives the sidebar selection and URL. Dividers resize columns and stacked panes, and the browser stores the layout locally across reloads.
- On narrow screens, split view keeps the layout but renders only the active pane; its pane header still provides session switching and close controls.
- If you send a message while a model picker change for the same session is still saving, the composer waits for that session patch before calling
chat.sendso the send uses the selected model. - Typing
/newcreates and switches to the same fresh dashboard session as New Chat, except whensession.dmScope: "main"is configured and the current parent is the agent’s main session; then it resets the main session in place. Typing/resetkeeps the Gateway’s explicit in-place reset for the current session. - The chat model picker requests the Gateway’s configured model view. If
agents.defaults.modelsis present, that allowlist drives the picker, includingprovider/*entries that keep provider-scoped catalogs dynamic. Otherwise the picker shows explicitmodels.providers.*.modelsentries plus providers with usable auth. The full catalog stays available through the debugmodels.listRPC withview: "all". - When fresh Gateway session usage reports include current context tokens, the chat composer toolbar shows a small context usage ring with the used percentage. Open the ring for the current context window, latest-run token counts and estimated total cost, provider/model identity, and the latest provider response’s input/output/cache cost breakdown when reported. The ring switches to warning styling at high context pressure and, at recommended compaction levels, shows a compact button that runs the normal session compaction path. Stale token snapshots are hidden until the Gateway reports fresh usage again.
Talk mode (browser realtime)
Talk mode (browser realtime)
Talk mode uses a registered realtime voice provider. Configure OpenAI with
talk.realtime.provider: "openai" plus an openai API-key/OAuth profile, an external Codex login, talk.realtime.providers.openai.apiKey, or OPENAI_API_KEY. Configured API-key sources take precedence and Codex OAuth is the automatic fallback. Configure Google with talk.realtime.provider: "google" plus talk.realtime.providers.google.apiKey. The browser never receives a standard provider API key or OAuth token: OpenAI receives an ephemeral Realtime client secret for WebRTC, and Google Live receives a one-use constrained Live API auth token for a browser WebSocket session, with instructions and tool declarations locked into the token by the Gateway. Providers that only expose a backend realtime bridge run through the Gateway relay transport, so credentials and vendor sockets stay server-side while browser audio moves through authenticated Gateway RPCs. The Realtime session prompt is assembled by the Gateway; talk.client.create does not accept caller-provided instruction overrides.Persistent provider, model, voice, transport, reasoning effort, exact VAD threshold, silence duration, and prefix padding defaults live in Settings → Communications → Talk; changing them requires operator.admin access. Configuring Gateway relay forces the backend relay path; configuring WebRTC keeps the session client-owned and fails instead of silently falling back to relay if the provider cannot create a browser session.The Talk control itself is the microphone button in the composer toolbar. Its caret lists System default and every microphone exposed by the browser, including USB, Bluetooth, and virtual inputs. The selected device ID stays browser-local and is never sent to the Gateway; if that exact device disappears, Talk asks you to choose another input instead of silently recording from a different microphone. When Talk starts, the composer status row shows Connecting Talk..., then Talk live while audio is connected, or Asking OpenClaw... while a realtime tool call is consulting the configured larger model through talk.client.toolCall.Maintainer live smoke: OPENAI_API_KEY=... GEMINI_API_KEY=... node --import tsx scripts/dev/realtime-talk-live-smoke.ts verifies the OpenAI backend WebSocket bridge, OpenAI browser WebRTC SDP exchange, Google Live constrained-token browser WebSocket setup, and the Gateway relay browser adapter with fake microphone media. The command prints provider status only and does not log secrets.Stop and abort
Stop and abort
- Click Stop (calls
chat.abort). - While a run is active, normal follow-ups queue. Click Steer on a queued message to inject that follow-up into the running turn.
- Type
/stop(or standalone abort phrases likestop,stop action,stop run,stop openclaw,please stop) to abort out-of-band. chat.abortsupports{ sessionKey }(norunId) to abort all active runs for that session.
Abort partial retention
Abort partial retention
- When a run is aborted, partial assistant text can still be shown in the UI.
- Gateway persists aborted partial assistant text into transcript history when buffered output exists.
- Persisted entries include abort metadata so transcript consumers can tell abort partials from normal completion output.
Connection loss and reconnect
Once a session is established, a dropped Gateway connection does not log you out. The dashboard stays visible with an amber “Gateway connection lost — reconnecting…” banner while the client retries automatically with backoff (800 ms up to 15 s). Live updates and actions pause until the connection returns; Retry now in the banner forces an immediate attempt. The login gate only appears when there is no established session yet (first open, page reload before connect) or when the Gateway actively rejects the credentials (bad token/password, revoked pairing) — states that need your input rather than waiting.PWA install and web push
The Control UI ships amanifest.webmanifest and a service worker, so modern browsers can install it as a standalone PWA. Web Push lets the Gateway wake the installed PWA with notifications even when the tab or browser window is not open.
If the page shows Protocol mismatch right after an OpenClaw update, first reopen the dashboard with openclaw dashboard and hard-refresh. If it still fails, clear site data for the dashboard origin or test in a private browser window; an old tab or browser service-worker cache can keep running a pre-update Control UI bundle against the newer Gateway.
| Surface | What it does |
|---|---|
ui/public/manifest.webmanifest | PWA manifest. Browsers offer “Install app” once it is reachable. |
ui/public/sw.js | Service worker that handles push events and notification clicks. |
push/vapid-keys.json (under the OpenClaw state dir) | Auto-generated VAPID keypair used to sign Web Push payloads. |
push/web-push-subscriptions.json | Persisted browser subscription endpoints. |
OPENCLAW_VAPID_PUBLIC_KEYOPENCLAW_VAPID_PRIVATE_KEYOPENCLAW_VAPID_SUBJECT(defaults tohttps://openclaw.ai)
push.web.vapidPublicKeyfetches the active VAPID public key.push.web.subscriberegisters anendpointpluskeys.p256dh/keys.auth.push.web.unsubscriberemoves a registered endpoint.push.web.testsends a test notification to the caller’s subscription.
Web Push is independent of the iOS APNS relay path (see Configuration for relay-backed push) and the
push.test method, which targets native mobile pairing.Hosted embeds
Assistant messages can render hosted web content inline with the[embed ...] shortcode. The iframe sandbox policy is controlled by gateway.controlUi.embedSandbox:
- strict
- scripts (default)
- trusted
Disables script execution inside hosted embeds.
http(s) embed URLs stay blocked by default. To let [embed url="https://..."] load third-party pages, set gateway.controlUi.allowExternalEmbedUrls: true.
Chat message width
Grouped chat messages use a readable default max-width. Wide-monitor deployments can override it without patching bundled CSS by settinggateway.controlUi.chatMessageMaxWidth:
960px or 82%, plus constrained min(...), max(...), clamp(...), calc(...), and fit-content(...) width expressions.
Tailnet access (recommended)
- Integrated Tailscale Serve (preferred)
- Bind to tailnet + token
Keep the Gateway on loopback and let Tailscale Serve proxy it with HTTPS:Open
https://<magicdns>/ (or your configured gateway.controlUi.basePath).By default, Control UI/WebSocket Serve requests can authenticate via Tailscale identity headers (tailscale-user-login) when gateway.auth.allowTailscale is true. OpenClaw verifies the identity by resolving the x-forwarded-for address with tailscale whois and matching it to the header, and only accepts these when the request hits loopback with Tailscale’s x-forwarded-* headers. For Control UI operator sessions with browser device identity, this verified Serve path also skips the device-pairing round trip; device-less browsers and node-role connections still follow the normal device checks. Set gateway.auth.allowTailscale: false if you want to require explicit shared-secret credentials even for Serve traffic, then use gateway.auth.mode: "token" or "password".For that async Serve identity path, failed auth attempts for the same client IP and auth scope are serialized before rate-limit writes. Concurrent bad retries from the same browser can therefore show retry later on the second request instead of two plain mismatches racing in parallel.Insecure HTTP
If you open the dashboard over plain HTTP (http://<lan-ip> or http://<tailscale-ip>), the browser runs in a non-secure context and blocks WebCrypto. By default, OpenClaw blocks Control UI connections without device identity.
Documented exceptions:
- localhost-only insecure HTTP compatibility with
gateway.controlUi.allowInsecureAuth=true - successful operator Control UI auth through
gateway.auth.mode: "trusted-proxy" - break-glass
gateway.controlUi.dangerouslyDisableDeviceAuth=true
https://<magicdns>/ (Serve) or http://127.0.0.1:18789/ (on the gateway host).
Insecure-auth toggle behavior
Insecure-auth toggle behavior
allowInsecureAuth is a local compatibility toggle only:- It lets localhost Control UI sessions proceed without device identity in non-secure HTTP contexts.
- It does not bypass pairing checks.
- It does not relax remote (non-localhost) device identity requirements.
Break-glass only
Break-glass only
Trusted-proxy note
Trusted-proxy note
- Successful trusted-proxy auth can admit operator Control UI sessions without device identity.
- This does not extend to node-role Control UI sessions.
- Same-host loopback reverse proxies still do not satisfy trusted-proxy auth; see Trusted proxy auth.
Content security policy
The Control UI ships a tightimg-src policy: only same-origin assets, data: URLs, and locally generated blob: URLs are allowed. Remote http(s) and protocol-relative image URLs are rejected by the browser and never issue network fetches.
In practice:
- Avatars and images served under relative paths (for example
/avatars/<id>) still render, including authenticated avatar routes the UI fetches and converts into localblob:URLs. - Inline
data:image/...URLs still render. - Local
blob:URLs created by the Control UI still render. - GitHub link preview avatars are fetched by the Gateway from GitHub’s fixed avatar host and returned as bounded
data:URLs; the operator browser never contacts the remote avatar host. - Remote avatar URLs emitted by channel metadata are stripped at the Control UI’s avatar helpers and replaced with the built-in logo/badge, so a compromised or malicious channel cannot force arbitrary remote image fetches from an operator browser.
Avatar route auth
When gateway auth is configured, the Control UI avatar endpoint requires the same gateway token as the rest of the API:GET /avatar/<agentId>returns the avatar image only to authenticated callers.GET /avatar/<agentId>?meta=1returns the avatar metadata under the same rule.- Unauthenticated requests to either route are rejected (matching the sibling assistant-media route), so the avatar route cannot leak agent identity on hosts that are otherwise protected.
- The Control UI forwards the gateway token as a bearer header when fetching avatars, and uses authenticated blob URLs so the image still renders in dashboards.
Assistant media route auth
When gateway auth is configured, assistant local-media previews use a two-step route:GET /__openclaw__/assistant-media?meta=1&source=<path>requires the normal Control UI operator auth; the browser sends the gateway token as a bearer header when checking availability.- Successful metadata responses include a short-lived
mediaTicketscoped to that exact source path. - Browser-rendered image, audio, video, and document URLs use
mediaTicket=<ticket>instead of the active gateway token or password. The ticket expires quickly and cannot authorize a different source.
Building the UI
The Gateway serves static files fromdist/control-ui:
ws://127.0.0.1:18789).
Blank Control UI page
If the browser loads a blank dashboard and DevTools shows no useful error, an extension or early content script may have prevented the JavaScript module app from evaluating. The static page includes a plain HTML recovery panel that appears when<openclaw-app> is not registered after startup.
Use the panel’s Try again action after changing the browser environment, or reload manually after these checks:
- Disable extensions that inject into all pages, especially extensions with
<all_urls>content scripts. - Try a private window, a clean browser profile, or another browser.
- Keep the Gateway running and verify the same dashboard URL after the browser change.
Debugging/testing: dev server + remote Gateway
The Control UI is static files; the WebSocket target is configurable and can differ from the HTTP origin. This is handy when you want the Vite dev server locally but the Gateway runs elsewhere.Notes
Notes
gatewayUrlis stored in localStorage after load and removed from the URL.- If you pass a full
ws://orwss://endpoint viagatewayUrl, URL-encode the value so the browser parses the query string correctly. tokenshould be passed via the URL fragment (#token=...) whenever possible. Fragments are not sent to the server, which avoids request-log and Referer leakage. Legacy?token=query params are still imported once for compatibility, but only as a fallback, and are stripped immediately after bootstrap.passwordis kept in memory only.- When
gatewayUrlis set, the UI does not fall back to config or environment credentials. Providetoken(orpassword) explicitly; missing explicit credentials is an error. - Use
wss://when the Gateway is behind TLS (Tailscale Serve, HTTPS proxy, etc.). gatewayUrlis only accepted in a top-level window (not embedded), to prevent clickjacking.- Public non-loopback Control UI deployments must set
gateway.controlUi.allowedOriginsexplicitly (full origins). Private same-origin LAN/Tailnet loads from loopback, RFC1918/link-local,.local,.ts.net, or Tailscale CGNAT hosts are accepted without enabling Host-header fallback. - Gateway startup may seed local origins such as
http://localhost:<port>andhttp://127.0.0.1:<port>from the effective runtime bind and port, but remote browser origins still need explicit entries. - Do not use
gateway.controlUi.allowedOrigins: ["*"]except for tightly controlled local testing; it means allow any browser origin, not “match whatever host I am using.” gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=trueenables Host-header origin fallback mode, but it is a dangerous security mode.
Related
- Dashboard — gateway dashboard
- Health Checks — gateway health monitoring
- TUI — terminal user interface
- WebChat — browser-based chat interface