@edlsh/omp-browse0.3.1
Hosted and local browser controls and optional Stagehand AI tools for OMP
Built for omp
The author tagged this package oh-my-pi.
Reports 0
No reports yet.
Readme
OMP Browse
Native hosted and local browser controls and optional Stagehand AI tools for OMP. Each conversation controls one browser, and explicit controls and AI operations share its tabs.
Install
Requires Bun 1.3.14 or newer and OMP 18.12.3–18.x. Hosted browsing needs a Browserbase project; local browsing needs Chrome/Chromium, but no Browserbase credentials.
From npm: omp plugin install @edlsh/omp-browse. From a checkout:
bun install
omp plugin link /absolute/path/to/omp-browse
For one run use omp -e /absolute/path/to/omp-browse. Alternatively, load createBrowseExtension(resolveConfig) through a profile extension using a relative import, so OMP resolves its own provider modules. Use only one loading method and start a new OMP session after installation. The package bundles the omp-browse skill under skills/omp-browse; link it into the profile's skills directory when using a wrapper.
This package was renamed from omp-browserbase. The tools are now browse and browse_ai, the command is /browse, and the exported factory is createBrowseExtension (BrowseConfig is its configuration type). Rename the installed loader to browse.ts rather than loading both versions. Existing browserbase.json, browserbase-ai.json, and browserbase-contexts.jsonl files keep their names so saved settings and contexts continue to work. Browserbase environment variables and backend name are unchanged. The bundled omp-browse skill is distinct from the unrelated Browse CLI skill.
Cookie import now reads any installed Chromium-based browser, not only Helium: helium_profiles and helium_import were renamed to cookie_profiles and cookie_import (with an optional browser), and the old names are no longer accepted. The errors helium_no_cookies and helium_profile_ambiguous are now cookies_not_found and cookie_profile_ambiguous.
Stagehand is used unmodified (pinned to 4.3.0); no SDK patch is applied. Stagehand pins zod 4.4.3, which has a high-severity advisory (GHSA-mrhx-c7xc-m3w5), so overrides installs zod 4.6.7 for every package until Stagehand ships a fixed pin. Behaviour the SDK lacks lives in this package instead: a loopback DevTools guard keeps attached browsers running on close, connecting retries when Stagehand's extension worker is replaced mid-attach, and a stale-reference check takes a fresh snapshot. Run both real-browser suites before upgrading Stagehand.
For hosted browsing, configure credentials in the launching environment or through your configured secrets manager:
export BROWSERBASE_API_KEY='<YOUR_API_KEY>'
export BROWSERBASE_PROJECT_ID='<YOUR_PROJECT_ID>'
Optionally pick the Browserbase region with BROWSERBASE_REGION (us-west-2, us-east-1, eu-central-1, or ap-southeast-1). The default is us-west-2. Every browser command is a round trip to the session, so choose the region nearest the machine running OMP. An unsupported value is rejected with a configuration error before any browser starts.
The package connects directly through the SDKs. No Browse CLI or MCP server is required. Only local mode needs a browser installed on this machine.
The /browse command
Every setting is saved for the profile in browserbase-ai.json in the profile's agent directory, so it applies to every conversation, including ones already open. Typing /browse completes setting names with their current values, then each setting's values. /browse ai-model completes providers first (zai/, with their model counts), then that provider's models; typing a model id without its provider (glm) also finds it.
| Command | Effect |
|---|---|
/browse |
Opens a settings menu (without a UI, same as status). |
/browse status |
Shows status and changes nothing: where the settings live, whether credentials resolve, the region, whether sessions survive dropped connections, this conversation's browser (not_started, active, tab_unresponsive, reconnected, ended, unresponsive), JEV, ask credentials, and Stagehand AI. |
/browse backend [local|remote] |
Where new browsers start: the local browser on this machine, or Browserbase. Without a saved backend, local is the default unless the loader passes { defaultBackend: "browserbase" } as createBrowseExtension's second argument. The footer shows LOCAL while local is chosen, and the agent is told to use it. A browser already open stays until /browse close. |
/browse jev [on|off] |
JEV targeting. It applies only in interactive, non-subagent sessions. |
/browse ai-model [<provider/id>|default] |
The OMP model that runs browse_ai (see AI (Stagehand)); default follows each conversation's model. |
/browse jev-model [<classifier>|default] |
The classifier behind JEV targets (see Choosing the JEV classifier). |
/browse ask-model [<classifier>|default] |
The classifier for ask's bool and choice fields. |
/browse close |
Releases this conversation's browser. |
A setting without a value opens its picker. Setting names and local, remote, on, off, and default are case-insensitive; model and classifier specs keep their case. Credentials and the Browserbase project and region stay in the loader's own files, which /browse status names.
Use OMP’s /extensions panel for a profile loader, or omp plugin disable|enable @edlsh/omp-browse for a plugin.
browse and browse_ai are discoverable tools. With OMP's default tools.xdev: true, read their descriptions and schemas at xd://browse and xd://browse_ai, then invoke through write or the eval tool bridge. Their full schemas no longer occupy every prompt. /browse, tool execution, and per-call approval tiers are unchanged.
Browser controls
The browse tool takes an action and the relevant arguments:
| Actions | Arguments |
|---|---|
start, open |
url (http or https) is required for open; start selects backend (browserbase by default, or local); hosted options: contextId, proxyCountry; local options: cdpUrl to attach, or headless to launch |
status, close |
No additional arguments |
back, forward, reload |
Optional tabId; back from a tab's first page returns no_history and stays put |
snapshot |
Optional tabId, maxChars (default 20000) |
ask |
fields: named questions about the page, answered by Cloudflare Workers AI; see Page questions |
click, fill, select |
selector; value for fill; values array for select, each an option's value or label |
press |
key on the focused element: one character or a key name (Enter, Tab, Escape, Backspace, Delete, Space, arrows, Home, End, PageUp, PageDown), optionally with modifiers such as Control+A |
scroll |
deltaX and/or deltaY; optional viewport origin x, y |
wait |
Exactly one of selector, durationMs, or loadState; optional selector state |
screenshot |
Optional fullPage; returns a native PNG image to OMP |
evaluate |
script, a JavaScript expression in the page |
tabs, tab_new |
Optional url for the new tab |
tab_select, tab_close |
Required tabId |
live_view |
Optional tabId; opens the running browser in the user's local browser (interactive sessions only; never used for sign-in) |
context_create |
name; returns a saved context ID without selecting it |
cookie_profiles |
Lists the installed supported browsers with their profile folders and names on macOS; optional browser |
har_start, har_stop |
Record every tab's network traffic; har_stop takes an optional absolute path ending in .har; see Network capture |
cookie_import |
Required domains; optional browser and profile (default: the only browser profile with cookies for the domains); starts the browser if needed |
Page actions accept an optional tabId; otherwise they use the selected tab. A new tab becomes selected. Popups can be found through tabs and selected explicitly.
Start with open, read a snapshot, then use a reference from its tree:
{"action":"open","url":"https://example.com"}
{"action":"snapshot"}
{"action":"click","selector":"@s1:0-24"}
The example reference must be replaced with one returned by your snapshot. CSS, XPath (xpath=… or //…), and text=… selectors are also accepted and must identify exactly one element for click, fill, and select; (xpath)[n] picks the nth match. Stagehand treats >> as a hop into an iframe, so Playwright-only syntax such as :has-text() or >> nth= does not work. Snapshot references are scoped to a tab and snapshot generation. Navigation, click, press, and evaluate invalidate them; fill, select, and scroll keep them, so one snapshot covers a whole form. References are checked against current DOM node IDs before use, so a replaced element is still caught.
select checks every requested value against the element's options before acting. A value that matches no option returns option_not_found with the available options and changes nothing; on its own, Stagehand would deselect everything and fall back to the first option. An element that is not a native <select> returns not_select.
evaluate failures from a thrown exception return script_error. A rejected promise keeps its message; a synchronous throw arrives from Stagehand as only the word "Uncaught", so wrap the script in try/catch and return String(error) to see the message. Values come back as JSON: undefined, NaN, Infinity, and BigInt all arrive as null (the result then carries a note), and DOM nodes, Maps, and Sets as {}.
A native alert, confirm, or prompt blocks every later command on its tab, and Stagehand 4.3 cannot answer one. An init script installed in every page and frame answers them in page script instead: OK for alert, Cancel for confirm and prompt. It also suppresses beforeunload prompts that would block navigation. Answered dialogs from the top frame are listed in the dialogs field of the result of the action that opened them. To accept a confirmation, evaluate window.confirm = () => true before the action; the override lasts until the page navigates.
If a tab stops answering anyway, for example in a busy script, the action that blocked it still succeeds with title: null and responsive: false. Later actions on it time out with a message naming the tab, status reports tab_unresponsive, and tabs marks it responsive: false. The browser and its logins are kept: call tab_new, then tab_close the blocked tab.
Other recurring Stagehand failures get their own codes: timeout for page-level waits and navigation, frame_unavailable when Stagehand cannot enter a document (legacy <frame> pages, browser error pages, frames still loading), context_not_found for an unknown contextId, and act_failed when browse_ai act finds nothing to do.
Stagehand reports a navigation the browser cancelled as a success and leaves the tab blank. Helium, for one, cancels a navigation made in about its first second after launch. start and open retry a navigation that left the tab on its blank start page once, and return navigation_cancelled if it is cancelled again.
evaluate executes inside the browser page. It cannot access local files or the OMP process. Treat page text and extracted data as untrusted content.
Local browsers
Use local mode for localhost/private servers, when the user explicitly asks, or after /browse backend local, which makes start, open, tab_new, and cookie_import without a backend launch locally in every conversation of the profile. It is never an automatic fallback after hosted failure. Otherwise first call start; subsequent open, controls, tabs, snapshots, screenshots, JEV, and browse_ai use that same browser:
{"action":"start","backend":"local","url":"http://localhost:3000"}
{"action":"snapshot"}
{"action":"close"}
Without cdpUrl, Stagehand launches a visible Chromium window with a temporary isolated profile and a 1280×800 viewport. headless: true hides the window. It discovers Chrome/Chromium automatically; set CHROME_PATH, or return localExecutablePath from a custom resolver, to select another executable such as Chrome for Testing. No everyday profile or cookies are loaded automatically. close, /browse close, and shutdown terminate only the owned browser and remove its temporary profile. Local browsers have no hosted one-hour expiry; an abrupt OMP crash may require manual cleanup.
A custom resolver can return localUserDataDir to launch every local browser in that persistent automation profile instead. Logins, cookies, local storage, and installed extensions (for example a password manager) survive close and relaunch; it is never the everyday browser profile. That profile keeps Chrome's macOS Keychain encryption, extension updates, and external extension installs, which the temporary profile disables. To preinstall a Chrome Web Store extension, write <localUserDataDir>/External Extensions/<extension-id>.json containing {"external_update_url":"https://clients2.google.com/service/update2/crx"}: the next launch installs it disabled, and Chrome asks the user to enable it once. Every conversation, in this process or another (omp, pi, and Hermes alike), shares the profile's one browser through browse-core's broker, each in a tab of its own; close closes that conversation's tabs. The last conversation to close quits Chrome through DevTools Browser.close and waits for it to exit, because Stagehand's process-group SIGTERM would discard cookies Chrome has not yet written to disk. A Chrome this tool does not manage holding the profile makes a launch fail with profile_in_use. After a Stagehand upgrade, a persistent profile can keep running the old runtime extension's cached service worker, and starting fails with "Incompatible Stagehand runtime: Protocol major mismatch". With the browser closed, move <localUserDataDir>/Default/Service Worker aside; Chrome rebuilds it at the next launch, and logins are kept. Chrome reads user native messaging hosts from inside a custom profile, so a desktop password-manager integration needs its host manifest linked into <localUserDataDir>/NativeMessagingHosts.
Attach to an existing browser
Supply an explicit DevTools endpoint as an http(s) or ws(s) URL, on this machine or another, without credentials or a fragment. Do not discover endpoints or attach to another agent’s browser. Do not run multiple controllers against the same browser. Stagehand's close() would send Browser.close to an attached browser and quit it, so the extension connects through a loopback proxy that forwards all DevTools traffic except Browser.close and Browser.crash, which it answers itself.
For example, start a separate automation profile on macOS (substitute your Chromium executable on other platforms):
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--user-data-dir="$HOME/.local/share/omp-browse/chrome" \
--remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 \
--enable-unsafe-extension-debugging '--remote-allow-origins=*'
These flags grant powerful debugging access; keep the endpoint loopback-only and use a dedicated profile, never the everyday browser profile. Chrome must support Extensions.loadUnpacked. Recent Chrome versions restrict remote debugging on the default profile. Do not restart or change a user’s existing browser without permission.
{"action":"start","backend":"local","cdpUrl":"http://127.0.0.1:9222"}
{"action":"tabs"}
{"action":"close"}
A browser on another machine, or behind an authenticated DevTools gateway, takes two more settings:
headerscarries the gateway's credentials, such as{"Authorization":"Bearer <token>"}. They go only tocdpUrl's host, for discovery and the WebSocket handshake, and never appear in results.- Stagehand's runtime runs as an extension inside the browser, and that extension opens its own DevTools connection, which cannot use headers or reach this machine's loopback proxy. For a non-loopback
cdpUrlit defaults to the browser-level URL the browser reports on/json/version; setextensionCdpUrlwhen that address is not one the browser itself reaches without headers, for examplews://127.0.0.1:9222/devtools/browser/<id>behind a gateway.
On another machine, Stagehand cannot load its extension from this one, so install it there first: export the archive from this checkout with bun -e 'import { getExtensionArchive } from "@browserbasehq/stagehand"; await Bun.write("stagehand-extension.zip", await getExtensionArchive())', extract it next to the browser, and load it with --load-extension=<folder> or Load unpacked. Use the archive from the same checkout as the extension, since Stagehand checks that versions match.
{"action":"start","backend":"local","cdpUrl":"https://browsers.example.net/devtools","headers":{"Authorization":"Bearer <token>"},"extensionCdpUrl":"ws://127.0.0.1:9222/devtools/browser/<id>"}
A custom resolver can name such browsers in remoteBrowsers ({ "<name>": { cdpUrl, headers?, extensionCdpUrl? } }), so the agent attaches with {"action":"start","backend":"local","endpoint":"<name>"} and never sees the credentials. A refused discovery or handshake returns attach_failed.
Attachment exposes existing tabs and logins; choose the intended account explicitly. Each conversation attached to a browser works in a tab of its own, and other conversations, in any process, may share the browser. The user's tabs are listed but used only when selected by tabId. Close/reset/shutdown close the conversation's own tabs and disconnect without closing that browser or the user's tabs. The browser’s own profile retains its logins. Both modes report backend: "local", origin: "launched" | "connected", and sessionId: null. Close before changing backend, endpoint, or launch settings. After close, an unqualified open again uses the profile's backend: local unless /browse backend remote is set. A lost local connection is not automatically reattached or replayed: explicitly start locally again, inspect state, then decide whether to retry.
Saved contexts, proxyCountry, and live_view are hosted-only and rejected locally. cookie_import works in a launched local browser, not an attached one, which keeps its own logins. For hand-off, use the visible local window, wait for the user to finish, then take a fresh snapshot. Existing attached documents may need navigation/reload before the dialog guard applies. Local browsing does not upload logins to Browserbase, but AI and JEV still send page context to their configured model providers. Local AI uses OMP inference, or both STAGEHAND_MODEL and STAGEHAND_MODEL_API_KEY; Model Gateway is hosted-only. Direct controls need no model credentials.
AI (Stagehand)
The browse_ai tool accepts action: "act" | "observe" | "extract", instruction, optional tabId, and optional schema (JSON Schema, for extract only). Start a browser with browse first. Supported model names and extraction schemas are validated by Stagehand/Zod.
act performs a bounded instruction; observe suggests actions; extract returns data. Results include Stagehand metadata and usage when supplied by the SDK. An act that finds nothing to do is an error with code act_failed. An unsupported schema returns invalid_arguments before any model call. OpenAI models on Model Gateway use strict structured output, which also rejects a schema whose required omits any property, so list every property there and allow null for values that may be missing.
Inference needs no extra setup. Where it runs depends on what is configured:
| Configuration | Inference |
|---|---|
| Nothing (OMP) | The OMP model chosen with /browse ai-model, else the conversation's current model, through OMP's model registry and credentials, with no reasoning level |
| Nothing (a custom loader without a session model) | Browserbase Model Gateway, which picks a model per call |
STAGEHAND_MODEL only |
Model Gateway, pinned to that model (for example google/gemini-3.8-flash) |
STAGEHAND_MODEL and STAGEHAND_MODEL_API_KEY |
That provider directly, bypassing Model Gateway |
The OMP route sends Stagehand's prompt and page context to that model, including custom providers, and bills it the same way. OMP's providers take one system prompt string, so Stagehand's prompt sections are joined into it. It supports Stagehand's single-turn text and structured requests; anything else returns an error suggesting STAGEHAND_MODEL. It runs without Browserbase's managed action cache (results report cache: DISABLED), so repeated steps run inference each time; reported usage counts prompt-cache tokens as input, with cachedInputTokens for cache reads. Model Gateway bills tokens to the Browserbase account at provider list price and uses Browserbase's managed action cache. /browse and status report the configured Stagehand model, the chosen OMP model, omp-session-model, or gateway-auto.
Choosing the OMP model
In the terminal UI, /browse ai-model opens OMP's own model picker (the one /model and /seance use, select-only so the session model stays put) over every configured provider's models, with search, speed, and prices. While a model is saved, a first step offers default to follow each conversation's model instead. Without the terminal UI (RPC), it opens a searchable list of every model OMP can authenticate in the session (the same set --model offers), plus Conversation model. /browse ai-model <spec> takes an exact provider/id or anything --model accepts, including role aliases such as @smol, and saves the model it resolves to; /browse ai-model default clears the choice. The choice is saved for every conversation in the profile in browserbase-ai.json in the profile's agent directory. If the chosen model later loses its credentials, AI calls fail and name it instead of running a different model. A configured STAGEHAND_MODEL still takes precedence.
AI configuration is resolved on each AI operation. Environment changes require restarting OMP; a custom configuration resolver can read updated local configuration without restarting.
JEV mode
JEV mode lets Jev, TypeSafe AI's classifier model, served through Vercel AI Gateway, choose the element for click, fill, and select. Turn it on for the profile with /browse jev on and off with /browse jev off. The footer shows JEV while the mode is on. It starts off, and print-mode and subagent sessions keep it off even when it is saved on. While it's on, the agent is told to use targets. If the classifier's service refuses it for billing or key reasons (HTTP 402, 401, or 403), the call returns jev_unavailable and the mode turns itself off for the profile with a notice.
export AI_GATEWAY_API_KEY='<YOUR_AI_GATEWAY_API_KEY>'
While JEV mode is on, pass target instead of selector:
{"action":"click","target":"Sign in button in the header"}
{"action":"fill","target":"email address field","value":"me@example.com"}
The extension takes a fresh snapshot and sends Jev the page URL and title, the action, the target text, and a label for each named or interactive element, native <select> included. For fill, only text-entry elements are sent (inputs, textareas, and textbox, searchbox, combobox, or spinbutton roles). For select, only native <select> elements are sent. A label is the element's role: name plus up to two named ancestors. Unlabeled controls also carry the text of their adjacent siblings (checkbox near "checkbox 1"), since visible labels often sit next to the control rather than on it. Fill and select values are never sent. Jev picks one element or none. Pages with more than 32 candidates are split into chunks, and each chunk's winner goes on to a final round. If only one chunk picks an element and it's confident, that pick is used and the final round is skipped.
The action runs only when the final confidence is at least 0.5. Otherwise nothing is clicked or typed, and the result has isError: true, code jev_none or jev_uncertain, jev.acted: false, and one of two jev.match values:
none: Jev was confident that no listed element matches, so check the page or rephrase.uncertain: Jev couldn't decide. Pass one of thejev.candidatesselectors straight toselector.
Chunks are small because Vercel AI Gateway's transient 503 rate grows with request size: about 3% under 2k input tokens, 39% at 127 options, and 92% at 32k tokens, while a successful answer takes about the same time at any size. A request that hasn't answered after 1.5 s gets a duplicate, and the first valid answer wins. When every copy so far failed with 408, 429, or 5xx, another starts after a short random delay. A request sends at most five copies.
Choosing the JEV classifier
Jev on Vercel AI Gateway is the default. /browse jev-model picks another System One classifier for every conversation in the profile, saved in browserbase-ai.json; jevModel from a custom resolver, or BROWSE_JEV_MODEL, sets the one used while nothing is picked. A picked classifier wins, and the running browser's next JEV call uses it. The picker lists, with what each reads (text, or text and images):
- Default: clears the choice, returning to the configured classifier.
vercel: Jev on Vercel AI Gateway.workers-airuns Jev 1.14 (typesafe/jev-1.14.0) on Workers AI with theCLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_API_KEYthataskuses.workers-ai/<model>runs another Workers AI classifier, such asworkers-ai/@cf/cloudflare/clef-omni. Workers AI bills third-party models such as Jev through the account's AI Gateway balance; while that balance is empty, Cloudflare answers HTTP 402 and JEV turns itself off.
OMP has no classifier models of its own, so /browse jev-model and /browse ask-model refuse <provider>/<id>, and such settings return jev_not_configured or ask_not_configured. /browse jev-model default clears the choice. Every classifier gets the same 32-option chunks; only Vercel AI Gateway requests are duplicated, and a Workers AI request that fails with a network error, 429, or 5xx is retried once. jev.model names the classifier that answered, and /browse status shows it with whether its credentials resolve.
visual: true on a target call also sends a JPEG of the viewport, for elements that labels do not describe, such as icon-only buttons or canvas controls. It needs a classifier that reads images, such as Jev or Clef on Workers AI. Jev on Vercel AI Gateway reads text only, so there visual returns visual_unsupported before anything is captured. The screenshot covers only the viewport, so scroll the element into view first: a classifier that sees the page may answer none for an element it cannot see. Every chunk request carries the image, about 1.1k input tokens on Clef for a 1280×800 viewport; in a live check on Hacker News, Clef Omni took about 0.75 s longer with it.
Jev can be confident about a plausible but arbitrary pick when the target is ambiguous. For example, "the authentication example" chose Basic Auth at 0.81 out of three matching links. Be specific when it matters.
A request uses up to about 2k input tokens, and a successful one takes about 1.2–1.4 s. A page with up to 32 candidates needs one request. Larger pages send one request per chunk in parallel, then one more for the final round unless a single chunk was confident. On a page the size of Hacker News, the first round takes about 2.5 s at the median.
Calling with target while JEV mode is off returns jev_off. Text nodes are skipped, so an element whose only label is text inside an unnamed container still needs a snapshot and a selector. browse_ai is not affected.
Page questions (ask)
ask answers typed questions about the selected tab without returning a snapshot to the model. Use it to read specific values or to check what an action did. It needs a Cloudflare account with Workers AI and a token that can run Workers AI models:
export CLOUDFLARE_ACCOUNT_ID='<YOUR_ACCOUNT_ID>'
export CLOUDFLARE_API_KEY='<YOUR_WORKERS_AI_TOKEN>'
{"action":"ask","fields":{
"signed_in":{"type":"bool","instructions":"Did the sign-in succeed?"},
"error":{"type":"text","instructions":"Error message shown to the user"},
"total":{"type":"number","instructions":"Order total in USD, including tax"},
"plan":{"type":"choice","instructions":"Which plan is selected?","options":["free","pro","team"]}
}}
The extension takes a fresh snapshot, turns it into page text (role: name lines plus text nodes, about 25k tokens at most), and sends it with the URL and title to Workers AI:
text,number,date,datetime, andduration(with an optionalunit) go to Clairic, Cloudflare's extraction model. The answer is{ value, confidence }.valueisnullwhen the page does not determine it, andtextvalues are copied from the page.boolandchoicego to Clef, Cloudflare's decision model.boolanswers{ value, probability }, whereprobabilityis the chance the answer is yes;choiceanswers{ value, confidence }with one ofoptions./browse ask-model, oraskModelfrom a custom resolver orBROWSE_ASK_MODELwhile nothing is picked, sends them to another classifier and takes the same values as the JEV classifier, for exampleworkers-ai/@cf/cloudflare/clef-omni. Clef Flash (workers-ai/@cf/cloudflare/clef-flash) reads 24,576 tokens, so it gets about 72k characters of page text.visual: truealso sends a viewport screenshot with theboolandchoicequestions, for questions about how the page looks (colors, icons, layout); value fields still read only the text.
When a call has both kinds of field, the two requests run in parallel. A call takes 1 to 20 fields. Snapshot refs stay valid after ask. truncated: true means the page text was cut before it was sent.
ask reads the page as it is when called. click returns once the click is dispatched, so a submission's next page may not have arrived yet. After a click that submits or navigates, wait for a selector on the expected page before asking: loadState alone can resolve on the old page before the navigation starts. If a new document commits while the models are answering, ask discards those answers and reads the new page once. If the page navigates again during that second read, it returns page_changed. In a test where the page replaced itself 250 ms after loading, ask without that check answered for the departed page 4 times in 5, under the new page's URL and title.
Every answer is well formed, not necessarily right. In testing, Clairic answered a field the page did not contain (a PyPI version on a GitHub repository page) with a plausible wrong value at 0.77 confidence. Confirm a value that decides a consequential step with a snapshot. A missing account or token returns ask_not_configured; a refused or failed request returns ask_error. In both cases, take a snapshot instead.
Both models charge only for input tokens: Clairic $0.10 and Clef $0.24 per million (Clef Omni $0.15, Clef Flash $0.038). A typical page costs 1k–25k tokens per request.
Clef was also measured as a replacement for Jev in JEV mode and was not adopted. On 68 targets across 14 real pages, Jev picked 64 correctly at a median of about 0.3 s. Clef, in 64-option chunks, picked 59 at about 0.55 s with no wrong clicks. Clef Flash picked 42–49 and made one wrong click at some chunk sizes.
Network capture (HAR)
har_start records the network traffic of every tab in the browser, including tabs opened later, and har_stop writes it as a HAR 1.2 file:
{"action":"har_start"}
{"action":"open","url":"https://app.example.com/login"}
{"action":"har_stop","path":"/abs/scratch/capture.har"}
Stagehand 4.3 forwards only console events from DevTools, so the recorder opens its own DevTools connection to the browser: the launched browser's debugging port locally, or the session's connect URL on Browserbase. Requests keep their Cookie header and responses their Set-Cookie headers (from Chrome's ExtraInfo events). Request bodies are kept, and so are response bodies up to 5 million characters; larger ones are counted in bodiesOmitted. Each redirect hop is its own entry. Requests still in flight at har_stop are dropped and counted as unfinished. Extension and data: URLs are left out. Entries carry _tabId and _resourceType.
The HAR holds cookies, tokens, and bodies, so it goes only to the file, created with mode 0600 (default: a new file under the system temp directory). The tool result reports the path, entry and tab counts, hosts, and failures, never values. Read the file with scripts. Attached browsers refuse har_start, because their tabs are the user's own. close discards a recording that was not stopped. status reports recordingHar.
Verified live on a launched local browser (a cookie-setting redirect, a JSON POST, and a second tab). The hosted path uses Browserbase's connect URL for a second DevTools client and has not been run live.
Live View hand-off
Sign-in never goes through Live View: the agent imports cookies from your browser instead (see below). live_view is for a user who explicitly asks to watch or take over the browser. It asks Browserbase for the session's Live View link and opens it in the user's default browser, where they can click and type in the same remote browser the agent drives. The tool result only says the view was opened: the link controls the logged-in browser, so it never reaches the model, the transcript, or OMP logs. Snapshot refs taken before the hand-off become stale; take a new snapshot once the user says they are done. Print mode and subagents return live_view_needs_user.
Proxies
start with proxyCountry (an ISO 3166-1 alpha-2 code such as GB or DE) routes the whole browser through Browserbase's residential proxies exiting in that country, so sites serve their local storefront, currency, and consent banners. Proxy traffic is metered against the plan's proxy data allowance, so it stays off by default. The country is fixed for the session: a later start with a different country returns proxy_change; close first. status reports the active proxyCountry.
Domain limits
start can fence a browser in. allowedDomains keeps it on those bare domains and their subdomains, and blockedDomains refuses them:
{"action":"start","allowedDomains":["github.com","githubusercontent.com"],"blockedDomains":["gist.github.com"]}
Stagehand enforces both inside the browser by failing every matching http(s) request, page assets included, so an allowlist must also name the CDN, API, and sign-in domains the task needs. start, open, and tab_new refuse a fenced URL with domain_blocked before navigating. When a page itself navigates or redirects to a fenced host, the tab ends on Chrome's error page; start and open report that as domain_blocked too. The fence covers local, attached, and hosted browsers, and survives a repaired hosted connection.
A custom resolver can return allowedDomains and blockedDomains for every browser in the profile (or set BROWSE_ALLOWED_DOMAINS and BROWSE_BLOCKED_DOMAINS, comma-separated). A start can then only narrow the profile's allowed domains (domain_policy_widened otherwise) and add blocked ones. status reports the lists; close the browser before changing them. A fence suits signed-in work: after cookie_import, a browser kept on the site and its identity provider cannot be sent elsewhere by page content.
For hosted sessions, navigationPolicyId from the resolver (or BROWSERBASE_NAVIGATION_POLICY_ID) also binds an existing Browserbase navigation policy through browserSettings.riskPolicyId. Browserbase enforces it only when navigation policy enforcement is on for the project, and it checks navigations, not the requests a page makes itself.
Sessions and saved logins
A hosted browser starts lazily through start, open, tab_new, or cookie_import, stays available between turns, and has a one-hour lifetime. The default viewport is 1280×800. Call close when finished. Conversation switching and shutdown also request cleanup. Only this extension's owned browser is closed. There is no attachment to another agent's browser or recovery after a OMP restart.
Sessions are created with keepAlive, so the remote browser survives a dropped connection. When Stagehand reports the connection closed, the extension checks the session with Browserbase and reconnects to it, keeping tabs, cookies, and logins. An action that never reached the dead connection is then run once on the new one; one that may have run returns connection_restored, so the agent takes a snapshot instead of repeating it. If Browserbase has ended the session, the call returns session_lost. status asks the browser itself and reports active, tab_unresponsive, reconnected, ended, or unresponsive. A reconnect reinstalls the dialog init script, which the restarted runtime forgets. On plans without keepAlive, the first launch falls back to sessions that end with their connection, and a drop returns session_lost.
Stagehand drives the browser through a runtime extension. The extension is uploaded to Browserbase once per OMP conversation, on the first launch, and reused by every later launch, instead of being uploaded and deleted around each one. It is deleted at shutdown, after the browser is released. A failed upload is retried on the next launch.
Direct operations default to a 30-second timeout, a fixed wait to its durationMs plus 5 seconds, and AI operations to 120 seconds. timeoutMs can override the deadline up to 120 seconds. On the deadline, or on cancellation, the extension checks whether the browser still answers within 2 seconds: if it does, the browser and its logins are kept; if not, it is released. A kept browser whose tab does not answer a trivial script within 2 seconds is reported as such, with the steps to continue in a new tab. Page-level waits, navigation, and screenshots get up to 2 seconds less than the deadline (never less than half of it), so they usually return an ordinary error first. An action might already have completed: inspect the website's resulting state before retrying a submission. If release cannot be confirmed, the tool reports the session ID and allows another close attempt. After abrupt process termination, a session lives until its one-hour lifetime ends.
Fresh sessions have no saved login. For persistence, create a context with context_create, then call start with its returned contextId. Changes are saved when that browser closes. Reopening a context just closed by this extension waits briefly for synchronization. Use a different context per site/account and avoid using the same context concurrently. Changing contexts requires closing the current browser first. A context must belong to the configured Browserbase project and must have been created by this extension under the same OMP profile (OMP_PROFILE, else default). context_create names contexts <profile>:<name>, and start rejects any other context with context_profile, so one profile cannot load another profile's logins, even when profiles share a Browserbase project. Separate Browserbase projects per tenant add isolation at the account level. Projects are created in the Browserbase dashboard; the SDK has no project-creation API.
Context IDs appear in the conversation. A custom resolver can also set contextHistoryPath to append identifier/name/project records to a profile-specific local file. No cookies, tokens, or browser storage are exported to that file. Context selection is always explicit.
Use start with backend: "local" for localhost and private networks. This extension does not automatically switch browser backends on failure.
Import cookies from your browser
On macOS, your everyday browser is how hosted and launched local browsers sign in. Every Chromium-based browser that shares Chrome's cookie format is supported:
browser |
Browser | Keychain item |
|---|---|---|
chrome |
Google Chrome | Chrome Safe Storage |
chromium |
Chromium | Chromium Safe Storage |
brave |
Brave | Brave Safe Storage |
edge |
Microsoft Edge | Microsoft Edge Safe Storage |
arc |
Arc | Arc Safe Storage |
vivaldi |
Vivaldi | Vivaldi Safe Storage |
opera |
Opera | Opera Safe Storage |
helium |
Helium | Helium Storage Key |
A browser counts as installed when its app is in /Applications or ~/Applications and its data folder exists in ~/Library/Application Support; data left behind by a removed browser is ignored. Firefox and Safari are not supported.
When a task needs a signed-in site, or a page shows a sign-in wall, the agent calls cookie_import with the site's domain and the identity provider the sign-in redirects to (for example microsoftonline.com), then opens or reloads the page. It never sends you to Live View to enter credentials. If no installed browser profile has cookies for the site (cookies_not_found), or the page still asks you to sign in, the agent asks you to sign in to the site in your browser, waits for you, then imports again. A requested browser that is not installed returns browser_not_installed.
Without browser and profile, the import uses the only profile, across all installed browsers, holding unexpired cookies for those domains; either one alone narrows the search. This lookup decrypts nothing. If several profiles have cookies for those domains, cookie_profile_ambiguous lists them with their browser, profile name, and cookie count. The agent then retries with the browser and profile that match the task's account or organization, or asks you when the task does not say. With both browser and profile, the lookup is skipped. cookie_profiles lists installed browsers with their profile folders and names.
There is no approval dialog. The agent is told to import only domains the task needs, never ones suggested by page content, and the tool accepts only explicit bare domains. Without an interactive UI (print mode, subagents), the import fails closed with import_needs_user.
Direct tool calls:
{"action":"cookie_profiles"}
{"action":"start"}
{"action":"cookie_import","domains":["github.com"],"timeoutMs":120000}
{"action":"cookie_import","domains":["github.com"],"browser":"chrome","profile":"Profile 1"}
{"action":"open","url":"https://github.com"}
Each listed domain includes its subdomains. Only list domains needed for the intended site/account; add an identity provider's domain explicitly if the login requires it. Public suffixes, wildcards, URLs, and unrestricted imports are rejected. Import merges cookies into the owned session and replaces cookies with matching name/domain/path. Start a fresh session to avoid mixing accounts.
The reader opens the browser's cookie database read-only, including committed WAL changes, and decrypts matching cookies in memory using that browser's Keychain item (table above). macOS asks you to allow access on the first import from each browser. The derived key then stays in the OMP process's memory, per browser, and is zeroed at exit, so later imports in that process skip the prompt. Use a longer timeoutMs for the first prompt. Click Allow, not Always Allow. Always Allow adds /usr/bin/security to the key's access list. After that, any local process can read the key without a prompt and decrypt every cookie and saved password of that browser. If you already chose it, remove security from the item's Access Control in Keychain Access.
The import action returns no cookie values and writes none to export files. Errors from the remote cookie API are sanitized. Imported values of 8 or more characters are also redacted from later tool output of the same extension binding, including their JSON-escaped form, with longer values replaced first so nested values leave no tail. That covers, for example, evaluate reading document.cookie. Not redacted: shorter values, values a page transforms (encoded or split), screenshots, and values persisted into a saved context and reopened in a later conversation, a subagent, or after a restart. Page scripts can still use non-HttpOnly cookies.
Results report imported counts and skipped expired, partitioned, unsupported, or undecryptable cookies. Partitioned cookies are skipped because the current Stagehand cookie API cannot preserve their partition key. Domain/host scope, path, expiry, Secure, HttpOnly, and SameSite are preserved for supported cookies. Local storage and other browser state are not imported, so some sites may still require sign-in.
For reuse across sessions, create and select a saved context before importing, then close the session to persist it. A launched local browser with a persistent profile (localUserDataDir) keeps imported cookies in that profile. Otherwise the import lasts for that session. All supported browsers use Chromium's cookie database format and v10 encryption; Helium names its Keychain item per its branding patch. Opera keeps its single profile directly in its data folder, reported as Default.
Custom local configuration
The package exports createBrowseExtension(resolveConfig, options?) and environmentConfig(). options.defaultBackend ("browserbase" or "local") is where new browsers start while the profile has no backend saved with /browse backend; a saved backend wins. options.configHint names where the loader's own settings (credentials, Browserbase project and region) live, and /browse status shows it. The resolver receives { sessionId, profile, cwd, signal, purpose, backend?, classifier? }; purpose is browser, ai, jev, or ask, and classifier is the one chosen with /browse jev-model or /browse ask-model, which replaces the returned jevModel or askModel. It returns { apiKey?, projectId?, model?, modelApiKey?, jevApiKey?, jevModel?, askModel?, cloudflareAccountId?, cloudflareApiKey?, contextHistoryPath?, region?, localUserDataDir?, localExecutablePath?, allowedDomains?, blockedDomains?, navigationPolicyId?, remoteBrowsers? }. Browserbase credentials are required only for the hosted backend: skip their retrieval when backend === "local". Pass () => environmentConfig() to use environment variables; BROWSERBASE_REGION applies only to hosted launches. Resolve model credentials only for ai, the AI Gateway key only for jev, and the Cloudflare token only for ask and for jev when the classifier (classifier, else jevModel) names Workers AI (the AI Gateway key also for ask when its classifier is vercel), so optional keys cannot block direct controls. Keep configuration outside the package and honor the cancellation signal.
OMP specifics
- OMP retains extension bindings across session switches;
session_switchandsession_shutdownboth release the browser, and the switched-to conversation rereads the profile's settings. - JEV mode and
/browse backend localappend their instructions to OMP's system-prompt array. - Each call declares an approval tier, so OMP's approval modes ask only where it matters. Reads (
status,snapshot,ask,screenshot,tabs,tab_select,wait,cookie_profiles, andbrowse_aiobserve/extract) areread; navigation, controls,har_start/har_stop,act, and session changes arewrite;evaluate,cookie_import, and attaching to an existing browser areexec. Under Ask and remember, a grant covers the site a call opens or acts on (the selected page's origin), the domainscookie_importreads, the endpointstartattaches to, orbrowse:sessionfor session changes such asclose. - Browser cookie databases are read with
node:sqlite(integers read as BigInt, because Chromium timestamps exceedNumber.MAX_SAFE_INTEGER); under Bun, as in the tests,bun:sqliteis used.
Development
The browser core (sessions, Stagehand, JEV, ask, HAR, cookie import, the /browse parser) lives in @edlsh/browse-core, shared with pi-browse and hermes-browse. This package adds OMP's tool registration, model adapter, and pickers.
bun install --frozen-lockfile --ignore-scripts
bun run check # tests, types, and package contents
See CONTRIBUTING.md for the CI matrix and releases.
The real-Chromium and hosted Browserbase suites, and the cookie, HAR, and attach-guard unit tests, run in browse-core. Unit tests here cover invalid local options, credential independence, unsupported hosted features, lost connections without replay, and late launch/attach cleanup after shutdown.
The focused tests cover tool registration, browsing state, image output, AI configuration and dispatch, AI readiness before a session, context records, profile-scoped contexts, independent conversations, stale references, shutdown cancellation, page-level timeouts that keep the browser, one Stagehand extension upload reused across launches and deleted after release at shutdown, redaction of imported cookie values including nested values, promptless cookie_import with automatic browser and profile choice (headless calls, missing cookies, uninstalled browsers, and ambiguous profiles never read cookies or start a browser), installed-browser detection, profile discovery and names across browsers (including Opera's single profile), the per-process, per-browser Keychain key cache, reconnecting after a dropped connection without replaying a possibly-sent action, deadlines that keep a responsive browser, grouped positional XPath, the keepAlive fallback, and JEV targeting. They also cover dialogs answered by the installed init script (run in a node:vm realm per document, including after a reconnect), blocked tabs reported instead of hanging, select option checks, error codes for Stagehand failures, argument validation for URLs and keys, and concurrent browse_ai calls keeping their own models. The JEV test covers the off-mode gate, the status-only bare /browse, a chunked pick that acts, a low-confidence result whose candidates stay usable, and a gateway refusal that turns JEV off.
Measuring tool latency
Direct operations validate targets, act, and read result metadata beside the browser in one Stagehand batch. References check tab/generation and URL, and a fresh snapshot confirms the ref still names the same DOM node (frame ordinal and backend node). Stagehand cannot cancel a running batch: on a deadline or cancellation the batch stops at its own timeout, an already-running action can still finish, and the tool keeps the browser only if it still answers, so inspect state before retrying. Browser rendering, frame-rate, scheduling, and launch defaults are unchanged.
The opt-in benchmark invokes the registered browse tool with real SDK calls. It uses a deterministic fixture, checks resulting form/tab state, and measures invocation-to-result time, including validation, queueing, browser work, and result preparation—not model thinking between calls.
CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
BENCH_BACKEND=local BENCH_OUT=artifacts/performance/local.json bun run benchmark
# Hosted credentials come from the environment; keep the same region in both versions.
BROWSERBASE_REGION=us-west-2 BENCH_BACKEND=browserbase \
BENCH_OUT=artifacts/performance/hosted.json bun run benchmark
Defaults are three sessions, ten measured workload repetitions per session, and three warm-ups. BENCH_SESSIONS, BENCH_REPETITIONS, and BENCH_WARMUP control sampling. BENCH_ROOT selects a frozen original checkout, including its original installed SDK, for matched comparisons; extract archived baselines outside this repository so Bun does not discover their old tests. Keep the fixture and operation sequence unchanged, alternate original/modified runs, and retain every sample and failed attempt.
JSON artifacts include per-call RPC timings (without arguments or credentials), source/SDK hashes, browser build, p50/p95, and separate cold-start/navigation measurements. Compare each backend independently and inspect per-action tails as well as the whole-workload median.
Reconnecting depends on Stagehand 4.3 internals: the runtime inside the remote browser stays initialized after its client drops, so resetStagehandRuntime in browse-core's src/session.ts sends the runtime's own stagehand.close over a raw CDP connection before attaching again. Recheck it, with a live drop-and-reconnect run, whenever @browserbasehq/stagehand is upgraded. Tests use SDK stubs and create no cloud resources. Live model-provider validation requires separately configured credentials.