Portals: Expose Development Servers Through the Gateway
Learn how Portals let operators view agent-run development servers in their own browser. This page covers quick start, proxy behavior, and declaring servers with portals.json.
Read this when
- Showing a development server in the Control UI
- Declaring workspace development servers for an agent
- Troubleshooting portal access or live reload
Portals let an operator view, from their own browser, a development server that is running on the Gateway host. Both HTTP and WebSocket traffic are proxied through them, which supports live reload, and they show up under Control UI → Portals.
Quick start
You can request a portal from the agent:
- "Show me in a portal."
- "Start the app in a portal."
When the agent opens a portal, it targets the application's port and launches the development server using a background exec call. Creating the portal merely sets up the proxy listener; no environment variables are injected into your server. The agent places PORT (the port it opened) and PUBLIC_URL (the portal's public base URL) into the environment of that exec command, allowing the app to bind the right port and build correct absolute URLs.
Declare development servers
If you want the agent to find the available development servers, you can commit .openclaw/portals.json to the workspace repository:
{
"portals": [
{
"name": "web",
"command": "pnpm dev",
"cwd": ".",
"port": 3000,
"title": "App",
"description": "Use the seeded test account."
}
]
}
The Gateway never runs these commands on its own. The agent reads the file and picks when to launch a declared server.
| Field | Required | Description |
|---|---|---|
name | yes | Stable name the agent uses to identify the server. |
command | yes | Command the agent starts with background exec. |
port | yes | Local TCP port the application listens on. |
cwd | no | Working directory relative to the workspace root. |
title | no | Display title shown on the Portals page. |
description | no | Operator guidance shown beside the portal. |
path | no | Initial URL path. It must begin with /. |
Application contract
The application has to respect PORT. When absolute URLs are needed, PUBLIC_URL should be used.
The proxy translates Host into the local target, so common development servers like Vite and Next.js work without extra setup. WebSockets and hot module replacement go through the same portal.
Availability and configuration
No dedicated configuration key exists for portals. The portal tool follows the standard tool policy, which is covered in Tools configuration.
By default:
portalis part ofgroup:uiand thecodingprofile, meaning coding agents have access whilemessagingandminimalagents do not.- Sandboxed sessions never get it, since opening a portal starts a listener on the Gateway host.
- It is disallowed for HTTP
POST /tools/invokeand limited to the session owner, matching howterminalis handled.
To disable portals everywhere, deny the tool in the global policy:
{
tools: { deny: ["portal"] },
}
To disable them for a single agent while leaving the rest untouched:
{
agents: { entries: { "<agentId>": { tools: { deny: ["portal"] } } } },
}
The same rules that govern any other tool, namely tools.profile, tools.allow, byProvider, and toolsBySender, also apply to portal, so portals can be scoped to particular providers, models, or senders without a portal-specific option.
One thing to plan for: portal listeners bind to the same interfaces as the Gateway. If the Gateway is bound to a LAN or tailnet address, the portal listener ports are exposed on that network as well. The portal token is still required to reach one, but if the Gateway host must not expose operator-reachable application ports at all, deny the tool.
Security model
Every portal gets its own origin on a dedicated port and binds to the same interfaces as the Gateway. Access depends on the token in the portal URL. On the first request, the proxy stores that token in an HttpOnly cookie and strips it from subsequent upstream requests. The proxy checks this cookie itself and never sends it to the application.
Because browser cookies are scoped by hostname rather than port, the proxy separates each application's cookie jar using an oc_portal_<targetPort>_ name prefix. Only cookies carrying that portal's prefix are forwarded, and the prefix is removed before they reach the application; Gateway cookies, unprefixed cookies, and cookies from other portals are discarded. Application Set-Cookie responses get the prefix added, and any Domain attribute is removed so the cookie remains host-only.
Portals proxy only the selected local development server. They never serve Gateway data, and every portal terminates when the Gateway restarts.
Limitations
- The development server must run on the Gateway host. Support for remote workers is on the roadmap.
- A proxy or tunnel placed in front of the Gateway does not automatically expose portal listener ports. The Control UI detects this situation and displays a reachable URL with retry instructions instead of loading a dead iframe.
- Browser-side cookie code sees the prefixed names in
document.cookie. Applications that manage cookies in browser code must handle the prefix; unprefixed cookies written directly by browser code are not forwarded to the target.
Troubleshooting
The portal shows a 502 waiting page
The proxy is up, but the application is not listening on the chosen port. The page retries on its own. Verify the background process and ensure the server honors PORT.
The portal is not reachable from this browser
The Control UI reached the Gateway but could not reach the portal's separate listener port. This usually happens when a proxy or tunnel only exposes the main Gateway port. Open the displayed portal URL from a browser on the Gateway host, or expose that portal listener port through the same network path, then choose Retry.
Close a portal
Tell the agent to "close the portal," or click the close button on the Control UI → Portals page.