---
name: native-browser-safety
description: "SAFETY rules for any agent driving the Adom Native 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-native-browser."
---
Part of **adom-native-browser**. 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 nbe 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
`pup_open`/pup call or an ah 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 `person@gmail.com` → 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 a Bridge notify: `adom-bridge-cli 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 Bridge 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.

## The user SEES your reason - write `purpose` for a human
Anything you open that the user will actually see (an approved foreground open, a page the browser
grouped into an existing window, `open_tab {active:true}`) puts a **3-second caption at the bottom of
their screen naming your reason** - taken from `foregroundReason`, falling back to your session
`purpose`. Those strings are **user-facing copy**, not log shorthand:
- bad: `"oauth flow"`, `"step 3"`, `"automation"` - reads as an AI that will not say what it is doing
- good: `"signing you in to Autodesk so Fusion can reach your cloud files"`
Say what you are accomplishing FOR THEM, not what the command does. This is the same standard as a
`reason` on any desktop action: written for the human in the moment, not for a log.

## Two badges: know which one you are earning
- **SOLID** Adom mark on the profile avatar = an AI has **live control** of that window.
- **HOLLOW** outline = Adom **opened** the window but cannot drive it; the user is in control.
Both fade to the plain avatar after ~3 min idle, on close, or when the user closes the window. Never
hand-paint or fake a badge - it is the user's only at-a-glance answer to "is something driving my
browser right now?", and a badge that lies is worse than no badge.

## 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.
