Skip to content

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:

bash
npm i -D playwright && npx playwright install chromium

If 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

jsonc
{
  "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"
}
ActionEffectReturns
navigateGo to url.Page title + resolved URL.
clickClick the element matching selector.Confirmation.
fillFill the element matching selector with value.Confirmation.
readReturn the current page's text content.Text, capped at ~20,000 characters.
screenshotSave a PNG of the current page.The path the screenshot was written to (auto-generated under ~/.neo/browser/screenshots/ if path is omitted).
closeClose 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 close action, 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.