Skip to main content

Agents & automation

Agent Browser

The agent browser is a real browser surface the agent drives itself - it can load a page, read the DOM, screenshot it, click, and type. It is separate from the Preview tab (your own dev server), it is available in both Chat Mode and Code Mode, and browsing anywhere other than localhost is off until you turn it on.

What it is, and where it lives

One tool drives it: preview_interaction, with fifteen actions - reads getSnapshot, getDom, screenshot, getConsoleErrors, wait_for; navigation navigate, back, forward; and click, type, submit, pressKey, selectOption, hover, scroll. wait_for polls for a selector, page text, a URL substring, or load:"complete" to become true, up to a timeout (default 5s, max 15s) - useful for content that appears after the page itself has loaded. A second tool, preview_script, runs a short batch of the navigation-phase actions (navigate, click, getDom, wait_for, screenshot) in one call - it runs each step through the same gates as a single call, and typing and submitting are never part of a batch.

The same tool serves two different surfaces:

  • Preview - your own project's dev server on localhost. Always available, never gated, not affected by any setting on this page. To open your project's preview the agent should use open_preview, which auto-detects the address instead of guessing a port.
  • Agent browser - real, non-localhost websites. This is the part that is off by default and gated below.

Opening the panel yourself:

  • Code Mode - the compass icon in the left Activity Bar. It only appears when agent web browsing is on and air-gap is off.
  • Chat Mode - the Agent Browser button in the chat header, same visibility rule. It opens as a resizable right-hand rail; drag its edge to resize (clamped 250–1200px, default 380px, and never past the point where the chat column would lose its own minimum) and the width is saved in ui.panel_sizes, so it survives a restart. Its empty state also has an Open a browser button, the same one Code Mode's panel has, for typing an address yourself without waiting for the agent.

You do not have to open the panel yourself: when the agent navigates, the panel for the mode you are currently in opens on its own (the Code-Mode tab, or the Chat-Mode rail). Before 2026-08-14 only the surface's internal visibility flag flipped, so with the panel closed a successful navigate showed you nothing.

Non-localhost browsing is off by default

Agent browsing beyond localhost is gated by browser.widened_enabled, which ships off. Turn it on at Settings → Privacy & Safety → Agent Web Browsing (non-localhost). Turning it on shows a confirmation dialog; turning it off applies immediately.

The gate is enforced twice, on purpose:

  1. Registration time - when the setting is off, the tool's own description tells the model only localhost is reachable. The affordance is not advertised at all.
  2. Dispatch time - a non-localhost navigate is refused even if a stale session or a hallucinated call gets that far.

preview_interaction itself is never removed. What changes is what it will do.

It takes effect on the next new chat or session, not mid-conversation. A tool's definition is fixed for the life of a session, so flipping this switch does not change what the agent already believes it can do.

browser.widened_enabled is a global-only setting - it is deliberately not in PROJECT_CONFIG_ALLOWED_KEYS, so a repo you clone cannot turn on outbound agent browsing on your machine by committing a config file.

Air-gap blocks it entirely

With air-gap mode on, non-localhost agent browsing is off regardless of the toggle: the widened capability is not offered to the model, and every state-mutating action against a non-localhost page - navigate, click, type, submit - is refused at dispatch and recorded in the air-gap refusal ledger. The five read actions (getSnapshot, getDom, screenshot, getConsoleErrors, wait_for) are deliberately not refused: reading a page that is already loaded - or polling it for a condition - sends nothing off your machine, so under an air-gapped or vaulted project the agent can still look at what is on screen - it just cannot change it or go anywhere new. Both Settings toggles are disabled and show an explanatory banner. Turning air-gap on also wipes every persisted browser login, after a confirmation that names the affected sites.

The localhost preview path is untouched by air-gap - loopback is not egress.

A project Air-Gap Vault covers the dispatch half of this. The effective air-gap is global OR project vault, and preview_interaction reads it that way - every air-gap decision inside the tool goes through getEffectiveAirGap(projectVault), with the vault flag threaded onto the tool context per call. Vaulting a project therefore refuses its non-localhost agent browsing without touching the global toggle.

The registration-time half is not vault-aware yet: the tool array is still built from the global general.air_gap value, so under a project vault with global air-gap off the widened surface is still described to the model, and the refusal happens when it tries. Everything the vault is supposed to stop is stopped - but it is stopped at dispatch, not by silence.

What "navigated" means

A successful navigate means a page loaded, not that the request was accepted. The tool waits for the browser to commit a document before reporting success, and returns one of three outcomes:

Result Meaning
navigated A page loaded. The panel is showing it.
load_failed The load started and died before any page appeared - most often a cancelled redirect. The agent is told explicitly not to claim the page is open or describe its contents.
load_pending Nothing had loaded within the wait budget (8 seconds). Neither success nor failure - the agent is told to look before saying anything.

This matters for sites that redirect. A site whose bare domain forwards to www can have that hop cancelled by the browser's redirect gate, leaving the panel empty. Until 2026-08-14 that case still reported success, and the agent would then describe a page it had never seen.

A successful navigate still tells the agent nothing about the contents of the page - it has to call getDom or screenshot, or wait_for a condition on the new page, to find out.

A widened (non-localhost) navigate that times out now double-checks before giving up. Before reporting a bare failure, it makes one more check of the current address; if the page actually landed at the destination despite the timeout, the result is load_pending (not error) with that address, instead of relying on the agent correctly reading a hint that the site might not really be down.

Clicking now reports what happened after the click, not just that the click itself landed: outcome is navigated (the page changed and settled), load_pending (still loading when the check gave up), or same_page (nothing navigated - most clicks). A click that doesn't navigate is not slowed down waiting to find that out.

Reading a page: snapshot first, act by ref

getSnapshot returns a compact list of the page's interactive elements - links, buttons, inputs, selects - each with a short ref like e3, plus the visible text blocks, with scripts and styles dropped and elements that are hidden by CSS or ARIA flagged [hidden] rather than silently listed as visible. It is viewport-first (fullPage: true reads the whole document) and is typically a few hundred tokens, where raw getDom is 8,000 characters of HTML.

The agent is told to call it before clicking or typing and then pass the ref - click with ref: "e3" - instead of writing its own CSS selector. click, type, submit, selectOption, hover, pressKey and wait_for all accept a ref. Refs belong to the snapshot they came from: after the next getSnapshot, old refs are refused (stale_ref) with an instruction to re-snapshot, never silently pointed at a different element.

Everything the agent reads out of a page - snapshot, DOM, element text - reaches it inside one clearly fenced block marked as untrusted page content, so instructions written into a page are data to the model, not commands. That is a mitigation, not a boundary; see "What it cannot do".

Every action asks - navigate, click, type, submit

Action When you are asked
navigate, back, forward The first time the agent goes to a site this session. Cached per site afterwards. Going back or forward resolves the history target first and runs it through the same gate - a refused target is never moved to.
click, pressKey, selectOption, hover, scroll The first time on a site, then cached - unless persistent logins are on, in which case every one asks.
type Every call on a non-localhost site. Never cached - the value is the consequence, and it differs each time.
submit Every call on a non-localhost site, even on a site you already approved. Never cached.
getSnapshot, getDom, screenshot, getConsoleErrors, wait_for Never - reading a page that is already loaded sends nothing anywhere.

On your own localhost dev server, type and submit skip this ask by default - that loop (fill a form, submit, check the result) is the most common one in Code Mode, and asking on every keystroke against your own machine was pure friction. The one exception: a password field, a card number/expiry/CVC field, a one-time-code field, or anything else that looks credential-shaped still asks every time, on localhost or anywhere else - typing into a password field is refused outright, never even asked. A result that skipped the ask this way carries approval: "localhost_exempt", so it is visible in the tool result, not a silent behaviour change. Non-localhost sites are unaffected - the table above still applies there.

The cards show consequence, not selectors: a click card shows the element's real visible text, and a submit card shows the actual form values read out of the page, not the values the agent claims it is submitting. If the page changes those values between your approval and the dispatch, the submit is refused and the agent has to ask again rather than submitting something you did not see.

Two more re-ask triggers on an already-approved site: a navigate whose query string differs from the one you approved (data leaving in a URL), and a click on a submit control after the agent has typed into a form - that gets routed through the stronger form-values approval instead of the weaker click card.

A navigate to a URL the agent read out of page content rather than reasoning to itself (urlSource: 'page_link') is auto-denied for a site you have not already approved.

What one approval covers

The unit of approval is the site - the registrable domain - not the exact URL and not the origin. Approving reddit.com covers www.reddit.com and its other subdomains.

The boundary comes from the Public Suffix List, so multi-tenant hosts stay separate: user1.github.io and user2.github.io are two different sites, not one. A suffix the bundled list does not know falls back to treating the whole hostname as its own site - that costs you extra prompts, never a merged approval.

Scheme upgrades follow, downgrades never do. An http approval covers an https load of the same site. An https approval does not cover http - a downgrade is refused or re-prompted, because a plaintext redirect is exactly how a network-position attacker would feed the agent a page.

Hosts with an explicit port, bare IPs, and localhost cannot be site-scoped, so they fall back to exact-origin matching. Approving http://localhost:3000 covers nothing else.

Browse approvals are remembered across restarts, per project, in the local database (browser_origin_approvals) - you are not asked again next launch for a site you already approved, and revoking a site is durable too. Rejections are remembered only for the life of the backend process, so the agent cannot re-prompt you every turn for a site you just refused.

A short list of sites can never be automated at all, whatever you approve: major banks and payment processors, brokerages, government-ID portals, and adult sites. A navigate to one of them is refused with never_automate before any approval card appears. There is no setting that unlocks it - this is policy, not a security layer; air-gap and containment are the security.

Keeping the agent signed in to a site

By default the agent's browser keeps no cookies - every session starts signed out.

Keep the agent signed in (Settings → Privacy & Safety, browser.persistent_sessions, off by default) lets you grant persistence to individual sites. The global switch alone grants nothing: the first time the agent navigates to a site while the switch is on, a consent card asks about that site specifically.

What granting means, plainly: the agent acts as you on that site until you revoke it. Anything you are signed in to there, it is signed in to.

  • It survives an app restart and idle teardown. That is the entire point of it.
  • It is per site. Each granted site gets its own isolated Electron partition (persist:agent-site-<domain>), so one site's cookies are never visible to another.
  • Turning the global switch back off stops new sites from being granted. It does not wipe sites already granted - revoke those explicitly.
  • The consent card has a "Don't ask again for this project" option. That auto-grants future sites in that project - but it is checked after the decline gates, so it can never override a site you explicitly declined.

Granting only ever happens at the point of use. There is no way to type a domain into Settings and pre-approve it.

Revoking, and undoing a decline

Everything below is at Settings → Privacy & Safety. The Agent Browser panel's ⋯ menu → Manage allowed sites is a shortcut to the same place - it opens Settings on Privacy & Safety (as an editor tab in Code Mode, as the settings overlay in Chat Mode). The menu only links; it shows no grants and changes none, so signing a site out is always done in Settings.

These controls stay reachable even when the toggle above is off - an install can still hold grants from an earlier build.

Persisted sites - every site currently holding a login, with a Sign out button each and a Sign out of all button. Signing out clears that site's stored login; you will be asked to grant again next time.

Declined sites - a site whose persistence card you explicitly declined is recorded durably and stays declined across restarts; the card does not fire again. Click Ask again on its row to clear that record so the next navigate re-asks.

A card that timed out or had nowhere to render is weaker evidence than a click, so it only suppresses re-asking for the rest of that session and is not written to the durable list.

Projects with "don't ask again" set - each project that has the blanket auto-grant, with its own Ask again button to revoke it.

Turning air-gap on wipes all persisted logins at once, with a confirmation naming them first.

Typing works. Logging you in does not.

The type action is real and it works, including on framework-controlled inputs - it sets the value and dispatches input/change events. If the agent tells you it cannot type, that is the model being wrong about its own tools, not a missing feature.

What it will not do is enter your credentials. Typing into a password or credential-class field is refused outright - not gated, not approvable, no setting relaxes it. The field is classified from the real DOM metadata Bodega reads itself, never from the agent's claim about what the field is: type="password", an autocomplete value in the password / card-number / CVC / one-time-code family, or a name or id matching the password, secret, token, api-key, CVV, card-number, or SSN families.

Separately, the value is scanned against the same credential table the shell tool uses. A value that looks like an API key, token, or private key is refused before it reaches the page, whatever field it was headed for.

So: the agent cannot log you in. You log in yourself, in the agent's browser panel, and then grant persistence if you want it to stay signed in.

One honest caveat on type: some pages submit or fetch on input with no separate submit step. That is not visible to this tool. It is bounded by the site already being approved and by the credential scan on the value - nothing else.

What it cannot do

It cannot tell which account you are signed in as. getDom always returns authenticatedUser: "unknown" as a distinct, labelled field, on purpose. The page does not reveal it, and the author of a post in a feed is not you - a model reading a timeline and concluding "I am logged in as this person" is guessing. If identity matters, tell the agent who you are; do not ask it to work that out.

No thread composition. There is no compose action. The agent can type into fields that already exist on a page and submit a form; it cannot construct a multi-post thread or drive a rich composer as a unit.

No media upload. There is no file-upload action. Attaching an image or video to a post is not something the tool can do.

Other limits worth knowing:

  • getDom output is capped at 8,000 characters (after scripts and styles are stripped) and credential-scanned before the agent sees it; getSnapshot is the cheaper read.
  • Only http: and https: are eligible. file:, ftp:, javascript: and the rest are refused.
  • The agent cannot point the browser at Bodega's own local servers - the backend API, llama-server, the embeddings server - it gets a corrective hint instead of the wrong page.

The risk nobody has solved: a page the agent reads can contain text written to steer it. The outbound-URL scanning here catches plaintext secrets, not base64- or hex-encoded ones. It is a speed bump, not a boundary. That is why this is off by default.

Screenshots stay in the transcript

A screenshot the agent takes now persists in the conversation after the turn ends, in both Chat Mode and Code Mode. It used to disappear once the turn finished, which made it impossible to check what the agent had actually looked at.

What is stored is a downscaled JPEG thumbnail, not the full-resolution capture. The image the model actually reasons over is ALSO capped, separately: by default it is resized to a 1280px longest edge and re-encoded as a JPEG before it ever reaches the model, so a 4K monitor does not turn one screenshot into most of a prompt. The agent can pass fullRes:true on the screenshot action for a bigger capture when it genuinely needs the detail - that is still capped, at 4096px, never fully unbounded. Neither of these caps affects what you see in the panel; both only shape what leaves the machine in the tool result. Up to 8 screenshots per session are kept in the transcript; past that the oldest is dropped, and the drop is logged rather than silent.

Tips

Toggling the setting mid-conversation does nothing. Enabling agent web browsing takes effect on the next new chat or session - the tool description the model is reasoning from was fixed when the session started. If the agent insists it can only reach localhost right after you flipped the switch, start a new chat rather than arguing with it.

If the agent reaches for web search instead of browsing, check the setting first. With widened browsing off, the tool's description tells the model localhost only - and it believes it, then falls back to web_search. That symptom is the gate, not a model failure.

Approvals reset when you restart the app. Persisted logins survive a restart; browse approvals do not. A site you approved yesterday will ask again today, and that is expected - the two records have deliberately different lifetimes.

Turning on "Keep the agent signed in" makes every click ask. While persistent sessions are active the per-site click cache is bypassed, so clicks stop being approve-once. That is intentional - a click while signed in acts as you - but it is a real jump in prompt volume, and it is worth knowing before you assume something is broken.

Approving a site approves its subdomains. "Allow example.com" also allows www.example.com, api.example.com, and anything else under it. If you only meant the one page, there is no narrower option - decline and let it ask again.

This page mirrors the in-app docs hub for app version 1.0.0-beta.40. Found something unclear or out of date? Tell us on Discord. New here? Download the free beta and follow along.