---
title: My Real Chrome — Full (RETIRED — see My Real Chrome v2)
---

# My Real Chrome — Full (RETIRED — see My Real Chrome v2)

## What it is

My Real Chrome lets an Omniscio agent drive **your real, already-logged-in Chrome** — the everyday Chrome with your Google/GitHub/etc. sessions already signed in. That is the one thing the built-in Native Browser can't reach: Chrome 136+ blocks `--remote-debugging-port`/`-pipe` on the **default profile**, so a normal CDP connection to your real Chrome is impossible.

The bridge works around that with a small **companion MV3 Chrome extension** (in the repo under `real-chrome-extension/`). You load the extension into your real Chrome; it attaches Chrome's `chrome.debugger` API inside the browser and relays the DevTools Protocol back to Omniscio over a **loopback, token-secured WebSocket** the app hosts on `127.0.0.1`. Omniscio then drives it with the exact same navigate / read / click / type / screenshot vocabulary the Native Browser uses — the bridge reuses that controller unchanged; the extension just supplies the browser-level `Target.*` commands over `chrome.debugger` + `chrome.tabs`.

> **RETIRED (owner decision 2026-09-15).** Superseded by
> **[My Real Chrome v2](real-chrome-bridge-v2.md)** ("My Real Chrome — Lite"), which drives your real
> signed-in Chrome with `chrome.scripting` + native messaging and **can be approved in the Chrome Web
> Store**. This page is kept for reference.
>
> **Why it went:** this build needs the high-risk `debugger` permission, which the Chrome Web Store
> will not approve — it could only ever be loaded by hand in developer mode. It stays the more
> capable of the two (real/trusted clicks, file uploads, full-page screenshots), so its code and
> routes are kept in the tree, but the feature is now hidden for everyone: **there is no Settings
> toggle any more**, and `/real-chrome/*` answers **404**. Re-reveal it for testing by launching
> Omniscio with `AMC_SHOW_REAL_CHROME_BRIDGE=1`.
>
> **"My Real Chrome"** is the user-facing name; the code namespace is `real-chrome-bridge`.

## Where to find it

You will not find this in the product any more. It is a retired build, kept on this page for reference only: the switch that used to turn it on has been removed from Settings, and agents get no answer from it at all.

The version that ships is the lite build, described at [My Real Chrome v2](real-chrome-bridge-v2.md), which drives your real signed-in Chrome with a permission set the Chrome Web Store will approve.

## How it behaves

### What it is NOT

- **Not your existing tabs — by default.** The extension attaches ONLY to tabs it opened itself (the fresh-tab guardrail). An agent drives fresh tabs it created — never your open banking, email, or work tabs. The one exception is the explicit, default-off **"Let agents drive any Chrome tab"** opt-in (see § Drive any tab), which you turn on deliberately and can turn off at any time. **It is NOT needed for signing Claude accounts back in** — that has its own built-in feature which drives only tabs it opened; see [account-relogin.md](account-relogin.md).
- **Not on the network.** The bridge WebSocket binds `127.0.0.1` only, never `0.0.0.0`. It is unreachable from other machines by construction.
- **Not reachable from your phone.** Driving your real Chrome is deliberately desktop-only — exposed over the local CLI control server, never as an MCP tool, and its status channel is on the mobile/web bridge's blocklist.
- **Not a replacement for the Native Browser.** It is a SIBLING that reuses the same plumbing; enabling one never affects the other.

### Setup

1. **Enable the feature** — Settings → Features → "My Real Chrome" (or `AMC_SHOW_REAL_CHROME_BRIDGE=1`). The bridge starts listening on a fixed loopback port (default `127.0.0.1:19733`, overridable in dev via `AMC_REAL_CHROME_BRIDGE_PORT`).
2. **Get the pairing token** — a pairing panel appears under the toggle showing the loopback port and a **masked pairing token** (reveal + copy). The token is generated once and persisted (encrypted at rest), so you pair the extension only once and it survives restarts.
3. **Load the companion extension** — load `real-chrome-extension/` as an unpacked extension in your real Chrome (chrome://extensions → Developer mode → Load unpacked), then paste the pairing token into it. Once it presents a valid token, it pairs and "Connected" shows in the panel.

### How an agent uses it (CLI routes)

While the feature is on AND an extension is paired, an agent drives the bridge through the local CLI control server. Every route is feature-gated (404 when the flag is off), bearer-authenticated, and fast-fails (409) with a plain message when no extension is connected. The surface is now full — 30 routes matching the agent-browser / Playwright MCP vocabulary:

**Perceive**

| Route                             | Does                                                                                                        |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `GET /real-chrome/status`         | Is an extension paired? bound port + pairing token; connection diagnostics; drivePopups + driveAnyTab state |
| `POST /real-chrome/read-page`     | A11y snapshot with element refs `{ tabId }`                                                                 |
| `POST /real-chrome/get-page-text` | The page's visible text (main-frame innerText) — read/extract content `{ tabId }`                           |
| `POST /real-chrome/screenshot`    | Screenshot a tab `{ tabId }`                                                                                |
| `GET /real-chrome/tabs`           | List open tabs; ALL windows (each with `windowId`) under the driveAnyTab opt-in                             |

**Navigate**

| Route                             | Does                                                                                   |
| --------------------------------- | -------------------------------------------------------------------------------------- |
| `POST /real-chrome/navigate`      | Navigate a tab `{ tabId, url }`                                                        |
| `POST /real-chrome/reload`        | Reload a tab `{ tabId }` (refs go stale — re-read after)                               |
| `POST /real-chrome/foreground`    | Bring a tab to the front + raise its window — clears covered AND minimized `{ tabId }` |
| `POST /real-chrome/navigate-back` | Browser back `{ tabId }`                                                               |
| `POST /real-chrome/wait-for`      | Wait for text / ref / ms `{ tabId, text?, ref?, ms?, timeoutMs? }`                     |

**Interact**

| Route                             | Does                                                                                |
| --------------------------------- | ----------------------------------------------------------------------------------- |
| `POST /real-chrome/click`         | Click an element `{ tabId, ref }`                                                   |
| `POST /real-chrome/click-at`      | Trusted click at raw viewport coords `{ tabId, x, y }` — screenshot→click, no `ref` |
| `POST /real-chrome/hover-at`      | Trusted hover at raw viewport coords `{ tabId, x, y }`                              |
| `POST /real-chrome/type-at`       | Click at coords to focus, then type `{ tabId, x, y, text }`                         |
| `POST /real-chrome/type`          | Type text into an element `{ tabId, ref, text }`                                    |
| `POST /real-chrome/press-key`     | Send a key event `{ tabId, key }`                                                   |
| `POST /real-chrome/hover`         | Hover over an element `{ tabId, ref }`                                              |
| `POST /real-chrome/select-option` | Pick from a `<select>` `{ tabId, ref, values }`                                     |
| `POST /real-chrome/fill-form`     | Fill multiple fields in one call `{ tabId, fields }`                                |
| `POST /real-chrome/drag`          | Drag from one ref to another `{ tabId, fromRef, toRef }`                            |
| `POST /real-chrome/upload-file`   | Set files on a file input `{ tabId, ref, paths }` _(Tier-3)_                        |

**Control**

| Route                          | Does                                                                                                     |
| ------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `POST /real-chrome/open-tab`   | Open a fresh tab `{ url? }`                                                                              |
| `POST /real-chrome/attach-tab` | Adopt a PRE-EXISTING tab so it's drivable `{ tabId }` _(drive-any-tab; listing no longer auto-attaches)_ |
| `POST /real-chrome/detach-tab` | Release a tab WITHOUT closing it `{ tabId }` _(let go of a user tab)_                                    |
| `POST /real-chrome/close-tab`  | Close a tab `{ tabId }` _(refuses a foreign tab — use detach-tab)_                                       |
| `POST /real-chrome/resize`     | Set viewport size `{ tabId, width, height }`                                                             |
| `POST /real-chrome/evaluate`   | Run JS in a tab `{ tabId, expression }`                                                                  |

**Observe (Tier 3)**

| Route                             | Does                                                        |
| --------------------------------- | ----------------------------------------------------------- |
| `POST /real-chrome/console`       | Buffered console messages `{ tabId }`                       |
| `POST /real-chrome/network`       | Buffered network requests `{ tabId }`                       |
| `POST /real-chrome/handle-dialog` | Accept/dismiss a JS dialog `{ tabId, accept, promptText? }` |

> **Tier-3 live-verification note:** console / network / handle-dialog / upload-file are built and unit-tested app-side, and their live `chrome.debugger`-facade behavior — event delivery timing, dialog-handling, and the file-chooser bypass on upload-file — is verified live by `npm run verify:real-chrome` (see § Live verification below).

The engine starts and stops with the flag (a gated startup task + paired shutdown task, reconciled live the moment you flip the toggle), so nothing listens unless you've turned it on.

### Errors & rate limits

- **A browser-side failure shows its real reason, not "Internal server error."** When Chrome rejects a command, a CDP command times out, or the extension disconnects, the route returns the browser's ACTUAL message with a `SERVICE_UNAVAILABLE` code (HTTP 503) — e.g. `Execution context was destroyed (CDP code -32000)` — instead of an opaque `INTERNAL`. That is diagnostic AND often recoverable: on a "context destroyed" / stale-ref message, call `read-page` again to refresh the element refs, then retry. (These surface as host-side warnings, never crash reports.)
- **A bad `evaluate` script is your mistake, not a crash.** When the JavaScript you pass to `POST /real-chrome/evaluate` throws — a syntax error (an unterminated regex, a stray bracket), a `ReferenceError`, or any thrown value — the route returns the script's actual error with an `UNPROCESSABLE` code (HTTP **422**) as `evaluate failed: <the error>`, logged as a host-side warning and **never** filed as a crash report. Fix the expression and retry; a broken script no longer looks like an Omniscio fault.
- **A mouse op on a tab Chrome is not rendering REFUSES instead of lying — and `foreground` is the fix that actually works.** A tab that is not the visible foreground tab of a visible window has no compositor surface, so Chrome silently DISCARDS every trusted mouse event and screenshot: `click` / `click-at` / `hover` / `hover-at` / `type-at` / `drag` used to return `ok:true, changed:false` having delivered nothing, and `screenshot` returned an empty string. They now throw an actionable **422** naming the fix: call `POST /real-chrome/foreground` with the `tabId`, then retry. Two ways in: driven tabs open `active:false` by design, and even an ACTIVE tab goes unrendered when its Chrome window is **minimized or fully covered by another window** (a maximized Omniscio window will do it). **As of 2026-09-13 `foreground` clears both cases** — the extension's `chrome.windows.update{focused}` is necessary but **not sufficient**, because it bottoms out in Chrome calling `SetForegroundWindow` on its own window, and Windows vetoes that for a process that is not already the foreground one — silently, returning success. So the bridge also raises the OS window itself (TOPMOST → NOTOPMOST, restoring it first if it was minimized). **Never tell the user to un-cover or un-minimize Chrome by hand.** If it still refuses after a foreground, the window is on another virtual desktop, on a display that is off, or another app has pinned itself always-on-top. Reads (`read-page`, `get-page-text`, `evaluate`, `console`, `network`), `navigate` and `press-key` never needed this.
- **A password-manager (LastPass) or other extension on the page can BLOCK driving — and the bridge now tells you exactly how to fix it.** When another Chrome extension injects its own `chrome-extension://` frame into a page (password managers do this on login pages — usually the moment you focus the email field), Chrome refuses to let Omniscio's companion extension operate on that tab **at all** — not just the foreign frame: the whole tab's debugging session is poisoned, so even a plain `screenshot` (which never looks at other frames) fails with `chrome-extension:// URL of different extension (CDP code -32000)`. This is a hard Chrome security boundary — one extension's debugger cannot operate on a tab that contains a _different_ extension's frame — and **no scoping or re-attach works around it** (Anthropic's own "Claude in Chrome" hits the identical wall and closed it as not-planned). So instead of a cryptic -32000, the bridge now returns ONE clear, actionable error on any blocked command (`screenshot` / `click` / `evaluate` / `read-page`): _"A frame from another Chrome extension (often a password manager like LastPass) is on this page… turn it off for this site."_ **The real fix is on your side:** disable or pause that extension for the site (in LastPass: its in-field icon → More options → **Turn off LastPass for this site**) or drive in a Chrome profile without it. Two things still work regardless, because they don't use the blocked debugger: **`navigate`** (a tab-level `chrome.tabs.update`) and **`foreground`** (bring the tab to the front) — so an agent can still open a magic link and surface the tab for a human to finish the sign-in even with the extension on.
- **The pairing panel reports the last safe bridge diagnostic.** `GET /real-chrome/status` includes `lastConnectedAt`, `lastDisconnectedAt`, and `lastError`; when the bridge is disconnected, the panel shows the last human-readable reason, such as a busy local port, a socket error, or a missed heartbeat. Raw system errors stay in the app log.
- **The link self-heals — a rare "no browser connected" is transient, not permanent.** The companion extension lives in a Chrome MV3 service worker that Chrome sleeps when idle, so the bridge keeps it warm (the app pings it every ~15s, which resets Chrome's idle timer) and, if the worker is ever killed anyway, a `chrome.alarms` tick wakes it and re-dials within ~60s with no clicking. A drive request that lands during a brief reconnect is held for a bounded moment (~2.5s) rather than failing instantly. So a **409 "no browser connected"** that appears right after an idle gap is usually gone a second later — wait a beat and retry once before telling the user to check pairing. (After an extension UPDATE, the user must reload it once in `chrome://extensions` for a new build to take effect; pairing persists.)
- **An idle background tab attaches instead of timing out.** When you keep many tabs open, Chrome DISCARDS idle ones to reclaim memory — an unloaded tab has no live debug target, so attaching it used to HANG until it timed out (it looked like "attach fails on claude.ai", but it was really any idle tab: an idle sign-in tab, an OAuth localhost callback, any background page). The bridge now **WAKES a discarded/frozen tab first** (a quiet background reload — its content was already dropped, so nothing extra is lost), then attaches with a brief retry, so `attach-tab` on a tab you left idle just works. An active tab is untouched.
- **A `wait-for` timeout is a clean result, not an error.** If the `text` / `ref` you're waiting for never appears within `timeoutMs` (default 5s, up to 60s), the route answers **HTTP 200** with `{ found: false, timedOut: true }` — a normal _"not there (yet)"_ signal to branch on (the page took another path, the element is named differently, the consent screen never rendered). When it does appear it answers `{ found: true }`. So the agent checks `found` rather than catching an error. (A bad/closed `tabId` is still a real error — a `NotFoundError` / 4xx — only the deadline itself is a result.) The shared click is also hardened: it scrolls the target into view and dispatches a trusted hover before the trusted press, so a click no longer "returns ok" without firing; and the page-reading tools retry once on a transient "execution context destroyed" that a login redirect commonly causes.
- **A live tab is never "Unknown", and a click reports whether it did something.** Every drive call self-heals its session on the fly, so a tab still open in Chrome keeps driving across an idle / service-worker cycle instead of turning into "Unknown tab" (only a tab the user genuinely closed returns a clean "closed"). And `click` / `type` (and `click-at` / `type-at`) return a `changed` boolean — an honest "did the page visibly react?" signal (navigation, focus, content, or an input's value moved). If `changed` is `false`, the action fired but nothing observable happened — re-read the page and reconsider rather than assuming it worked; never treat a bare success as proof.
- **A mid-action network blip never leaves the mouse or a key stuck down.** A click, drag, or keypress dispatches a press then a release as two separate steps; if the connection hiccups exactly between them the button (or key) used to stay logically held — turning every later click into a drag-select and wedging typing for the life of the tab. The bridge now always sends a best-effort release on that failure (a broken drag falls back to a plain click at the start point), so the action still reports its real error but the tab stays usable — you can just re-read the page and retry.
- **No `ref` for something you can SEE? Click by coordinates.** When `read-page` has no `ref` for a target you can see in a `screenshot` (an unlabeled control, a canvas hit-area, a menu item the a11y tree missed), use `POST /real-chrome/click-at` / `hover-at` / `type-at` with raw viewport `{ x, y }` (CSS pixels) — a trusted click/hover/type at that exact point. And `read-page` now keeps unlabeled interactive items (icon-only menu items, separators, unnamed menus), so an open menu no longer reads as zero items.
- **Read-only captures don't spend the write budget.** `read-page`, `screenshot`, `console`, and `network` are reads — they use a generous per-token read budget, so a burst of screenshots while automating a page never trips the rate limit.
- **Acting routes have their OWN per-AI budget, not a shared one.** The **acting** routes (`click`, `type`, `evaluate`, `navigate`, open/close tab, …) each draw from a **30 actions/minute per-AI-source budget** — your session id gets its own cap, so multiple AIs driving tabs in parallel don't starve each other. A single AI is still bounded at 30/min (one action every ~2s sustained).
- **Filling a form? Batch with `fill-form`.** Driving a multi-field form field-by-field with separate `type` + `click` calls burns the per-AI drive budget fast. `POST /real-chrome/fill-form` fills every field in ONE call (one action), so a whole form is `fill-form` (1) + a `click` to submit (1) — comfortably under the budget.

### Handling logins — drive, hand off, resume (recommended)

The clean, robust way to get an agent into a site that needs a login is **not** to have the agent type your password or click through OAuth — it's to let the agent drive up to the login, hand control to **you** for the sign-in itself, then take back over on the now-authenticated page:

1. **Agent drives to the login** — opens a fresh tab, navigates to the site, and (if needed) clicks its "Sign in" / "Continue with Google" button.
2. **Agent hands off** — it calls `POST /real-chrome/foreground` with the `tabId` (which raises the Chrome window itself, so the tab is really in front even if Chrome was behind another app or minimized) and tells you: _"complete the sign-in in that tab, then tell me when you're done."_ The tab is yours; Chrome's "being debugged" banner does not stop you clicking or typing in it.
3. **You sign in** — password, OAuth consent, 2FA, or passkey — a **real** human interaction, which clears the anti-automation defences that block synthetic clicks.
4. **You say "done"** — the agent reads the tab (`POST /real-chrome/read-page` or `GET /real-chrome/tabs`) to confirm it landed on the authenticated app, and continues the task.

**Why this is the default over automating the login:** major identity providers (Google, Microsoft, GitHub …) actively **block completing OAuth consent in an automation/debugger-driven browser** — a synthetic click on the final "Continue" returns success but the flow does not advance. A real human click does. Handing the sign-in to you sidesteps that entirely, works for **any** auth method (password / OAuth / 2FA / passkey), and never routes your credentials through the agent.

**What makes "take back over" work:** a sign-in is a cross-origin **redirect chain** (`site → accounts.google.com → site`), and each cross-process hop fires `chrome.debugger.onDetach`. The extension's **re-attach** behaviour (see the contract's § Redirect-flow logins) keeps the agent's session alive across every hop, so when you hand the tab back it is still owned + drivable and the agent resumes on the logged-in page — no re-pairing. Without re-attach the agent would lose the tab at the first redirect and could not resume. The re-attach also **re-enables the tab's CDP domains**, so the page-reading tools (`read-page` / `screenshot` / `evaluate`) keep working after the hop — not just input. This same detach → re-attach happens on any heavy page whose sub-frames navigate (a cookie CMP, cross-origin iframes), which is why the re-enable matters well beyond logins.

> **Automated popup-driving** (next section) is a **narrow, opt-in alternative** for pop-up sign-in flows an agent must complete unattended. For anything interactive — especially a hardened OAuth consent — the hand-off pattern above is the recommended path.

### Popup-driving permission

By default the extension **cannot adopt tabs opened by `window.open`** — so if a site opens a sign-in or OAuth popup, the agent can't reach it. Enabling **"Allow driving sign-in popups"** in the pairing panel (the `realChromeBridgeDrivePopupsEnabled` setting) changes that:

- When on, the extension's `chrome.tabs.onCreated` listener adds a new tab to its `ownedTabs` ONLY when the opener was itself an owned tab — the typical OAuth / sign-in popup flow.
- The agent can then drive that popup (navigate it, read the page, fill credentials, click "Allow") and complete a Google login or any pop-up-based auth entirely autonomously.
- **Your existing tabs are never touchable regardless of this setting.** The fresh-tab guardrail (I4) still governs `Target.attachToTarget`; only popups whose opener was already an agent-owned tab are ever adopted.
- On disconnect `drivePopups` resets to `false`, so the next connection starts safe until the config frame arrives.

### Drive any tab (opt-in) — reach a tab the agent didn't open

By default the agent can only reach tabs it opened. **"Let agents drive any Chrome tab"** in the pairing panel (the `realChromeBridgeDriveAnyTabEnabled` setting, default OFF) is the one deliberate exception — turn it on and:

- `GET /real-chrome/tabs` lists **every** window and tab, each with its `windowId`, so the agent can find a specific pre-existing tab (e.g. a Claude sign-in tab you opened).
- `POST /real-chrome/attach-tab { tabId }` adopts that tab so the agent can drive it. Listing no longer auto-attaches — a tab the agent opened is drivable immediately, but one of your own tabs is adopted only deliberately (the extension refuses a non-owned attach unless the opt-in is on).
- `POST /real-chrome/detach-tab { tabId }` lets the agent **let go** of one of your tabs without closing it. (The agent can never CLOSE a tab it didn't open — `close-tab` refuses a foreign tab.)
- Every tab the agent drives — including your own — shows the loud **"Omniscio AI is controlling this tab"** banner, and its browser-tab **title is prefixed with `● Omniscio · `** so you can spot a driven tab in the tab strip even when it is a background tab (driven tabs open in the background, where the in-page banner isn't visible until you switch to them) — so it is never invisible.
- This **widens the safety boundary** (an agent could drive your email/banking tab), which is exactly why it is default-off, persistent, full-trust-token-only, and revoked the instant you turn it off (adopted tabs are released, never closed).

### Watch it live + take over (the docked Browser pane)

When "My Real Chrome" is on, a **Browser** toggle appears in a session's header **only while that
session's agent is actually driving a Chrome tab** — never on every session. It opens a docked pane
that streams the tabs **that session** is driving in your real Chrome **live** — one full-motion
focused tile that fills the pane (aspect-preserving) plus lightweight thumbnails of the rest, capped
so many streams never bog your machine. Click a thumbnail to focus it.

- **Live, hands-free** — the tab list updates itself: a tab the agent opens or closes appears or
  disappears on its own (the per-session activity signal now re-fires on any tab gain/loss, and a
  light 5-second poll keeps titles/URLs fresh across navigations). The manual Refresh button remains
  as a fallback.
- **Context toolbar** — the focused tab's title and URL sit at the top beside a tab count, a Refresh
  icon, and **Open in Chrome**, which focuses that real tab in your Chrome window (CDP
  `Page.bringToFront` over the same relay — nothing new for the extension to support).
- **Per-session** — the toggle is scoped to the session whose agent is driving: it appears only where a
  session has a live tab (and stays visible while its pane is open, so you can close it), and the pane
  shows only that session's tabs — never another session's, and never a personal tab no agent touched.
  The docked pane is one slot bound to one session; clicking the toggle in a different active session
  switches it there. Under the hood the bridge records which session drove each tab (from the source
  session id on the drive routes; pruned when a tab closes) and pushes a desktop-only "which sessions
  are active" signal that is blocked from the phone/web bridge — see the contract's I13.
- **Take over** — click **Take over** to drive the focused tab yourself with full fidelity: clicks
  land exactly where you point (letterbox-aware mapping), and scrolling, dragging, right-click,
  double-click, keyboard shortcuts (Ctrl/Shift/Alt/Meta combos), and pasting all work — paste rides
  CDP `Input.insertText`, since raw key events can't perform a clipboard paste. The pane makes the
  mode unmistakable (an accent ring + a "You're driving" banner), the keyboard works immediately
  (no second click), and **Esc** — or the **Release** button — hands control back to the agent. One
  trade-off to know: because Esc exits take-over, you can't send Esc itself to the page from the pane.
  You can only take over the tab you're currently watching.
- **What's on screen** — every streaming tile shows a small badge; a tab the agent _didn't_ open
  (a personal tab, only reachable with "Drive any tab" on) is flagged in amber so you always know a
  personal tab is being shown.
- **Privacy** — the live picture is **desktop-only**. It is never sent to your phone or the web
  bridge and never included in a published Share, even though it streams your real, signed-in Chrome.
  It only runs while the pane is open and stops the moment you close it. The "Open in Chrome" and
  paste commands are blocked from the phone/web bridge exactly like the stream itself.

### Re-logging in a Claude account (the flow this was built for)

When a managed Claude account logs out (health = `auth` in the account manager), re-authing it involves a **magic link**, and there is one hard constraint to design around: **an Anthropic magic link is bound to the exact browser session that requested it** — open it anywhere else and you get _"We were unable to verify you with this link."_ So the link must be opened in the **same tab** where the email was entered.

**Which browser does Omniscio's "Log In" capture actually read from?** This is the key fact: the account "Log In" (the `addLoginAccount` store action → `ACCOUNT_ADD_LOGIN` IPC → `authService.login()`, now also exposed as `POST /accounts/login-capture`) is a **PKCE OAuth flow, NOT a cookie/session read**. It calls `shell.openExternal(authUrl)`, which opens your **default system browser** to Anthropic's sign-in, and a tiny loopback server (`http://localhost:<random-port>/callback`) waits for the OAuth redirect. The account captured is **whoever completes that flow** — it is account-agnostic; Omniscio never reads cookies from any window, it receives an OAuth authorization code at that localhost callback. **Therefore the magic link must be opened in the default-system-browser tab that `shell.openExternal` spawned** — and for the agent to reach that tab (which the bridge did not open), the **drive-any-tab opt-in must be on**. That is the whole reason this feature exists.

**Constraint:** this works only when your **default browser is the same Chrome the companion extension is installed in** (same profile). If your default browser is something else — or a different Chrome profile — `shell.openExternal` opens a browser the bridge can't see, and the agent can't reach the login tab.

> **There is now a built-in feature that does all of this for you, and it does NOT need
> drive-any-tab.** Settings → Accounts → **Sign them back in** drives the sign-in in tabs it
> opens itself, reads the magic link from the connected Google account, types the code, and
> captures each account — asserting afterwards that the captured account is the one it was
> fixing. See [account-relogin.md](account-relogin.md). **Prefer it.** The manual runbook below
> is the fallback for the cases it does not cover (an account Omniscio has never seen, or a
> box with no Google grant and no bridge), and it is the reason the drive-any-tab escape hatch
> exists at all.

**The re-login runbook (per account) — the MANUAL fallback:**

1. **Start the capture** — an agent (or the user) triggers the "Log In" capture: `POST /accounts/login-capture` (optionally `{ "loginHint": "<the account email>" }`). It opens your default Chrome to Anthropic's sign-in and returns immediately (`{ started: true }`); the flow then waits in the background for the OAuth redirect.
2. **Enter the email + pass the CAPTCHA** — a **human** step, on Claude's sign-in page in that tab.
3. **Fetch the magic link** — Anthropic emails a one-time link to the Gmail the account forwards to (**often in Spam**). An agent reads it out of Gmail.
4. **Open the link in the SAME tab** — with drive-any-tab on, the agent finds the sign-in tab (`GET /real-chrome/tabs`, match the claude.com sign-in URL), attaches to it (`POST /real-chrome/attach-tab`), and navigates it to the magic link (`POST /real-chrome/navigate`). Opening the link signs that session in and the OAuth redirect completes → the loopback callback fires → **Omniscio captures the account automatically**.
5. **Confirm + move on** — poll `GET /accounts/login-capture/status` (and the account's health) until the account appears / `inProgress` is false; confirm health = ok; then log out and repeat for the next account. `POST /accounts/login-capture/reopen` re-opens the OAuth tab if it was closed mid-flight; `POST /accounts/login-capture/cancel` abandons a stuck attempt.

> **Safer alternative that avoids drive-any-tab — now shipped as a feature:** if the agent OPENS the sign-in URL in a tab **it owns** (via `open-tab`), it can drive that tab — including opening the magic link there — with **no** boundary-widening at all. That is exactly what the built-in automated re-login does ([account-relogin.md](account-relogin.md)), so reach for it first. The drive-any-tab path above remains only for the case where the login tab was opened by Omniscio's own `shell.openExternal` (an unowned tab) or by you by hand.

### Security — the linchpin (loopback + token + fresh-tab)

Three layers make it safe to point an agent at your real, signed-in browser:

- **LOOPBACK, never the network** — the WebSocket binds `127.0.0.1` only. A DNS-rebind defence rejects any `Host` header that isn't `127.0.0.1` / `localhost` / `[::1]` on the bound port, and a `chrome-extension://` Origin is required as defence-in-depth.
- **TOKEN at the upgrade** — every connection must present a 256-bit pairing token (offered as a WebSocket subprotocol, constant-time compared) or the WebSocket upgrade never completes. Only one authenticated extension is connected at a time (a new pairing replaces the old).
- **FRESH-TAB guardrail** — the extension refuses to attach to any tab it did not open, and lists only its own tabs. On disconnect/stop it detaches `chrome.debugger` from every driven tab (which also clears Chrome's "being debugged" banner).

### Live verification

Run **`npm run verify:real-chrome`** to exercise the whole bridge live against a real, local Google Chrome. It boots the engine on an ephemeral loopback port, loads the companion extension into a **throwaway Chrome profile** (zero real logins) via CDP `Extensions.loadUnpacked` (Chrome 137+ removed the `--load-extension` switch), and drives **benign local pages** through every route: open-tab / navigate / read-page / screenshot / click / type / fill-form / select-option / press-key / hover / drag / resize / evaluate / wait-for / navigate-back, the AI-control overlay, the Tier-3 routes (console / network / handle-dialog / upload-file), and the cross-site re-attach that backs the OAuth-redirect fix. It kills only the Chrome it launched and deletes the throwaway profile; the exit code is the number of failed checks. It is a **local dev command** (needs real Chrome + network), never part of the cloud CI gate — an integration test with a fake extension over a real loopback socket stands in for it there. Set `AMC_RCB_VERIFY_VISIBLE=1` to watch the window on-screen.

**What it does NOT cover — by design:** completing a real signed-in **OAuth consent**. Hardened IdPs (Google / Microsoft / GitHub) block a synthetic click on the final consent even in a debugger-driven browser, so that path stays the **drive → hand-off → resume** flow above. NEVER point the verification (or an agent) at your real logged-in data. See `real-chrome-extension/README.md`.

## For agents

### Key code

- `src/main/services/real-chrome-bridge/` — the bridge:
  - `real-chrome-bridge-server.ts` — the loopback WebSocket server; all auth (token / Host / Origin) runs at the upgrade.
  - `real-chrome-auth.ts` — the token / loopback-Host / extension-Origin predicates.
  - `websocket-cdp-transport.ts` — a `CdpTransport` over the socket (the seam the Native Browser's tab controller drives unchanged).
  - `real-chrome-engine.ts` — ties the server to the reused `NativeBrowserTabController`; `tabs()` is non-null only while an extension is paired.
  - `real-chrome-bridge-service.ts` — the singleton lifecycle: start/stop/reconcile on the flag, `status()` diagnostics for the panel.
  - `real-chrome-session-attribution.ts` — the side-band `tabId → chat-session` map (`per-session-browser-toggle-and-pane`) powering the per-session Browser toggle + pane; the service captures it from the drive routes, prunes on close / `Target.targetDestroyed`, and pushes the deduped, web-blocked `real-chrome-bridge:session-activity` signal.
- `src/main/services/cli/cli-server-real-chrome-bridge-routes.ts` — the gated `/real-chrome/*` CLI routes (incl. the driveAnyTab `attach-tab`/`detach-tab` pair, and the `captureTabAttribution` hook that records which session drove each tab).
- `src/main/services/cli/cli-server-accounts-routes.ts` — the `/accounts/login-capture` routes; `performLoginCapture` (the shared OAuth "Log In" capture) is extracted in `src/main/ipc/account/claude-provider.ts`.
- `real-chrome-extension/drive-any-tab.js` + `real-chrome-extension/owned-tabs.js` — the pure `decideAttach` `fresh-tab-guardrail` boundary + the `storage.session` (de)serialization for durable owned-tab tracking across an MV3-worker cycle.
- `src/renderer/src/features/settings/RealChromeBridgePairingPanel.tsx` — the pairing panel (a connection status dot + last disconnect/startup diagnostic + the loopback port + masked token with reveal/copy + the popup-driving toggle + the drive-any-tab opt-in toggle with its safety warning), built from the app's shared settings components.
- `real-chrome-extension/` — the companion MV3 extension (the `Target.*` facade over `chrome.debugger` + `chrome.tabs`); `overlay.js` builds the on-page "Omniscio AI is controlling this tab" banner — painted as an aurora gradient (the user's LIVE theme accent → brand cyan) on both the top strip and the border, fronted by the Omniscio orb logomark (an embedded PNG that hides itself if a strict page blocks it). The renderer reports the accent to main (`real-chrome-bridge:set-accent`), the service forwards it in the config frame, and the banner repaints on a theme change (falls back to the app default). See the contract's `ai-control-indicator` / `the-indicator-starts-from-the-live-accent`.
- `src/renderer/src/stores/theme-store.ts` — reports the live `--accent-rgb` to main (desktop-only, deduped) so the banner can track the theme.
- **Per-session Browser view (`per-session-browser-toggle-and-pane`):** `src/renderer/src/stores/session-browser-dock.ts` (which session's pane is open) + `src/renderer/src/stores/real-chrome-session-activity.ts` (the active-session set, fed by the web-blocked `session-activity` push + a `useRealChromeSessionActivitySync` seed) + `SessionPanel/SessionPanelHeader.tsx` (gates the toggle on the session's activity) + `SessionBrowserPane.tsx` (takes a `sessionId`, requests only that session's tabs).
- `src/shared/unreleased-features.ts` — the `real-chrome-bridge` entry (setting key `realChromeBridgeEnabled`, env `AMC_SHOW_REAL_CHROME_BRIDGE`).

Contract: [`.claude/memory/contracts/real-chrome-bridge-contract.md`](../../.claude/memory/contracts/real-chrome-bridge-contract.md) (the loopback / token / fresh-tab invariants).

## Related

Readers of this page will want [real-chrome-bridge-v2.md](real-chrome-bridge-v2.md), the shipped replacement that drives your real Chrome with a lighter, store-approvable permission set, and [account-relogin.md](account-relogin.md), the built-in flow for signing a Claude account back in that originally motivated this bridge.
