Browser tool
The browser tool (Wave 6 W6.3, "Reach") gives Neo control of a headless Chromium browser via Playwright: navigate to a URL, click, fill in forms, read page text, and take screenshots. It's the extension point for tasks the read-only web-fetch/web-search tools can't do — anything that needs a real DOM, JS execution, or interaction.
Installing Playwright
Playwright is an optional peer dependency — Neo does not install it for you. Install it yourself before using the tool:
npm i -D playwright && npx playwright install chromiumIf the package isn't installed, the tool returns a clear tool-error result (not a crash) with these exact install instructions. See src/tools/browser/playwright-bridge.ts.
Input
{
"action": "navigate" | "click" | "fill" | "read" | "screenshot" | "close",
"url": "https://example.com", // required for "navigate"
"selector": "#submit", // required for "click" and "fill"
"value": "hello", // required for "fill"
"path": "/tmp/shot.png" // optional for "screenshot"
}| Action | Effect | Returns |
|---|---|---|
navigate | Go to url. | Page title + resolved URL. |
click | Click the element matching selector. | Confirmation. |
fill | Fill the element matching selector with value. | Confirmation. |
read | Return the current page's text content. | Text, capped at ~20,000 characters. |
screenshot | Save a PNG of the current page. | The path the screenshot was written to (auto-generated under ~/.neo/browser/screenshots/ if path is omitted). |
close | Close the browser session. Idempotent — safe to call when nothing is open. | Confirmation. |
All actions use a 30-second default timeout and are failure-soft: a bad selector, a navigation timeout, or any other Playwright error comes back as a tool-error result (isError: true), never an uncaught exception.
Session model
A single persistent Chromium context is launched under ~/.neo/browser/profile (cookies, localStorage, and login state survive across calls, like a real browser profile) and reused across every browser call within a process — one context, not one per call. It is closed:
- explicitly, via the
closeaction, or - automatically, on graceful process shutdown (registered with the shared cleanup registry,
src/runtime/cleanup-registry.ts— the same mechanism the MCP server registry uses).
Permissions and containment
browser is not listed in any channel tool-policy profile (src/channels/_profiles.ts) — it falls through to that profile's defaultBehavior (ask for the built-in profiles), so every call over an untrusted channel (Telegram, WhatsApp) surfaces for owner approval. It is never added to DAEMON_SAFE_CORE_TOOL_NAMES (src/runtime/daemon/agent-deps.ts), so the always-on daemon can never declare or invoke it, regardless of capability tier.
isDestructive() returns true for click/fill (they mutate page state) and false for navigate/read/screenshot/close. Note that the permission engine's dynamic isDestructive gate is bash-only today (src/permissions/engine.ts:299-303), so this flag does not yet add an extra approval prompt beyond the static buildTool defaults + the implicit ask channel containment above. Generalizing that gate to non-bash tools is a documented follow-up, not solved here.
Testing
Unit tests (tests/tools/browser.test.ts) inject a fake Playwright module — no real browser or network access is used. A separate describe block runs real-browser end-to-end checks, gated behind NEO_E2E_BROWSER=on (it.skipIf) and skipped by default/in CI.