name: native-browser-safety description: "SAFETY rules for any agent driving the Adom browser extension (nbrowser_* verbs). This bridge drives the user's REAL, signed-in Chrome/Edge - their live tabs, forms, sessions, saved logins, and other AI threads. Read this BEFORE driving. The cardinal rule: only ever act on a window/tab YOU opened (nbrowser_open_window); NEVER navigate, reload, or close a window/tab the user already had open (this includes the OS-level desktop_navigate) - doing so destroyed a user's other Claude threads once. Also: background by default (never foreground unless asked), the CDP "debugging" bar, credentials/autologin are gated and never expose passwords, recording is WGC-first, and the user blocklist is hands-off. Match on: browser safety, agent browser isolation, don't touch my tabs, navigated my window, wiped my threads, never reuse a window, background by default, real browser safety, nbrowser safety, drive the real browser safely. Part of the adom-browser-extension."

Part of adom-browser-extension. Verb surface: driving-the-extension. Recording: native-browser-recording.

Driving the user's REAL browser safely

nbrowser_* is not pup. It runs inside the user's actual, signed-in Chrome/Edge, so every action lands on their live browser: open tabs they are reading, forms they are filling, authenticated sessions, saved logins, and other AI threads' tabs. That power is the whole point (it sails through logins and captchas pup cannot), and it is exactly why a careless move is destructive. These rules are not optional polish; one of them exists because an agent wiped a user's other Claude threads.

⛔ Cardinal rule: only ever drive a window/tab YOU opened

Open your own surface and work only in it. nbrowser_open_window gives you a background window + a sessionId; do ALL your driving through that sessionId.

REGISTER every window (v0.1.0+). nbrowser_open_window now REQUIRES three fields or it refuses (session_unregistered):

nbrowser_open_window {sessionId:"<short task id>", thread:"<which AI thread you are>",
                      purpose:"<human-readable, e.g. finishing John's IRS signup>", url:"…"}

purpose shows up in nbrowser_status and the Session Monitor so the user can see who is driving each window and why. No sessionId/thread/purpose = no window.

⛔ Track & reap the windows a page SPAWNS (the leaked-compose fix)

A page you drive can open a new window you did not ask for: clicking an email in Google Contacts pops a Gmail compose window; target=_blank links, window.open, OAuth popups, and PDF viewers do the same. These are standalone windows in no session - historically unreachable, so they leaked.

Now they are auto-adopted into your session. The moment one appears:

  • a spawned_window event fires (see nbrowser_events), and
  • your next driving verb is refused (unacknowledged_spawns) until you deal with it.

Deal with it: nbrowser_owned {sessionId} lists everything you own (opened + spawned); then either keep what you need or nbrowser_cleanup {sessionId} to close the litter (it closes ONLY what you spawned, never the user's own windows). End every task by closing what you own: nbrowser_close_window {sessionId, includeSpawned:true}. If you ever find a TRUE orphan ABE can't attribute, nbrowser_force_close is a deliberately hard, two-step last resort (it can never close another thread's window). It has TWO forms behind the SAME gate (ticket → attest with a written reason): {windowId} for windows the extension can see, and {hwnd} (v0.1.23+, from desktop_list_windows) for browser windows the extension CANNOT reach — e.g. Chrome/Edge litter that a browser_open/pup call or an HD test left behind, or a window in a profile with no extension. The hwnd form only targets real browser windows and sends a graceful WM_CLOSE. So cleaning up ANOTHER tool's browser litter is allowed — but only through this gate, with a real justification, and never a window a live thread owns.

⚠️ Pitfall — reading a contact spawns a compose: to read a person's email/phone in Google Contacts, do not click the mailto/email link (that spawns a compose window). Navigate the person's contact URL or nbrowser_fetch_url the data instead.

NEVER navigate, reload, close, or repurpose a window or tab the user already had open:

  • not with nbrowser_navigate / nbrowser_reload / nbrowser_close_tab / nbrowser_close_window,
  • and not with the OS-level desktop_navigate either (same destruction, different door).

Why it is this strict: navigating a user's active tab yanks their live page out from under them and wipes anything unsaved - including other running AI threads. That actually happened - twice (2026-06 and again 2026-07-08, mid-Meet-call), which is why as of extension v0.0.21 the code enforces it: every mutating verb refuses a bare call (no_target - the active-tab fallback is gone) and refuses any target outside an agent-opened window (not_owned). There is no agent-passable override; refusals return a teaching _hint, and each connection's first response carries a one-time _safety banner. Do not attempt to work around a refusal - including "restorative" moves like history.back() on the user's tab. If you damaged their state, STOP and tell them; more uninvited commands make it worse.

Map identities BEFORE driving. nbrowser_profiles first, every time: pick the browser:email profile matching the identity the task needs (a task about [email protected] → that profile, never "the browser that happens to be active" - Chrome and Edge each hold different accounts).

The only thing you may do to one of the user's existing windows is desktop_bring_to_front to surface it for them - and only when they explicitly ask to see it.

⛔ Background by default - never foreground unless asked

The user is almost always doing something else on their computer - reading, typing, on a call, in another app. Your work has to DEFAULT to the background, especially when you open new browser windows: a window that jumps in front of them interrupts what they were doing, and their next keystrokes land in whatever control you just surfaced. Opening in the background is therefore the default posture, not a nicety; foregrounding is opt-in and happens ONLY when the user explicitly asks to watch. The verbs are built to stay out of their way: nbrowser_open_window opens unfocused, nbrowser_open_tab opens active:false, nbrowser_switch_window does not raise. Leave those defaults alone. You can fully see and drive a window without foregrounding it: nbrowser_screenshot (CDP) and desktop_screenshot_window (WGC) both capture a background/occluded window, and WGC records one too. So there is never a reason to pull a window forward. Foregrounding is the user's choice (desktop_bring_to_front), made by them, only when they want to watch.

This is not just etiquette - foregrounding corrupts your own work. When you steal focus while the user is typing, their next keystrokes land in whatever control you just focused (a search box, a text field), silently changing your state and derailing the task (real case: a foregrounded chrome://extensions search turned into Adoms mid-flow and the target extension vanished). Note this applies to SendInput verbs too - desktop_type / desktop_press_key / desktop_click foreground the target before sending. Prefer background UIA (desktop_ui_click Invoke, desktop_ui_set SetValue); if a step genuinely needs foreground keyboard input, caption the user to pause, do the one action, and return focus immediately.

⚠️ Foreground can happen even when you NEVER call bring_to_front. Opening a new window, and especially desktop_navigate (its omnibox SetValue + posted-Enter commit can surface/activate the window to land the Enter), may bring a browser window to the FRONT despite focused:false. The rule is about the outcome, not the verb: after any open or navigate, check desktop_list_windows (a window at z:0 is topmost = you surfaced it) and treat a window that jumped forward as a disruption. Minimize opens and navigations, drive chrome:// pages only when the user is not mid-task, and never repeatedly surface a window. (Observed live: a desktop_navigate to chrome://extensions left the window at z:0 even though no bring_to_front was called and the window was opened focused:false.)

The bridge now ENFORCES all of this in code (ext >= 0.0.22) - you cannot shove a window in front of the user even by accident. open_window/open_tab open in the background regardless of a stray focused:true/active:true (those legacy flags are IGNORED); switch_window/focus never raise; and after every open/navigate/switch/state change the bridge checks the z-order - if a driven window landed topmost, it is auto-sent back to the background (focus returned to the user's previous window, or the window minimized if the user was in another app) and the response carries a _warning + focusAutoReverted:true. Each auto-correction is logged as a focus_violation event (nbrowser_events) so the user can see when a thread tried to steal focus.

Foregrounding takes real work now (v0.1.0+), because it STEALS the user's keyboard focus. Users automating the browser almost always have 5 other things in the foreground; a raised window captures their keystrokes (real incident: it typed a user's chat message into a username field and dismissed Chrome's password dropdown, twice). So a bare userRequestedForeground:true is no longer enough - without a foregroundReason the window stays background. To genuinely foreground (some auth pages / browser features truly need it):

  1. Warn the user first with an AD notify: adom-desktop notify_user {message:"Adom is about to bring a browser window forward for <reason>"}.
  2. Then pass userRequestedForeground:true + foregroundReason:"<why>".
  3. Do the ONE thing that needed the foreground, then return to the background so you stop capturing focus. Every justified raise is logged as a foreground_raised event. And never fall back to an OS-level launch (launch_process / Start-Process / msedge/chrome --new-window) or desktop_bring_to_front to surface a window - that is the exact rude behavior the enforcement exists to stop; if a background open is ever blocked, hand the user a shareable link instead.

The "debugging this browser" bar is expected - and now steadier

CDP verbs (click, type, screenshot, eval, ...) attach chrome.debugger, which makes Chrome show the yellow "Adom for Chrome & Edge" started debugging this browser bar. It is a Chrome security guarantee and cannot be hidden on consumer Chrome (Adom-managed machines launch with --silent-debugger-extension-api, which removes it). Two things you should know:

  • It slides in/out and REFLOWS the page - each transition shifts content up/down, which can move where a click lands. As of v0.1.0 the idle-detach window is long (~45s) so the bar stays STEADY through a drive session instead of flickering between verbs; it still clears once you genuinely stop (not left up forever). Do a burst of driving, then let it settle - don't attach/detach rapidly.
  • It only appears on a window you TARGETED. CDP verbs no longer fall back to the user's active tab - nbrowser_screenshot/nbrowser_eval/nbrowser_errors need a sessionId/tabId, so the bar can never land on the user's own tab. If you only need to read/navigate your own window, prefer the native verbs (list/navigate - no bar).

Credentials and auto-login are gated - and you never see secrets

  • nbrowser_credentials is a read-only query of which vendors the user has a saved login for. It never returns passwords; Chrome autofills those when you focus the field.
  • Auto-login is opt-in per call (confirm:true + an explicit host:) so it can never fire for an arbitrary site. The password is decrypted on the laptop and injected by the extension - the agent never sees it.
  • Treat everything you read from the user's logged-in sessions (fetch_url, page content, eval results) as sensitive. Do not exfiltrate it; use it for the task at hand.

Respect the user's blocklist

nbrowser_profile_block marks a profile hands-off; the bridge refuses to drive it until nbrowser_profile_unblock. That is the user telling you no. Never work around it.

Recording: WGC first (not the extension's picker)

To record a browser window, use the core adom-desktop verb desktop_record_window_start (WGC), or nbrowser_record_start {method:"wgc"} which delegates to it: background, high-fps, no picker, no banner, captures occluded, no focus theft. The extension's own getdisplaymedia/tabcapture path is a refused-by-default fallback (Chrome forces a manual share picker). Full detail: native-browser-recording.

The badge is the user's trust tell - never fake it, never blank it

While you drive, the bridge automatically paints the window's taskbar button with a composite badge: the profile's own circle avatar + the Adom favicon in its corner. That is how the user knows, at a glance, that an AI is in that browser and which profile it is. Three rules:

  • Don't manage it. It applies at open_window, follows your drive verbs, and idle-expires (~3 min of silence) back to the plain native avatar. A badge that lingers with nothing driving is a bug, not a feature - never re-apply it to a window you aren't actively working.
  • Never blank the overlay slot. Chrome/Edge keep the profile avatar there; overlay:{badge:"none"} is safe (the bridge RESTORES the native avatar), but never try to clear the slot by other means.
  • The orange flash is the "come look" cue and the user can silence it (flash_mode:"off"). Errors, human-gates (captcha/2FA/login), and fresh spawned windows auto-flash in quiet mode; flash manually at task-done. Respect off absolutely. The bounded activity trail (nbrowser_activity) records who drove what and when - if the user asks "why is my browser badged?", query it instead of guessing.

Every verb tells you how to behave - read the hints

Every nbrowser_* response carries a _hint (what you got, what to do next, the trap to avoid), and nbrowser_describe lists each verb's hint / related / pitfalls. When unsure, call nbrowser_describe once and read the pitfalls - they encode these safety rules per verb.

The one-line version

Open your own window, work in the background, leave the user's tabs and focus untouched, keep their secrets secret, honor the blocklist, and record with WGC. If a move would change something the user did not ask you to change, don't.