git:20261012.906168f to git:20261031.87bbf6a

8 added, 7 removed. Audit A to A.

---
name: interceptor-browser
description: "Drive a signed-in Chrome / Brave / Safari session via the interceptor CLI: open/read pages, click, type, inspect DOM/text/network, automate rich browser editors and scene graphs, capture WebSocket/Beacon/BroadcastChannel traffic, record/replay flows, take VLM-budgeted screenshots, compare pages, and route to specific browser contexts with --context. Use for browser page content, tabs, forms, SPA extraction, request overrides, page communication capture, and deployment checks. Not for native macOS apps, Electron desktop app web contents, OS dialogs, browser chrome, or large scraping."
#os:win32 description: "Drive a signed-in Chrome / Brave / Edge session via the interceptor CLI: open/read pages, click, type, inspect DOM/text/network, automate rich browser editors and scene graphs, capture WebSocket/Beacon/BroadcastChannel traffic, record/replay flows, take VLM-budgeted screenshots, compare pages, and route to specific browser contexts with --context. Use for browser page content, tabs, forms, SPA extraction, request overrides, page communication capture, and deployment checks. Not for native apps, Electron desktop app web contents, OS dialogs, browser chrome, or large scraping."
#os:linux description: "Drive a signed-in Chrome / Brave session via the interceptor CLI: open/read pages, click, type, inspect DOM/text/network, automate rich browser editors and scene graphs, capture WebSocket/Beacon/BroadcastChannel traffic, record/replay flows, take VLM-budgeted screenshots, compare pages, and route to specific browser contexts with --context. Use for browser page content, tabs, forms, SPA extraction, request overrides, page communication capture, and deployment checks. Not for native apps, Electron desktop app web contents, OS dialogs, browser chrome, or large scraping."
metadata:
short-description: Drive a real signed-in Chrome / Brave / Safari session via the interceptor CLI
#os:win32 short-description: Drive a real signed-in Chrome / Brave / Edge session via the interceptor CLI
#os:linux short-description: Drive a real signed-in Chrome / Brave session via the interceptor CLI
---
# Interceptor Browser
Agent-operator skill for the Browser surface of Interceptor. Use the `interceptor` CLI (no prefix) to drive a live Chrome, Brave, or Safari session: pages, network, scene graph, monitor, screenshots. For native macOS apps load `interceptor-macos` instead. <!-- os:darwin -->
Agent-operator skill for the Browser surface of Interceptor. Use the `interceptor` CLI (no prefix) to drive a live Chrome, Brave, or Edge session: pages, network, scene graph, monitor, screenshots. For native apps load `interceptor-desktop` instead. <!-- os:win32 -->
Agent-operator skill for the Browser surface of Interceptor. Use the `interceptor` CLI (no prefix) to drive a live Chrome or Brave session: pages, network, scene graph, monitor, screenshots. For native apps load `interceptor-desktop` instead. <!-- os:linux -->
This installed skill is self-contained. Source checkouts also have `AGENTS.md`, but packaged users may only have the skill directory below `/Library/Application Support/Interceptor/skills`. <!-- os:darwin -->
This installed skill is self-contained. Source checkouts also have `AGENTS.md`, but packaged users may only have the `skills` folder in the Interceptor install folder (`%LOCALAPPDATA%\Programs\Interceptor\skills`). <!-- os:win32 -->
This installed skill is self-contained. Source checkouts also have `AGENTS.md`, but packaged users may only have the skill directory below `~/.local/share/interceptor/skills`. <!-- os:linux -->
## The User Is On This Mac Too <!-- os:darwin -->
## The User Is On This Computer Too <!-- os:win32,linux -->
Assume the person is using this browser and this Mac while you work. Never change the tab showing in a window they are using, never focus a window, and never bring the browser forward to make your own work easier or to test something. Stop at the first rung that works: <!-- os:darwin -->
Assume the person is using this browser and this computer while you work. Never change the tab showing in a window they are using, never focus a window, and never bring the browser forward to make your own work easier or to test something. Stop at the first rung that works: <!-- os:win32,linux -->
1. **Plain background verbs.** `open`, `read`, `act`, `click`, `type`, `screenshot`, `net`, and the rest run on a hidden tab.
2. **`interceptor tab keepalive <id>`** when a page will not render while hidden (it reads visible and runs its animation frames).
- 3. **Your own window** when the page needs a real paint (video playback, a virtualized grid that measures itself) or a trusted click: `interceptor window new [url]` opens a background window and moves your tab group into it. The tab showing there paints like a foreground tab even when the window is fully covered, and `interceptor tab switch <id>` there changes only that window. <!-- os:darwin -->
+ 3. **Your own window** when the page needs a real paint (video playback, a virtualized grid that measures itself): `interceptor window new [url]` opens a background window and moves your tab group into it, and `interceptor window new --tab <id>` does the same around a tab you already have. The tab showing there paints like a foreground tab even when the window is fully covered. `tab switch` changes only that window, but a tab switched in while the window is covered stays hidden: use `window new --tab <id>` for it. <!-- os:darwin -->
3. **Your own window** when the page needs a real paint (video playback, a virtualized grid that measures itself): `interceptor window new [url]` opens a background window and moves your tab group into it, and `interceptor tab switch <id>` there changes only that window. Read the page again to confirm it rendered; a covered window painting in the background is measured on the Mac only. <!-- os:win32,linux -->
- 4. **Trusted input without focus.** `interceptor click <ref> --trusted` (also `type`, `keys`) reaches the active tab of any window that is not minimized on a full install. No `tab switch` in the user's window, no `window focus`. <!-- os:darwin -->
+ 4. **Trusted input without focus.** `interceptor click <ref> --trusted` (also `type`, `keys`) reaches any tab on a full install. A tab that is not showing gets a background window of its own for the input and goes back where it was, and the result says whether the page received it. No `tab switch` in the user's window, no `window focus`. <!-- os:darwin -->
`tab switch` in a window the user is working in and `window focus` are last resorts. Say so first, do one fast batch, and put the previous tab back.
## Core Rules
- Use compound commands (`open`, `websearch`, `read`, `act`, `inspect`) before low-level verbs.
- `websearch "<query>"` searches through the browser's configured default provider in an Interceptor-managed background tab and returns the provider page. It is not Google-specific. `find "<query>"` never navigates: it searches the current page's complete rendered-text snapshot plus accessible elements. Use `find --text-only` for passages and `find --elements-only`/`--role` for controls.
- Browser commands operate inside managed Interceptor tab groups. Do not use `--any-tab` unless the user explicitly authorizes acting outside those groups.
- - Supported agent shells get a soft per-session group automatically, labeled `s-<hash16>`, so bare commands reuse one tab per session and the idle sweeper has a cleanup unit. `INTERCEPTOR_SESSION_ID` is the harness-neutral contract; Interceptor also detects verified Maestro, Claude Code, and Codex session variables. Soft scope falls back to the active managed tab when the session group is empty. `--shared-group` (or empty `INTERCEPTOR_GROUP=`) explicitly uses Interceptor's shared default group; it does not remove managed grouping. **Concurrent lanes often share one host session id**, so give each lane a unique `--group lane-<n>` or `INTERCEPTOR_SESSION_ID`. Explicit `--group <label>` and non-empty `INTERCEPTOR_GROUP` provide hard isolation by default: resolution stays within that group and cross-group targets are rejected unless `--any-tab` is explicitly authorized. `interceptor group list` shows the automatic label.
- - Close your group with `interceptor group close <label>` when the job is done. The extension auto-closes groups after 10 minutes without tab activity by default; metadata polls such as `status` and `group list` do not keep a group alive. The timeout is configurable in the extension popup and is crash safety, not a substitute for cleanup.
+ - Supported agent shells get a soft per-session group automatically, labeled `s-<hash16>`, so bare commands reuse one tab per session and the idle sweeper has a cleanup unit. `INTERCEPTOR_SESSION_ID` is the harness-neutral contract; Interceptor also detects verified Maestro, Claude Code, and Codex session variables. Open your own page first (`interceptor open <url>`). While your session group is empty, input (click, type, keys, eval, navigate, tab close) refuses instead of landing on the tab the user is viewing; a read may answer from that tab with a `warning:` line, or refuse when other agents' groups are open. Pass `--any-tab` only when the user asked you to act on their tab. A browser without tab groups keeps the old fallback. `--shared-group` (or empty `INTERCEPTOR_GROUP=`) explicitly uses Interceptor's shared default group; it does not remove managed grouping. **Concurrent lanes often share one host session id**, so give each lane a unique `--group lane-<n>` or `INTERCEPTOR_SESSION_ID`. Explicit `--group <label>` and non-empty `INTERCEPTOR_GROUP` provide hard isolation by default: resolution stays within that group and cross-group targets are rejected unless `--any-tab` is explicitly authorized. `interceptor group list` shows the automatic label and marks yours `"own": true`. Every other `s-` group belongs to another agent that may still be working: leave it alone, even when asked to clean up; Interceptor closes it after it sits unused. Close another agent's group only when the user names that group.
+ - Close your group with `interceptor group close` (no label closes your own) when the job is done. The extension auto-closes groups after 10 minutes without tab activity by default; metadata polls such as `status` and `group list` do not keep a group alive. The timeout is configurable in the extension popup and is crash safety, not a substitute for cleanup.
- In a named group, including an automatic session group, `open` navigates the group's most-recent tab by default (address-bar semantics; the reused tab stays in the background unless you add `--activate`). Pass `--no-reuse` when you need to keep the current page and open another, for example before comparing two pages or fanning out. `tab new` creates by default; explicit `--reuse` navigates the group's most-recent tab. Shared-default `open` creates by default.
- `interceptor open <url>`, `interceptor tab new <url>`, and `interceptor window new [url]` create background tabs and windows by default. Only `open --activate`, `tab new --activate`, `window new --activate`, `tab switch <id>`, and `window focus <id>` intentionally move browser focus. New tabs are created in the window that already holds Interceptor groups (the caller's own group's window first), not the window the user is focused on; the `tab new`/`open` result carries `windowId`.
- `interceptor window new [url]` gives your tab group its own background window: the new tab is the one showing there, and the group's other tabs move with it, so later `open` calls land in that window too. A `groupWarning` in the result means you are on the shared default group, which does not move; pass `--group <label>`.
- A page that will not render while hidden (empty grid, "Loading" that never resolves, an app that pauses on `document.hidden`): run `interceptor tab keepalive <id>`, then `read` again. The page reads as visible and its animation frames run; the user's view is untouched. Limits: IntersectionObserver and ResizeObserver callbacks still wait for a real paint, and background timers stay at Chromium's rate (about once per second after 10 s). When keepalive is not enough (a video that stays stalled, a virtualized grid with zero rows), put the page in your own window with `interceptor window new <url>`; do not `tab switch` in the user's window. Measured: a YouTube video stalled in a hidden tab even with keepalive and played within 3 s in its own background window; a virtualized options grid rendered 0 rows hidden and 16 in its own window; a Google Doc painted with keepalive alone. <!-- os:darwin -->
- A page that will not render while hidden (empty grid, "Loading" that never resolves, an app that pauses on `document.hidden`): run `interceptor tab keepalive <id>`, then `read` again. The page reads as visible and its animation frames run; the user's view is untouched. Limits: IntersectionObserver and ResizeObserver callbacks still wait for a real paint, and background timers stay at Chromium's rate (about once per second after 10 s). When keepalive is not enough (a video that stays stalled, a virtualized grid with zero rows), put the page in your own window with `interceptor window new <url>`; do not `tab switch` in the user's window. Check that it rendered with `read`; the measurements behind this are from the Mac. <!-- os:win32,linux -->
- - A trusted click does not need the tab brought on screen for the user. `interceptor click <ref> --trusted` is delivered to the tab's window in the background when the tab is the active tab of that window. A ref inside an iframe (`e<frame>_<n>`) works the same way. If a `tab switch` in the user's window ever was the only way, `tab switch <prior>` back to the tab that was showing is allowed even though it is unmanaged. <!-- os:darwin -->
- - If multiple browser profiles are connected, run `interceptor contexts` and pass `--context <id>`.
+ - A trusted click does not need the tab brought on screen for the user. `interceptor click <ref> --trusted` is delivered to the tab's window in the background, and a tab that is not showing is moved to a background window of its own and back. The result ends `the page received it`, or fails with `the page received nothing`, which means nothing happened: the usual cause is that the person is using the browser right then, so fall back to synthetic `click` (page listeners read `isTrusted: true`) or try again later. A ref inside an iframe (`e<frame>_<n>`) works the same way. If a `tab switch` in the user's window ever was the only way, `tab switch <prior>` back to the tab that was showing is allowed even though it is unmanaged. <!-- os:darwin -->
+ - If multiple browser profiles are connected, run `interceptor contexts` and pass `--context <id>`. The person can save a default once with `interceptor contexts default <name>`; it is used only when a command names no context and several are connected.
- Safari registers as the stable context `safari`; route with `interceptor --context safari <verb>`. If it is absent, verify the notarized Interceptor Safari extension is enabled before attempting page commands. Safari's enable switch is a protected user-present action; never try to bypass its Touch ID/password gate. <!-- os:darwin -->
- Prefer structured reads (`read`, `tree`, `text`, `inspect`, `scene`) before screenshots. Open `references/screenshot-policy.md` before screenshot-heavy work.
- Passwords and passcodes are typed by name from the keychain-backed vault: `interceptor type <ref> --secret <name>`. The daemon checks the tab's host against the secret's allowlist (`browser:<host>`) and the monitor and tree/forms reads use `***SECURE***`. Password fields are masked regardless of their source. A marked element stays sensitive until the document is replaced; a new plain-text DOM node does not inherit that marker. Never put a credential in a literal `type` call or ask the user to paste one into chat; ask them to run `interceptor macos secret register <name> --target browser:<host>`. <!-- os:darwin -->
- Passwords and passcodes are typed by name from Interceptor's vault, which lives in the system credential store: `interceptor type <ref> --secret <name>`. The daemon checks the tab's host against the secret's allowlist (`browser:<host>`) and the monitor and tree/forms reads use `***SECURE***`. Password fields are masked regardless of their source. A marked element stays sensitive until the document is replaced; a new plain-text DOM node does not inherit that marker. Never put a credential in a literal `type` call or ask the user to paste one into chat; ask them to run `interceptor desktop secret register <name> --target browser:<host>`. <!-- os:win32,linux -->
- If the password is already saved in a Chromium browser (Chrome, Brave, Vivaldi, Edge, Chromium, Arc), skip registration and fill it straight from that browser's store: `interceptor type <ref> --browser-login <host> [--user] [--browser <key>]` (password by default, username with `--user`; `--browser` restricts to one browser, default searches all installed). The daemon reads and decrypts the saved login; the requested host must match the live tab, so a page only ever fills its own credential. `interceptor browser creds list [--host <host>] [--browser <key>]` shows which hosts have a saved login (host + username + browser only, no passwords). macOS only; the value is kept off argv and delivery logs, and tree, forms, and scene reads mask it. This does not redact arbitrary eval, screenshots, network/storage capture, or credentials copied elsewhere by page code. <!-- os:darwin -->
- Default to plain text output. Use `--json` only when piping into scripts or when a downstream tool needs a machine-readable contract.
- - Unknown flags are rejected (exit 1, naming the flag and command) rather than ignored, so a typo never reads as success; `screenshot` writes to disk with `--save`, not `--out`. Fix the flag instead of setting `INTERCEPTOR_LAX_FLAGS=1`.
+ - Unknown flags are rejected (exit 1, naming the flag and command) rather than ignored, so a typo never reads as success; `screenshot --out <path>` writes that file (`--save` writes an auto-named one in the current directory). Fix the flag instead of setting `INTERCEPTOR_LAX_FLAGS=1`.
- A verb whose result is a failure prints `error: …` and exits non-zero (every browser verb, including `back`/`forward` with no history). Check `$?` in scripts; do not grep stdout for `error:` to detect failure.
- `eval --main` can recover from strict CSP by stripping the blocking header and reloading the tab. The result discloses that reload and the CLI prints a `warning:` line for it, on success and on failure; `save` shares the same recovery. Treat it as state-changing because unsaved page state is lost. Pass `--no-reload` on a page whose state matters to get the CSP error instead; task verification always refuses the reload.
- CSS-selector verbs (`query`, `exists`, `count`, `click --selector`, `wait`, `table`, `attr`, `style`, `rect`, `screenshot --selector`) search open and closed shadow roots after the light DOM, so a control inside a web component is reachable by selector; light-DOM matches keep their order and `--nth` its meaning.
- If the extension behaves stale after a package update, run `interceptor reload --context <id>`: an unpacked copy picks up the installed files; a Chrome Web Store copy only asks the store for an update and keeps its version until the store publishes the new one. `interceptor contexts --verbose` shows which copy (store or unpacked) and version each context runs.
- Safari package updates are loaded through the containing app/appex; do not look for a Chrome-style unpacked-extension reload button. <!-- os:darwin -->
- Safari suspends its background worker when idle, so `--context safari` can briefly report "context 'safari' not found" between commands and then self-heal. Re-issue the command rather than treating one transient drop as failure. Note two Safari capability limits: `headers add` only modifies recognized standard headers (arbitrary `X-…` names are refused — use `override` instead), and passive `net` capture reflects genuine page traffic, not requests you originate from `eval` (its world is separate from the page's). <!-- os:darwin -->
## Fast Path
```bash
interceptor contexts # 0. Browser profiles connected. More than one? Pick the lane's once:
export INTERCEPTOR_CONTEXT=<id> # (or pass --context <id> on every call; the flag overrides the env)
interceptor status --verbose # 1. Daemon + extension alive per context; eval --main availability
interceptor websearch "example docs" # 2a. Default provider → managed background results tab
interceptor open "https://example.com" --tree-format compact # 2b. Or open a known URL → wait + compact tree + text
interceptor read # 3. Current state (re-read after any mutation)
interceptor act e5 # 4. Click ref e5 (refs come from `read`)
interceptor act e7 "example user" # 5. Type into ref e7
+ interceptor act e7 "query" --submit # Type and submit the form in one call (a typed Enter never submits)
interceptor inspect # 6. Tree + text + network in one read
```
If your harness truncates tool results, also set `INTERCEPTOR_TREE_MAX_CHARS` / `INTERCEPTOR_TEXT_MAX_CHARS` once (defaults 50000 / 8000); a truncated tree or text ends in a marker that says how to scope or widen. A `stale element [eN]` error means the element left the DOM and nothing was clicked or typed; refs never re-bind to look-alike elements, so `read` again. A `timeout … The outcome is unknown` means the action may still land; `read` before retrying it.
Inside this repo without `interceptor` on PATH, use `./dist/interceptor ...`.
## Workflows
Each workflow is a complete self-contained "you are doing X" procedure. Open the file when the task matches.
| Workflow | When to invoke |
|---|---|
| [`workflows/verify-deploy.md`](workflows/verify-deploy.md) | "Verify the deploy", "check that X works on the page", reproducing a bug before touching code |
| [`workflows/read-and-extract.md`](workflows/read-and-extract.md) | Compound page read + SPA state extraction — pull a specific value off a page |
| [`workflows/drive-rich-editor.md`](workflows/drive-rich-editor.md) | Canva, Google Docs, Google Slides, design-tool layer manipulation — anything where DOM refs aren't enough |
| [`workflows/rich-editor-workflows.md`](workflows/rich-editor-workflows.md) | Canva shape insertion, Docs table build+fill, Slides table insert — what works natively vs the `eval --main` last mile |
| [`workflows/task-state.md`](workflows/task-state.md) | Durable task checkpoints, scoped lessons, compact resume and fresh browser completion checks |
| [`workflows/google-docs-fill-empty-table-cells.md`](workflows/google-docs-fill-empty-table-cells.md) | Fill empty Docs table cells with the value above (canvas caret + per-char typing + Tab) |
| [`workflows/canva-custom-size-creation.md`](workflows/canva-custom-size-creation.md) | Create a custom-size Canva design from home (normalized semantic replay) + monitor launch/handoff pattern |
| [`workflows/cook-in-canvas.md`](workflows/cook-in-canvas.md) | Draw effects/markers directly through a page's own `CanvasRenderingContext2D` (Docs/Excalidraw), pixel-verified |
| [`workflows/cook-on-top-of-pages.md`](workflows/cook-on-top-of-pages.md) | "Cook" a live page in-place — banners, HUDs, overlays that track real DOM, full-screen takeovers, over the real session |
| [`workflows/override-xhr.md`](workflows/override-xhr.md) | Mutate a request before it hits the server — change params, force a status, throttle |
| [`workflows/capture-page-communication.md`](workflows/capture-page-communication.md) | Capture WebSocket, Beacon, and BroadcastChannel activity without CDP |
| [`workflows/record-and-replay.md`](workflows/record-and-replay.md) | Learn a real user flow, export a replay plan, run it back |
| [`workflows/screenshot-for-vlm.md`](workflows/screenshot-for-vlm.md) | Take a screenshot the model will actually understand — VLM-budgeted, WebP, on-disk |
| [`workflows/multi-page-compare.md`](workflows/multi-page-compare.md) | Compare facts across multiple pages (e.g. "who designed Python vs JavaScript") — sequential `open --text-only` per page |
## References
| File | Topic |
|---|---|
| [`references/browser-and-network.md`](references/browser-and-network.md) | Command selection, SPA extraction, request overrides, SSE capture, page-world `eval --main` cautions |
| [`references/page-communication-capture.md`](references/page-communication-capture.md) | P1 WebSocket, Beacon, and BroadcastChannel capture mechanics, commands, event shapes, and limits |
| [`references/rich-editors.md`](references/rich-editors.md) | Overview: Canva, Google Docs, Google Slides behavior, canvas-rendered editor input, WebGL camera apps, blob export capture (deep mechanics in the four `references/canvas-*`/`webgl-*`/`blob-*` files below) |
| [`references/canvas-rendered-editor-input.md`](references/canvas-rendered-editor-input.md) | Deep mechanic: caret / typing / key-nav inside canvas-rendered editors (Docs/Slides/Sheets) via dispatched events + iframe-window `KeyboardEvent`. The `eval --main` + `__interceptor_trust`/`userActivation` foundation lives here. |
| [`references/canvas-camera-overlays.md`](references/canvas-camera-overlays.md) | Deep mechanic: pan/zoom a WebGL map viewer + lat/lng DOM overlays (Web Mercator), URL-watcher pattern, CSS-filter restyle |
| [`references/webgl-camera-control.md`](references/webgl-camera-control.md) | Deep mechanic: generic, app-agnostic WebGL camera control + overlay anchoring |
| [`references/blob-export-capture.md`](references/blob-export-capture.md) | Deep mechanic: capture a webapp's client-side export bytes (PNG/PDF/SVG) with no Save dialog |
| [`references/monitor-and-replay.md`](references/monitor-and-replay.md) | Monitor session behavior, replay-plan generation, cross-tab/focus-follow notes |
| [`references/command-catalog.md`](references/command-catalog.md) | Full browser command surface with flags and examples |
| [`references/screenshot-policy.md`](references/screenshot-policy.md) | VLM-aware screenshot budget table; agent-default recipe |
## When To Switch Surfaces
If the target is **outside the page** - a native dialog, browser chrome (URL bar, profile picker), Save/Open file picker, OS notification, or any non-browser macOS app - load `interceptor-macos` instead. <!-- os:darwin -->
If the target is **outside the page** - a native dialog, browser chrome (address bar, profile picker), Save/Open file picker, OS notification, or any other app - load `interceptor-desktop` instead. <!-- os:win32,linux -->
If the target is an **Electron / Chromium desktop app's web contents** (Slack, VS Code, Notion, Descript, etc.), use the CDP/app reference from `interceptor-macos`: `references/cdp-app.md`. <!-- os:darwin -->
If the target is an **Electron / Chromium desktop app's web contents** (Slack, VS Code, Notion, Descript, etc.), use `interceptor desktop cdp` from `interceptor-desktop`. <!-- os:win32,linux -->
If the task is **breadth research** — "investigate / go deep on / find everything about X" across many sources — load `interceptor-research`, which layers a planner loop, source ledger, and verification pass on top of this surface.
## Do Not Default To Troubleshooting
- User wants a browser task completed → run Interceptor commands.
- User wants Interceptor fixed, installed, or explained → that's a separate task; ask before diving into repo state.
- Inside the Interceptor repo, use this skill for live browser validation, not as the primary source of repo-development instructions.
## Completion
A browser job is complete only when:
- every claim about page state comes from a re-read (`read`/`inspect`) taken *after* the last mutation, not from the action's success alone;
- artifacts you produced (screenshots, saved files, captured payloads) are named by absolute path in the report;
- your tab group is closed (`interceptor group close <label>`) and `interceptor group list` no longer shows it — the list output is the proof, not the close command's exit code.
Report what failed or was skipped as prominently as what worked. Never report only the happy fields.