The Control UI is a small Vite + Lit single-page app served by the Gateway:
- default:
http://<host>:18789/ - optional prefix: set
gateway.controlUi.basePath(e.g./openclaw)
It speaks directly to the Gateway WebSocket on the same port.
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.
Auth is supplied during the WebSocket handshake via:
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"
The dashboard settings panel keeps a token for the current browser tab session and selected gateway URL; passwords are not persisted. Onboarding usually generates a gateway token for shared-secret auth on first connect, but password auth works too when gateway.auth.mode is "password".
Device pairing (first connection)
Connecting from a new browser or device usually requires a one-time pairing approval, shown as disconnected (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.
Pair a mobile device
An already paired administrator can create the iOS/Android connection QR without opening a terminal:
Creating a setup code requires 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 through config.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 → General 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 the system.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 Settings -> General -> Language (the picker lives on the General page, 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.
Docs translations are generated for the same non-English locale set, but the docs site's built-in Mintlify language picker only lists locale codes Mintlify accepts. Thai (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 accepts https://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 Text size setting. 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.
Theme, theme mode, text size, language, and chat display preferences sync through the gateway config (ui.prefs), so they follow you across devices and agents can change them through the approval gate — connected clients apply changes live via the gateway's config.changed notice. Each browser keeps a local mirror for instant boot; clients that cannot write config (viewer scope, offline) keep changes device-local. See Configuration reference.
OpenClaw system care
Open OpenClaw in the sidebar to talk to the system setup and repair agent. Outside onboarding, this page can show at most one dismissible event chip per visit. It stays silent for routine Gateway traffic and reacts only to health snapshots that report a disabled configuration reloader, a configured channel disconnect/degradation, a failed channel probe, or unavailable channel credentials. A newer event replaces the pending chip only when it is more severe; dismissing or using the chip silences event prompts for that visit. Clicking the chip sends its diagnosis question as a real openclaw.chat message, so the transcript records the request and OpenClaw performs the diagnosis. Onboarding never shows these event chips.
Manage plugins
Open Plugins in the sidebar, or use /settings/plugins relative to the
configured Control UI base path, to browse and manage plugins without leaving
the Control UI. For example, a base path of /openclaw uses
/openclaw/settings/plugins. The page is always available, even when every
optional plugin is disabled.
Plugins is a hub with four tabs: Installed and Discover manage plugin
code at /settings/plugins, Skills hosts the per-agent skill manager at
/skills, and Workshop hosts Skill Workshop proposal review at
/skills/workshop. Each tab keeps its own URL, and the sidebar shows the
single Plugins entry for all of them.
The Installed tab shows the full local inventory grouped by category, with
overview counts. Each row opens a detail view; its overflow (…) menu enables
or disables the plugin and offers Remove for externally installed plugins.
It also lists configured MCP servers and supports adding, disabling,
and removing them inline. The same server controls live on Settings → MCP.
The Discover tab is the store: featured plugins included with OpenClaw,
official external plugins, and one-click MCP connectors for popular services.
Typing in the search box queries
ClawHub inline and appends a From ClawHub
section with download counts and source-verification badges. Deep links can
target the store directly with /settings/plugins?tab=discover.
The Skills tab keeps the skill status report, enable/disable toggles, API key entry, and inline ClawHub skill search, scoped to the selected agent. The Workshop tab keeps the Skill Workshop board and Today review flow for skill proposals. Find skill ideas reviews a bounded window of substantial sessions from newest to oldest and leaves any results as pending proposals. The panel shows cumulative coverage; Scan earlier work continues from the persisted cursor, then becomes Scan new work after older history is exhausted. Manual history review works while autonomous self-learning is disabled and uses the selected agent's configured model.
Included plugins are already present on the Gateway and show Enable or Disable instead of Install. For example, Workboard is included with OpenClaw but disabled by default, so its action is Enable. Bundled plugins cannot be removed, only disabled.
Reading the catalog and searching ClawHub require operator.read. Installing,
enabling, disabling, or removing a plugin and changing MCP servers require
operator.admin; those actions stay disabled for read-only operators.
ClawHub installs run through the Gateway and keep the same trust, integrity,
and plugin-install policy checks as other Gateway-mediated installs. Installing
or removing plugin code requires a Gateway restart. Enabling or disabling an
installed plugin can apply without a restart when the plugin and current
Gateway runtime support it; otherwise the UI reports that a restart is
required. OAuth-backed MCP connectors need a one-time
openclaw mcp login <name> from the CLI after they are added.
The page intentionally focuses on inventory, discovery, install, enablement,
and removal. Use openclaw plugins for arbitrary npm, git, or
local-path sources, updates, and advanced plugin configuration.
Apps and extensions
Open Apps from the sidebar More menu, the command palette, or the
sidebar agent menu (Get the apps), or use /apps relative to the
configured Control UI base path. The page collects install links for every
OpenClaw companion surface: the iOS and
Android apps, the Apple Watch and Wear OS companions
bundled with them, the macOS, Windows,
and Linux desktop apps, the
Chrome extension, the in-app Plugins hub with
ClawHub, and the Discord community and docs.
Sidebar navigation
The sidebar organizes everything around the agent. The identity row at the top is the active agent; below it, the Pages section starts with Home — the agent's rolling main session, badged with its unread or running state — followed by the pinned destinations (Usage, Automations, and Plugins by default). The customize control on the Pages header opens a menu with every other destination, including plugin-provided tabs, plus Edit pinned items; right-clicking the navigation area opens the pin editor directly. The session list below splits into zones: Threads for the agent's chat sessions (the main session stays behind Home; sessions it spawned appear here as top-level threads, and named threads show without a type prefix), Groups for group and room conversations, and Coding for sessions bound to a managed worktree or exec node (rows show a repo ⎇ branch line plus the node host), ACP-backed harness sessions, and the Codex/Claude CLI catalogs. Coding starts collapsed on first run and remembers your choice; its collapsed header keeps the true count and shows a running indicator while contained sessions work. Custom groups (the session category) and Pinned rows sit above Threads, and assigning a session to a custom group always wins over the automatic zone classification. The Threads header holds the sort control (Created or Last updated, plus a Group by toggle) and the + that opens the New session page. Opening a session moves the selection highlight without reordering rows. Parent sessions with recent child runs show a disclosure and child count; expand it to inspect nested child sessions, live or terminal status, and runtime without leaving the sidebar. Selecting a child opens its chat and automatically reveals its ancestor path. Child rows stay outside root grouping, pinning, dragging, multi-select, and pagination; collapsed zones do not consume the visible page budget. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. Cloud-worker lifecycle states use a globe badge; local and reclaimed sessions omit a placement badge because local execution is the default. Each root 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. Cmd/Ctrl-click toggles root rows into a multi-select and Shift-click extends it across the visible order; opening the menu on a selected row then offers batch actions (Mark N as unread/read, Move N to group, Archive N, Delete N) that apply to every selected session, with a single confirmation for batch delete. Drag a root session onto Pinned to pin it, or onto a custom group to move it. Custom group headers can be collapsed, expanded, or dragged to reorder them; group names and their order live in the gateway (sessions.groups.*), so they follow you across browsers, while collapsed state stays in the browser profile. Group headers also have a menu (kebab button or right-click) with Rename group, New group, and Delete group; renaming or deleting a group updates every member session server-side, including archived ones, and deleting a group keeps its sessions and moves them back to Threads.
New session page
The + in the sidebar session-list header opens a full-page draft at /new: nothing is created until you send the first message. A target row above the message box picks where the session works: the agent (multi-agent setups), where exec runs (Gateway · local or a paired node that exposes system.run; requires operator.admin), the folder (defaults to the agent workspace; other absolute Gateway paths require operator.admin and a worktree), and an optional Worktree toggle with a base-branch picker (backed by worktrees.branches, so no fetch happens) and an optional worktree name (the branch becomes openclaw/<name>). The composer footer chooses the new session's model and reasoning level, and cloud starts persist both choices before dispatching the session to its worker. The folder chip's browse button opens an inline directory picker backed by the admin-only fs.listDir method. Its top level shows the Gateway and every known node; offline nodes and nodes without directory-browsing support stay visible but disabled. Selecting the Gateway starts from the current folder or Gateway home. Selecting a capable node browses that node's host filesystem, binds exec to it, and uses the selected absolute node path directly (managed worktrees remain Gateway-only). Submitting calls sessions.create with the first message, so the run starts in the same round-trip and the UI jumps to the new session's chat. If the Gateway creates the session but rejects that first send, the chat preserves the prompt and error across reloads; Retry sends it through the already-created session instead of creating another one.
Inside Settings, the dedicated sidebar starts with a Search settings field for quickly finding settings sections.
A Search field at the top of the sidebar opens the command palette (⌘K). Clicking the agent identity row at the top of the sidebar opens the agent menu; Home opens the main session. When something needs action — failed or overdue cron jobs, expiring or expired model auth — compact attention chips appear above the sidebar footer and click through to the owning page. The identity row shows the agent's avatar (identity image or emoji), name, connection dot, and a live subtitle. Clicking it opens the agent menu: an agent switcher (multi-agent setups), "What can this agent do?", Agent settings, Settings, mobile pairing, Docs, the build chip, and the color-mode toggle. Rosters above ten agents get a filter field and list pinned agents first; pin or unpin agents from the Agents settings page, with the pinned set stored in the browser profile. Choosing an agent scopes Chat plus Usage, Automations, Tasks, Workboard, and Sessions to that agent. Each scoped page exposes an Agent control with All agents as an escape; this widens the shared page scope without changing the concrete chat agent, while direct session links still open their target. The Agents settings page keeps its own ?agent= selection and does not follow the shared page scope. The footer bar holds the product logo, the build chip, a gateway connection dot, and a Settings shortcut. When the gateway runs from a source checkout on a branch other than main, the footer also shows that branch name in red so a non-release gateway is obvious at a glance (release installs never show it). Shift-Command-Comma opens Settings without overriding the browser's Command-Comma shortcut. The sidebar header also holds the collapse toggle (⌘B); collapsing hides the sidebar entirely for a full-width workspace, and a floating expand control (or ⌘B) brings it back; the macOS app hosts that toggle natively in the titlebar instead. The sidebar is the only navigation chrome on desktop, with no top bar. Narrow viewports swap the sidebar for a slide-over drawer behind a compact header row holding the drawer toggle, brand, and command-palette search; on phones, Chat absorbs that navigation row into its title bar, with the menu and search controls beside the session title. In the macOS app the separate header row folds the titlebar clearance into a single compact strip beside the window controls. Navigation uses regular browser history, so the browser's back/forward buttons traverse it; the macOS app adds a native sidebar toggle next to the window controls plus trackpad swipe gestures, with back/forward buttons at the sidebar's right edge while it is expanded and native search (command palette) and new-session buttons while it is collapsed.
What it can do (today)
Import assistant memory
Open Settings → Import Memory to bring local Codex or Claude Code memory into an OpenClaw agent. The Gateway discovers supported local memory on its own host, so a remote Control UI imports from the Gateway computer rather than the browser computer.
- Choose the destination agent.
- Review the detected source collections and Markdown filenames. File contents are not sent in the plan response or displayed in the page.
- Select the collections to import and confirm. Apply rebuilds the plan before writing so stale selections fail safely.
- If files already exist, enable Replace existing imports, refresh the preview, and confirm the replacement.
Codex imports only its consolidated MEMORY.md and memory_summary.md. Claude
Code imports Markdown from project auto-memory directories and a configured
autoMemoryDirectory; it does not import sessions, settings, instructions, or
credentials through this page. Files are copied below memory/imports/ in the
selected workspace, where the active memory plugin can index them. Sources are
never changed.
Planning and applying require operator.admin. Every apply creates a verified
OpenClaw backup when state exists, writes a redacted migration report, and keeps
item-level backups before replacing existing destination files. See
Memory overview for paths and
recall behavior.
MCP page
The dedicated MCP page is an operator view for OpenClaw-managed MCP servers under mcp.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.
- Add, enable, disable, or remove servers directly on the MCP page. Use the Plugins page for one-click connectors and 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.
The page redacts credential-bearing URL-like values before rendering and quotes server names in command snippets so copied commands still work with spaces or shell metacharacters. Full CLI and config reference: MCP.
Activity tab
The Activity tab lives in Settings › System, next to Logs and Debug. It is an ephemeral browser-local observer for live tool activity, derived from the same Gateway session.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, set gateway.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.
Owner-authorized, unsandboxed agents can use the terminal tool for long or interactive work that the operator should watch. Each tool call can open, read, write, resize, close, or list the agent's own gateway PTYs. New sessions open a co-attached Control UI tab by default, so the agent and operator share output and either can type or resize. Agent access is exact-session scoped: an agent cannot read or control operator-created terminals or terminals opened by another agent session.
Drag one or more files onto the active terminal, or use the paperclip button to choose files. OpenClaw stages each file on the machine that owns the PTY and pastes shell-quoted absolute paths at the cursor; it never presses Enter or executes the input. A compact batch indicator shows the current file and completed count. Cancel stops the remaining batch without pasting paths; a failed transfer stays visible so you can retry from that file without re-uploading completed files. Images, PDFs, archives, and other file types are accepted up to 16 MiB per file. Staged files use a private system-temporary directory on POSIX hosts (directory mode 0700, file mode 0600) or a directory under the user-profile ACL boundary on Windows, plus a 24-hour cleanup timer, so move or copy anything you need to keep.
Path insertion supports PowerShell, cmd.exe, and recognized POSIX shells (sh, Bash, Dash, Ash, Ksh, Zsh, and Fish), including Git Bash on Windows. Other shell overrides are refused because their quoting rules cannot be inferred safely; run the Gateway inside WSL for a native WSL terminal and Linux upload paths. cmd.exe paths containing % or ! are also refused because that shell expands those characters even inside double quotes.
Codex and Claude Code sessions discovered in the sessions sidebar can open in their native CLI inside the same terminal panel. In Settings › Chat, set Open Codex/Claude threads in to Terminal to make a normal row click open codex resume or claude --resume; the default remains the read-only OpenClaw viewer. A row's right-click or kebab menu always offers both choices, and the viewer header includes Open in terminal when that session is eligible.
Eligibility is per session and per host. Gateway-local sessions start the provider-owned resume command on the Gateway host. Paired-node sessions start an allowlisted provider command on the owning node and relay only that PTY's output, input, and resize events; this does not expose a general node shell or accept browser-supplied commands. File uploads use the separate, size-bounded terminal.upload node command and remain bound to the already-open terminal session. Approve the node pairing upgrade when that command first appears. Nodes that do not advertise the matching terminal-resume command, including embedded worker bridges without duplex streaming, keep the viewer available and show terminal opening as unavailable; older nodes can still run a terminal but cannot receive dragged files.
Connection-owned 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 connection-owned sessions are killed after gateway.terminal.detachedSessionTimeoutSeconds (default 300 seconds; 0 restores kill-on-disconnect). Attaching one of these sessions remains tmux-style take-over.
Agent-owned sessions are not bound to a browser connection. terminal.attach adds each browser as a viewer without taking ownership, and closing a viewer tab detaches only that browser. The PTY remains until the owning agent closes it, its process exits, policy disables it, or the Gateway shuts down. terminal.list marks each entry as connection- or agent-owned, and terminal.text lets an admin connection read recent plain-text output without attaching.
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.
Browser panel
The Control UI ships a dockable browser panel that renders the Gateway-controlled browser (the same one agents drive through the browser tool) in any regular web browser - no native webview required. It appears when the connected Gateway advertises browser.request to an operator.admin connection; the globe button in the thread workspace rail toggles it. The panel shows a live page snapshot with tabs, an editable URL bar, back/forward/reload, and open-in-your-browser, docks right or bottom, and forwards clicks, wheel scrolling, and basic typing to the remote page.
Two capture modes package page context for the agent:
- Annotate (pencil): draw freehand markup over the page. Send to chat composites the strokes into the screenshot, attaches the image to the active chat composer, and prefills a prompt describing the page URL, title, and each marked region so the agent knows exactly what you circled.
- Inspect (pointer): hover to see the element under the cursor (selector, accessible name, role, size); click to send that element's details plus a highlighted screenshot through the same composer flow. Inspect, wheel scrolling, and back/forward need
browser.evaluateEnabled(on by default).
The macOS app keeps its native link-browser sidebar for links clicked in the dashboard; the browser panel works there too, and is the way to annotate pages on every other platform.
Chat behavior
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. While Talk is live, the microphone button becomes a pill showing the live input-level meter; clicking it stops voice input, and hovering it reveals the stop glyph. Screen readers announce `Connecting voice input...`, `Listening...`, or `Asking OpenClaw...` while a realtime tool call is consulting the configured larger model through `talk.client.toolCall`. Stopping a running agent response stays a separate square **Stop** control next to the pill.
**Video Talk** is available for OpenAI Realtime WebRTC and Google Live browser sessions. Click the camera button, allow camera and microphone access, and confirm the local preview. OpenAI sends one bounded JPEG frame over its browser data channel when `describe_view` requests visual context. Google Live sends bounded JPEG frames directly from the browser to the provider at the supported maximum of one frame per second and answers `describe_view` function calls with the camera-stream state. Camera frames never pass through the Gateway. Stopping Talk closes the preview and releases both media tracks. See Google's [Live API capabilities](https://ai.google.dev/gemini-api/docs/live-api/capabilities#video) and [function-calling guide](https://ai.google.dev/gemini-api/docs/live-api/tools) for the provider wire contracts.
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 setup with a JPEG frame and `describe_view` function roundtrip, and the Gateway relay browser adapter with fake microphone media. The command prints provider status only and does not log secrets.
Connection loss and reconnect
Once a session is established, a dropped Gateway connection does not log you out. The dashboard stays visible with a floating amber "Gateway connection lost — Reconnecting…" pill under the top bar while the client retries automatically with backoff (800 ms up to 15 s). Live updates and realtime/session actions pause until the connection returns; Retry now in the pill forces an immediate attempt. Chat remains editable: ordinary text and attachment sends are kept in the current tab's gateway/session-scoped browser storage, shown as waiting for reconnect, and sent automatically when the Gateway returns. Live controls and slash commands remain unavailable while offline.
When this browser already holds credentials (a configured token/password or an approved device token), first opens and reloads show a small animated OpenClaw mark while the connection is established instead of flashing the login gate. The login gate only appears when no credentials are stored yet or when the Gateway actively rejects them (bad token/password, revoked pairing) — states that need your input rather than waiting.
PWA install and web push
The Control UI ships a manifest.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.
Inside the macOS app, the Notifications settings page shows the app's native notification permission instead of browser push because the app delivers notifications natively.
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. |
state/openclaw.sqlite → web_push_vapid_keys |
Auto-generated VAPID keypair used to sign Web Push payloads. |
state/openclaw.sqlite → web_push_subscriptions |
Persisted browser subscription endpoints, keys, and registration timestamps. |
Upgrades from the retired push/vapid-keys.json and push/web-push-subscriptions.json stores are imported by openclaw doctor --fix. Stop the Gateway before running that repair so an older process cannot recreate retired state during import. Run the repair before using Web Push after an upgrade; registration, delivery, deletion, and key resolution refuse to proceed while either retired source or an interrupted Doctor claim remains. The Gateway runtime reads and writes SQLite only.
Override the VAPID keypair through env vars on the Gateway process when you want to pin keys (multi-host deployments, secrets rotation, or tests):
OPENCLAW_VAPID_PUBLIC_KEYOPENCLAW_VAPID_PRIVATE_KEYOPENCLAW_VAPID_SUBJECT(defaults tohttps://openclaw.ai)
The Control UI uses these scope-gated Gateway methods to register and test browser subscriptions:
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.
Hosted embeds
Assistant messages can render hosted web content inline with the [embed ...] shortcode. The iframe sandbox policy is controlled by gateway.controlUi.embedSandbox:
The core show_widget tool renders self-contained SVG or HTML directly from a tool call. The browser and supported native chat clients advertise the inline-widgets Gateway capability, and the resulting Canvas document remains available when chat history reloads. Discord Activities provide the same tool name on Discord; other channel-originated runs do not receive it.
{
gateway: {
controlUi: {
embedSandbox: "scripts",
},
},
}
Absolute external 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
The chat transcript uses a centered readable frame aligned with the composer. Assistant and tool output stay left-aligned while user bubbles stay right-aligned inside that frame. Wide-monitor deployments can override the transcript width without patching bundled CSS by setting gateway.controlUi.chatMessageMaxWidth:
{
gateway: {
controlUi: {
chatMessageMaxWidth: "min(1280px, 82%)",
},
},
}
The value is validated before it reaches the browser. Supported forms include plain lengths and percentages such as 960px or 82%, plus constrained min(...), max(...), clamp(...), calc(...), and fit-content(...) width expressions.
Tailnet access (recommended)
```bash
openclaw gateway --tailscale serve
```
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.
<Warning>
Tokenless Serve auth assumes the gateway host is trusted. If untrusted local code may run on that host, require token/password auth.
</Warning>
Open `http://<tailscale-ip>:18789/` (or your configured `gateway.controlUi.basePath`).
Paste the matching shared secret into the UI settings (sent as `connect.params.auth.token` or `connect.params.auth.password`).
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
Recommended fix: use HTTPS (Tailscale Serve) or open the UI locally at https://<magicdns>/ (Serve) or http://127.0.0.1:18789/ (on the gateway host).
`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.
<Warning>
`dangerouslyDisableDeviceAuth` disables Control UI device identity checks and is a severe security downgrade. Revert quickly after emergency use.
</Warning>
See Tailscale for HTTPS setup guidance.
Content security policy
The Control UI ships a tight img-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.
This is always on and not configurable.
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.
If you disable gateway auth (not recommended on shared hosts), the avatar route also becomes unauthenticated, in line with the rest of the gateway.
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.
This keeps media rendering compatible with browser-native media elements without putting reusable gateway credentials in visible media URLs.
Approval links
Operator approval notifications can deep-link to a standalone approval document served under the reserved ${controlUiBasePath}/approve/{approvalId} namespace (for example /approve/<approvalId>, or /openclaw/approve/<approvalId> with a configured base path). The URL is stable for the lifetime of the approval and safe to forward between your own devices: it identifies the approval, never authorizes it.
- The one-segment
/approve/<approvalId>namespace is reserved by the Gateway ahead of plugin HTTP routes for all HTTP methods, so a plugin route can never shadow or intercept an approval document. - Opening an approval document requires the same gateway auth as the rest of the Control UI (token/password, Tailscale Serve identity, or trusted-proxy identity); credentials are never part of the approval URL.
- When Control UI serving is disabled, requests to the namespace return
404instead of falling through to plugin handlers. - Signing in on an approval document is ephemeral for that page: it does not overwrite the gateway selection or settings saved by the full Control UI in the same browser.
The Gateway serves static files from dist/control-ui:
pnpm ui:build
Optional absolute base (fixed asset URLs):
OPENCLAW_CONTROL_UI_BASE_PATH=/openclaw/ pnpm ui:build
Local development (separate dev server):
pnpm ui:dev
Then point the UI at your Gateway WS URL (e.g. 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.
Optional one-time auth (if needed):
```text
http://localhost:5173/?gatewayUrl=wss%3A%2F%2F<gateway-host>%3A18789#token=<gateway-token>
```
{
gateway: {
controlUi: {
allowedOrigins: ["http://localhost:5173"],
},
},
}
Remote access setup details: Remote access.
Related
- Dashboard — gateway dashboard
- Health Checks — gateway health monitoring
- TUI — terminal user interface
- WebChat — browser-based chat interface
Source: docs/web/control-ui.md