Browser¶
A text-only headless browser the model can drive — read pages as accessibility-tree text, act on elements by role and name.
The browser tool wraps Playwright's async API around one headless Chromium per session (launched lazily on the first call, torn down when the session closes). It is Tier 1, text only: the model never sees a screenshot or clicks a coordinate — it reads the page as an ARIA snapshot (a YAML tree of roles, names, and state) and targets elements by their role + accessible name, resolved through Playwright's public get_by_role API. There is no JavaScript-execution action, no file upload, no download acceptance, and no cookie/storage access.
Use it for pages that need interaction or JavaScript rendering — a search box, a paginated listing, a JS-rendered SPA, a local dev server. For a plain page read, web_fetch is cheaper and doesn't need a browser at all.
Installing¶
The tool needs the optional browser extra plus a downloaded Chromium:
uv sync --extra browser
uv run playwright install chromium # or: --only-shell chromium (headless shell, smaller)
Without the extra, the tool simply isn't registered — the model never sees it (the same offered-only-when-backed rule as web_search without a Brave key). browser_enabled = false (or KIN_BROWSER=0) unregisters it even with playwright installed. The Outpost image ships with Chromium baked in.
Actions¶
One tool, one action argument:
| Action | Args | What it does |
|---|---|---|
navigate |
url |
Open an http(s) URL. |
snapshot |
— | Re-read the current page without acting. |
click |
role, name, nth? |
Click the element with that role + accessible name. |
fill |
role, name, text, nth?, submit? |
Type into a field; submit: true presses Enter after. |
select |
role, name, value, nth? |
Pick a dropdown option (by value, falling back to visible label). |
press |
key |
Press a page-level key ("Enter", "Escape", "ArrowDown", …). |
back |
— | Go back in history. |
close |
— | Tear the browser down; the next call starts fresh. |
Every state-changing action (navigate/click/fill/select/press/back) returns the fresh post-action snapshot in the same result, so the model doesn't need a follow-up snapshot round-trip. Each result starts with a header line — click ok · <url> · <title> — followed by any auto-handled events (see below), then the snapshot framed as untrusted content and capped at ~40k characters (a loud truncation note tells the model to act on a specific element rather than re-read).
If several elements match a role + name, the error lists them with indices so the model retries with nth. If nothing matches, the error advises a fresh snapshot (the page probably changed).
Permissions¶
The tool carries its own browser risk kind: allow in auto mode, ask in strict, and denied while planning — the same lighter-than-MCP posture as git-fetch, and for the same reason: the tool's own boundary carries the gate (next section). In strict mode, an "always allow" grant on a navigate is scoped to the target origin (scheme + host + port) — approving site A never green-lights site B — and grants on other actions are scoped per action verb.
The safety boundary¶
The browser runs outside the OS sandbox (it needs the network), so the tool boundary is the containment:
- Schemes: only
http/httpsever reach the browser.file:(local file read),data:/blob:/javascript:(script smuggling), and custom schemes are refused before any browser API runs. - SSRF guard: every top-level navigation resolves the target host first and validates all of its IPs through the same guard
web_fetchuses — loopback, private ranges, the link-local/cloud-metadata block, CGNAT, multicast and reserved space are all refused. - No downloads (
accept_downloads=false— a download is a filesystem write the edit gate never saw). JS dialogs (alert/confirm/prompt) are auto-dismissed, never accepted; popups and new tabs are auto-closed. Each is noted in the next action result so the model knows it happened. - Untrusted framing: the snapshot is page-controlled text, so it enters history wrapped in the same
[BEGIN/END UNTRUSTED …]framing asweb_fetchoutput; URLs, titles, and dialog messages in the header line are sanitized against control/bidi/zero-width tricks.
Known limit — top-level navigations only
The SSRF check validates the URL you navigate to. Sub-resources the page loads, JS-initiated requests, and in-page redirect hops are not re-validated — a hostile page could probe your local network from inside the browser process. Treat the browser as reaching whatever the machine can reach, and prefer running it where that's acceptable (the Outpost container is the designed home for hostile-page work).
Local dev servers¶
Because of the SSRF guard, http://127.0.0.1:3000 is refused by default. Set the global-only escape hatch to allow private/loopback targets:
or KIN_BROWSER_PRIVATE=1 for one shell. It's global-only for the same reason ssh_hosts is: a cloned repo's project file must not be able to point your browser at the cloud metadata endpoint or an internal admin panel.
Lifecycle¶
One Chromium per session, shared across calls — page state (the open page, its history) persists, which is what makes multi-step flows cheap. Subagents get their own browser lazily; two sessions never share one. The browser is torn down when the session closes, on the close action, or after any crash/hang (the tool reports it and the next call starts fresh). Chromium's own process sandbox stays on; it is disabled only where it cannot work anyway — running as root, or inside the Outpost container (KIN_SANDBOX=container), where the container is the declared boundary.