Skip to main content

More

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 seven actions - screenshot, getConsoleErrors, getDom, navigate, click, submit, type. A second tool, preview_script, runs a short batch of the navigation-phase actions (navigate, click, getDom, 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 a non-localhost navigate or submit is refused at dispatch and recorded in the air-gap refusal ledger. 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 to find out.

Every action asks - navigate, click, type, submit

Action When you are asked
navigate The first time the agent goes to a site this session. Cached per site afterwards.
click The first time on a site, then cached - unless persistent logins are on, in which case every click asks.
type Every call. Never cached - the value is the consequence, and it differs each time.
submit Every call, even on a site you already approved. Never cached.

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 live in memory, per project path, for the life of the backend process. They are not written to disk and do not survive an app restart - you will be asked again next launch. Rejections are remembered the same way, so the agent cannot re-prompt you every turn for a site you already refused. This is a known deviation from the original spec, which called for a persisted list.

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 and credential-scanned before the agent sees it.
  • 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 full-size image the model reasons over is held in memory for the turn and never written to the database. Up to 8 screenshots per session are kept; 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.35.1. Found something unclear or out of date? Tell us on Discord. New here? Download the free beta and follow along.