Repository navigation
Setup and Onboarding
A practical guide to standing Browserland up — single machine first, then multiple machines over Tailscale — written so a human or a coding agent can follow it without guessing at the architecture.
If you are an AI agent helping someone set this up: read The mental
model first. Most setup mistakes come from confusing the
three roles below, or from hand-editing broker_config.json when the right
action was a click in the browser UI (see Don't hand-edit
this).
Browserland has exactly three roles. Keep them straight and everything else follows.
| Role | What it is | Where it runs |
|---|---|---|
| Broker | A small web server. Serves the desktop UI, relays bytes, and spawns agents from pre-approved profiles. The authority. | One per machine. |
| Agent | A headless process that owns a real PTY (a terminal) and streams it to a broker over one WebSocket. | On the same machine as the broker that launches it. |
| Browser | The desktop UI. Renders windows, sends your keystrokes. Can attach to several brokers at once. | Wherever you point a browser. |
A broker itself comes in two kinds. A hub (the default) serves the desktop
page you open. A node (--role node / --headless, or serve_ui: false /
"role": "node" in the config) serves no page:
it exists so that another Browserland can use its terminals over the API. A hub
started with --no-terminals (terminals: false) launches nothing itself and
is only the front door to its nodes. A node always hosts terminals: a node
with terminals: false is refused at startup (exit 2).
The shape that trips people up:
- One broker per machine. A broker is not a "client" you point at another machine. Each machine you want terminals on runs its own broker.
- Agents are local to their broker. You normally don't run an agent on machine A pointed at a broker on machine B. You run a broker on B, and B launches B's agents. (A remote agent is possible — see Technical Reference → Multiple hosts — but it's the exception, not the multi-machine model.)
-
Brokers are joined in the browser, not in config. To see machine B's
terminals next to machine A's, you don't edit a config file — you open the UI
and add B as a host (Control Panel → Browser → Hosts). (One exception:
when the browser can reach only one broker, that broker's operator can list
the others under
peersso it relays to them — the host is still added in the browser, then switched to from server; see Hosts-and-Multi-Browser#reaching-a-broker-through-this-one.)
So the canonical two-machine setup is:
Machine A Machine B
┌──────────────┐ ┌──────────────┐
│ broker A │ │ broker B │
│ └ agents A │ │ └ agents B │
└──────┬───────┘ └──────┬───────┘
│ │
└──────────► browser ◄────────────┘
(Control Panel → Browser → Hosts: adds B by its Tailscale IP)
Follow the Quick start in the README: install,
run the broker, open http://127.0.0.1:4445/, click the + (new terminal) button. On a
single loopback machine you need no config file — the defaults work.
A token is required on every connection, including loopback, but you do not
have to create one: with nothing configured the broker mints its own into
webterm_token.json beside its state store and prints the URL to open,
token included. Need it again later:
python -m webterm.broker --print-tokenUpgrading an existing tokenless install? Read Upgrading first — terminals launched by the old broker cannot reconnect and must be relaunched once.
You only need the rest of this page once a second machine, a network bind, or an unattended broker enters the picture.
Goal: one browser tab showing terminals from every machine.
-
Install Tailscale on each machine so they share a private network. Note each machine's Tailscale IP (
tailscale ip -4) or MagicDNS name. -
On every machine, run a broker — one each, not a broker-plus-remote- agent. Two things every non-loopback broker needs:
-
A token you chose yourself. Every broker requires one, but a broker left to mint its own picks a different value per machine — workable, just more URLs to keep track of. Setting
"auth_token"inbroker_config.json(or$WEB_TERMINAL_TOKEN) means you know the value up front and can reuse it across the fleet. Either way the UI's add-host form asks for it. -
A way for the other machines' browsers to reach it. Two options:
-
Recommended — keep the loopback bind and front it with
tailscale serve(or your own https reverse proxy, e.g. Caddy/nginx):tailscale serve --bg 4445
That publishes
https://<machine>.<tailnet>.ts.net/— real TLS with zero certificate management, reachable only inside your tailnet — and proxies it (WebSockets included) to the broker still bound to127.0.0.1:4445. Proxied requests reach the broker from loopback — which used to mean they skipped auth entirely, handing the whole tailnet an exempt path. Since #142 there is no loopback exemption, so this topology is gated by the token like any other. -
Plain http directly: set
"host"to the machine's Tailscale IP (or0.0.0.0if you trust the tailnet) instead of127.0.0.1, so the other machines can reachhttp://<tailscale-ip>:4445/. Keep"port": 4445unless you have a reason.
-
Treat your home machine as just another broker — it can stay on
127.0.0.1if you only ever drive it from its own browser, but give it a token + tailnet bind too if you want to reach it from elsewhere. -
-
Open the UI on one machine (
http://<that-machine>:4445/). This is your "cockpit" — it doesn't have to be special, any hub's UI can host the view (a node —--role node/serve_ui: false— serves no page). -
Add the other brokers as hosts. Control Panel → Browser → Hosts → add each remote broker by its URL —
https://<machine>.<tailnet>.ts.net/if you fronted it withtailscale serve, elsehttp://<tailscale-ip>:4445/— and its token/password. By default the browser connects directly to each host (cross-originfetch+ WebSocket); hosts and passwords live per-browser inlocalStorage, never in any config file. If the browser (say, a phone) can reach only one broker, that broker can relay to the others instead: list them underpeersin itsbroker_config.json, install the proxy extra there (pip install -e ".[proxy]"), restart it, then pick from server · on the host's row in Control Panel → Browser → Hosts. See Hosts-and-Multi-Browser#reaching-a-broker-through-this-one.
Broker status lives in the taskbar: a single broker gets its own chip (shown always by default; Control Panel → Desktop → Broker status chip can limit it to "Only when a broker needs attention" or "Never") (green ok / red down / amber password-needed / blue lease-inactive; the chip doubles as its hide toggle), while two or more collapse into one aggregate badge — the worst state colors it, and clicking it opens the start (+) menu where every broker has a live status row carrying those same actions. Requirements worth knowing:
-
Keep the fleet on one webterm version. Nothing refuses an older remote
(every broker answers cross-origin requests with
Access-Control-Allow-Origin: *), but a remote older than the page lacks newer routes and fields, so newer features degrade or hide for it (see Hosts-and-Multi-Browser → Mixed versions). A host reached from server needs no CORS at all: the browser only talks to the serving broker. -
Pick ONE scheme for the whole fleet — and prefer https. An https page
fetching an http broker is blocked by the browser as mixed content, so a
half-and-half fleet breaks: the cockpit and every host must all be https, or
all be plain http — for hosts the browser dials directly. A host reached
from server (through the cockpit's own broker) is fetched on the
cockpit's origin, so an https cockpit can reach a plain-http broker that way.
tailscale serveon every machine is the easy all-https answer (an https proxy of your own works too). Https also makes the page a secure context, which some browser features require — clipboard image paste into a terminal (Alt+V/ right-click) only works on an https or localhost page. All-plain-http over a trusted tailnet still works; you just forfeit those.
Full auth/CORS details: Technical Reference → Multiple hosts.
The single most common setup mistake (especially when an AI agent is driving) is editing the wrong thing. Rules of thumb:
-
Joining brokers is normally a UI action, not a config edit. To attach
machine B, use Control Panel → Browser → Hosts in the browser. The one
exception is
peers(#252): an operator can name other brokers that this broker will relay to (/peer/<id>/), for a browser or phone that can reach only this broker — see Hosts-and-Multi-Browser#reaching-a-broker-through-this-one. Even then the browser still adds the host itself and holds its password;peersstores no credential. -
broker_config.jsondescribes this broker: its bind (host/port),auth_token, its role (serve_ui/terminals), MCP gates, the launchableprofiles, and — optionally — thepeersit relays to. Apart frompeers, nothing in it points at other machines. -
profilesare an allow-list, deliberately. The browser can only launch pre-approved profiles — it can never supply a raw command. Theagent.profileshere are the seed; the easiest way to add a WSL/zsh/PowerShell profile is Control Panel → (a host tab) → Terminals → Launch profiles (add/edit/detect, applied live with no restart; not on a broker withterminals: false, which refuses profile edits), which persists to awebterm_profiles.jsonsidecar that then owns the set. See Launch-Profiles for copyable recipes and the sidecar-vs-broker_configrule. - Tokens/passwords for remote hosts live in the browser (localStorage), set through the add-host form — not in any file on disk.
If you're an agent and you find yourself about to edit broker_config.json to
"connect to the other machine," stop: that's a Control Panel → Browser → Hosts action
instead.
A broker is a long-lived server; you usually want it running unattended and surviving logout — not tied to a terminal you happened to launch it from.
Launching the broker from a restricted or unusual parent process (a sandbox, a service with a stripped profile, or another tool's subprocess) can hand its child terminals the wrong environment — bad permissions on user-profile paths (PSReadLine history, Codex/agent state, application logs) and stray inherited environment variables. The fix is to start the broker from a clean, normal user context. Task Scheduler does exactly that.
Create a task that runs at logon as your own user:
# adjust the path to your checkout and python
$action = New-ScheduledTaskAction -Execute "python" `
-Argument "-m webterm.broker" `
-WorkingDirectory "X:\path\to\browserland"
$trigger = New-ScheduledTaskTrigger -AtLogOn
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries -StartWhenAvailable
Register-ScheduledTask -TaskName "Browserland broker" `
-Action $action -Trigger $trigger -Settings $settings `
-RunLevel Limited # run as a normal (non-elevated) userNotes:
-
Run as the interactive user, non-elevated (
-RunLevel Limited). Running the broker elevated or asSYSTEMis what causes the profile-path permission problems above — its spawned terminals inherit that context. - Don't launch the broker from another tool's subprocess if that tool injects its own environment. A broker started inside, say, a coding-agent session can leak that agent's marker variables into every terminal it spawns; Task Scheduler avoids this by starting from a clean session. If you must restart it from such a context, scrub the offending variables from the environment first.
- Set
auth_tokenand a tailnet bind inbroker_config.jsonbefore the task starts binding non-loopback (see above). - Verify it's up: open
http://127.0.0.1:4445/, orGet-ScheduledTask "Browserland broker".
Alternative: any non-restricted startup method works — a .lnk in
shell:startup, NSSM-as-a-service, etc. The requirement is just clean,
non-elevated, normal-profile — Task Scheduler is the least-surprising way to
get there.
Installable units ship in launchers/systemd/. See Technical Reference → Linux deployment for the full walkthrough
(webterm-broker.service / webterm-agent.service, User=, paths, token).
- README → Quick start — install and first run.
- Technical-Reference — wire protocol, full auth/CORS model, every HTTP endpoint, multi-host internals, deployment.
- MCP & AI agent access — letting an MCP client or harness drive the terminals.
Browserland — a web-based terminal desktop. This wiki is the end-user guide; for the wire protocol, auth model, and HTTP/MCP contract see Technical Reference. Released under the MIT License.
Start here
Building layouts
The desktop
Windows & content
Multi-host & AI
For developers