Interceptor is a Chrome extension that operates through the actual browser UI plus an optional macOS bridge that extends the same control surface to native apps, OS-level input, and on-device VMs.
Tool: interceptor CLI — Chrome/Brave extension that controls the real browser from inside, plus a macOS bridge that drives native apps, OS-level input, and full VM lifecycle.
Repo: https://github.com/Hacker-Valley-Media/slop-browser
Install: ~/Projects/interceptor (built from source — see Workflows/Update.md)
Chrome "Load unpacked" target: ~/.claude/skills/Interceptor/Extension/ (symlink → upstream extension/dist/)
Pinned upstream: v0.15.1 — interceptor --version reports the build SHA + date.
Capabilities Overview — Six Verb Trees
Each row is an independent capability class. Each one uses a different WebSocket message type at the daemon→extension boundary, so a wedge on one rarely affects the others. When debugging a broken page, every row is a separate diagnostic path — never bail on Interceptor because screenshot hangs without trying eval, net log, or monitor first.
| Verb tree |
Top-level verbs |
What you get |
| VISUAL |
screenshot, screenshot --region, screenshot --pixel --full |
PNG/WebP at any size, selector, region, or scroll-and-stitch full page |
| DOM READ |
read [--markdown], tree, text, html <ref>, find |
Accessibility tree, structured markdown, raw markup, refs |
| JS EVAL |
eval <code>, eval --main |
Run JavaScript in isolated or main world — the way you read console errors, runtime exceptions, hydration warnings, and DOM state at runtime |
| NETWORK |
net log, net headers, net export --format har|pcapng|json, override, headers add/remove |
Passive request capture with zero CDP fingerprint; HAR 1.2 + pcapng for Wireshark |
| INPUT |
click, type, keys, act <ref> [--trusted], drag, scroll, select, focus |
Browser + native macOS input; --trusted for OS-level HID source state |
| RECORD/REPLAY |
monitor start/stop, monitor export --plan, monitor export --format har |
Real user-flow capture as deterministic replay scripts; multi-session, browser + macOS AX events |
Reading console errors is a recipe, not a separate verb: inject a console.error / window.error listener via eval --main, store events on window.__errs, then eval again to read them back. Useful for hydration mismatches, React error boundaries, JS exceptions on load, and any silent runtime failure. Triggering a reload between install and read loses the captured errors — capture happens after load, so reproduce-by-reload doesn't help; instead capture forward from the next user action, or poll getEventListeners(window) / DOM mutation observers for evidence of failure.
Why this matters in practice. Hydration failures on Astro pages, blank-page after React mount, "cards flash and disappear" — all of these surface through eval reading the live DOM state and console captures, not through screenshots. A screenshot wedge does not block diagnosis; the page is still inspectable through every other verb tree.
Why Interceptor?
agent-browser (the Browser skill) uses CDP — sites can detect it. Interceptor is a Chrome extension that operates through the actual browser UI. No debugger, no automation flags, no separate browser instance. You stay logged in, you pass bot detection, the agent sees what you see. The optional macOS bridge extends the same control surface to native applications, OS-level input, and on-device VMs — that combination is what "Computer Use" means in this skill.
Hard Prohibitions — Operative on Every Invocation
Visual verification goes through Interceptor only. The following are FORBIDDEN with zero exceptions:
screencapture — the raw macOS screenshot binary. Not as a primary tool, not as a fallback when Interceptor wedges, not "just for one screenshot." Forbidden.
osascript for Chrome control — no tell application "Google Chrome" to activate, no set frontmost of process, no set bounds of window, no set active tab index, no set index of window, no other window-state mutation. Forbidden.
osascript System Events keystrokes — no key code, no keystroke, no key down. These send input to whatever is focused, which steals from the operator. Forbidden.
- Any focus pull in service of automation — bringing Chrome (or any app) to the front so a screenshot will land is forbidden. The bridge's CGS / DOM-render paths capture without focus change.
- Any window-state mutation — moving, resizing, repositioning, or reordering Chrome windows is forbidden. The operator owns their window arrangement; the agent never touches it.
- AppleScript-driven tab switching —
set active tab index of window N is doubly forbidden: it both pulls focus and changes which tab the operator is looking at.
These rules survive Interceptor failures. A wedged Interceptor is NOT a license to use raw OS tools. When Interceptor cannot deliver evidence, the recovery is to fix Interceptor (see WebSocket-wedge gotcha below) or to STOP and tell the operator the verification cannot be captured this run — never to fall back.
Bridge-routed Computer Use is separate. interceptor macos open <app>, interceptor macos act <ref>, interceptor act <ref> --trusted (formerly --os) and other bridge-routed actions go through the sanctioned bridge surface and are allowed when a workflow explicitly requires native app control. The prohibition above is on (a) raw OS-level paths that bypass Interceptor entirely AND (b) focus-pulling purely in service of a screenshot.
Preflight Isolation Gate (MANDATORY)
Every browser workflow's first step. No exceptions.
Before any interceptor open|read|act|inspect|screenshot|navigate|tab|monitor|net|cookies|scroll|click|type lands in Chrome, the workflow runs:
bash ~/.claude/skills/Interceptor/Tools/PreflightIsolation.sh
The script asserts two invariants:
- Binary version >= 0.15.0 — older builds silently ignore
--context and fall back to whichever Chrome connection the daemon can find. That fallback is how a tab lands in the operator's Default window.
interceptor-test context is connected — the dedicated test profile must be live and registered with the daemon. Without it, the operator's Default profile is the only available target.
If either check fails, the script exits non-zero with a structured remediation message to stderr. The workflow MUST STOP on a non-zero exit. Surface the message to the operator. Do not fall back to operating against the Default profile, ever. Do not "try anyway." Do not use screencapture or osascript as a substitute. The cost of one stray tab in the operator's working window is higher than the cost of skipping verification this run.
Exit codes (for handlers that need to discriminate):
2 — interceptor binary not on PATH
3 — version string unparseable
4 — version below minimum (upgrade via Workflows/Update.md)
5 — no browser contexts connected (Chrome closed or extension dead)
6 — interceptor-test context missing (one-time profile setup needed)
The gate is doctrine. It runs unconditionally — for read-only public-page fetches, for authenticated tooling verification, for screenshot capture, for everything. There is no "safe to skip" case, because every silent fallback to Default is a violation of the operator's window.
Profile Isolation — Default Behavior (CRITICAL)
All live testing routes through a dedicated Chrome profile in its own window, never the operator's Default profile. This is a constitutional rule, not a preference.
- The isolation boundary is Chrome PROFILE, not user-data-dir. The test profile lives inside the same Chrome installation as the operator's Default profile but with separate cookies, separate tabs, separate window. It IS signed into the operator's accounts (Google, GitHub, Cloudflare, blog admin, ULAdmin) — that's the whole point. A
--user-data-dir sandbox would be useless because it has zero auth and can't reach any of the operator's signed-in tooling.
- Default context for every live-test command:
--context interceptor-test. Launched via Tools/LaunchTestProfile.sh, which uses Chrome's --profile-directory="Profile N" to open the test profile's window.
- The operator's Default profile is read-only by default. Never open a tab, click, type, navigate, or record in the Default profile unless the operator explicitly says so ("verify in my Default profile", "use the main window"). When they do, route via
--context <default-id> after interceptor contexts confirms the connection.
- One-time setup lives in
Workflows/LaunchTestProfile.md — operator clicks Chrome's avatar menu → Add profile → signs the new profile into their accounts → loads the Interceptor extension → names the context interceptor-test in the popup.
- If
interceptor-test is not connected (the test window is closed), surface the gap and ask the operator which profile to relaunch. Do not silently fall back to the Default profile.
- Multi-context backstop. With two contexts connected, bare commands (no
--context) fail fast per AGENTS.md. That's a structural safety net, not the primary protection — the primary is the rule above.
- Context ID is per-machine and lives in
preferences.env. The friendly literal interceptor-test is the default when no preferences file exists. The actual binding lives in ~/.claude/PAI/USER/CUSTOMIZATIONS/SKILLS/Interceptor/preferences.env as INTERCEPTOR_TEST_CONTEXT_ID=<friendly-name-or-uuid>. PreflightIsolation.sh sources that file and checks interceptor contexts for the configured ID. When the binding is a raw UUID, expect rot on every extension reload — set a friendly name in the extension popup once and the UUID rot stops mattering.
Why this is doctrine, not preference. The operator's Default profile holds the tabs they're actively working in and the tabs their DA has been driving. The cost of one extra flag on every command is zero. The cost of opening a test tab in the Default profile, clicking something, or triggering an unexpected redirect in a tab the operator was about to read is permanent and disruptive.
NEVER auto-run LaunchTestProfile.sh on preflight failure. When PreflightIsolation.sh exits non-zero with context-not-connected or context-name-mismatch, surface the error to the operator and stop. Do NOT auto-run LaunchTestProfile.sh to "fix" it. The INTERCEPTOR_TEST_CHROME_PROFILE value in preferences.env is a literal --profile-directory argument (e.g., Profile 4). If that value is stale, auto-launching it would open the wrong Chrome profile — potentially the operator's monitoring profile where they have live windows open. The only safe path is operator-confirmed launch. Same principle for context-name-mismatch: when the live UUID differs from the configured ID, the fix is editing preferences.env, not launching anything new.
Install Modes (v0.15.x)
Two install modes, same CLI binary. Confirm with interceptor status and read the mode: line:
| Mode |
What's installed |
What unlocks |
mode: full (default for this skill) |
CLI + daemon + extension + Swift bridge .app + LaunchAgent |
Browser automation plus Computer Use: AX tree, OS-level trusted input, ScreenCaptureKit, Vision OCR, Speech, NLP, Apple Events, OSLogStore, file watching, container runtime, VM lifecycle |
mode: browser-only |
CLI + daemon + extension |
Browser automation only. interceptor macos * returns a structured setup_required error in under 1s. No TCC prompts. |
Promote a browser-only install with interceptor upgrade --full. Downgrade with bash scripts/uninstall.sh --bridge-only.
Install channels (pkg installers landed v0.11+):
Interceptor-Browser-<v>.pkg → mode: browser-only
Interceptor-Full-<v>.pkg → mode: full
bash scripts/install.sh --browser-only|--full → dev path
- Linux browser-only supported (Microsoft Edge + Vivaldi also recognized as of v0.13.4)
Operating rule: if the user asks for native and status reports mode: browser-only, respond "I'm on a browser-only install. Run interceptor upgrade --full to enable that." Don't run the macos command anyway to see what happens — the preflight short-circuits, but it wastes turns.
Prerequisites
- Chrome or Brave (or Edge/Vivaldi on supported platforms) running with the Interceptor extension loaded — load it once via
chrome://extensions/ → Developer Mode → "Load unpacked" → ~/.claude/skills/Interceptor/Extension/
interceptor CLI in PATH (/opt/homebrew/bin/interceptor)
interceptor-daemon in PATH (/opt/homebrew/bin/interceptor-daemon)
- Native messaging manifest registered (
bash ~/Projects/interceptor/scripts/install.sh --chrome --skip-extension)
- macOS bridge as a LaunchAgent (full mode only) — see
Workflows/Update.md
- Sparkle.framework at
/usr/local/Frameworks/Sparkle.framework (full mode, v0.10.0+) — bridge depends on it for auto-update
Quick health check:
interceptor --version # → "interceptor 0.15.x (sha, date)"
interceptor status # → daemon: running, bridge: running, mode: full|browser-only
interceptor status --verbose # → adds extension reachability + browser-default mismatch warning
interceptor contexts # → list of connected browser contexts (multi-profile)
interceptor init # → one-time write of ~/.config/interceptor/config.toml
Background-First Contract (v0.14.2+)
The whole product is background-first. Routine work never moves the user's focus.
| Surface |
Verbs that move focus |
Everything else |
| Browser |
open --activate, tab new --activate, tab switch <id>, window focus <id> |
Stays on whatever the operator was looking at — click, type, read, inspect, screenshot, net, cookies, scroll, act. New tabs land in the background by default. |
| macOS |
app activate <app>, open <app> --activate |
Stays on whatever was frontmost — open (no --activate), all input verbs, AX reads, capture, menu, intent dispatch, vision, overlays. |
If you call any verb not listed in the "moves focus" column and the frontmost changes, that's a bug.
Reuse path: open --reuse navigates the existing managed tab without leaving dead tabs behind. Preserves the reused tab's focus state — pair with --activate only when the user explicitly says to bring it forward.
Multi-Context Routing (v0.15.x)
When multiple browser profiles are connected (e.g., personal Chrome + isolated test profile + work Brave), commands need to know which one to drive.
interceptor contexts # List connected context IDs
interceptor open <url> --context interceptor-test # Route to the isolated test profile (DEFAULT)
interceptor open <url> --context <main-id> # Route to the operator's personal Chrome (only when explicitly requested)
Without --context, browser commands auto-route only when exactly one context is connected. Zero or multiple contexts fail fast with a structured error — that's the multi-context backstop for the Profile Isolation rule above.
Context IDs are set via the Interceptor extension popup (click the toolbar icon → Context ID field → Save). One-time setup; the daemon remembers across restarts.
Standing default: --context interceptor-test. See Workflows/LaunchTestProfile.md for one-time setup.
Computer Use — macOS Native Helper
The bridge is a Swift LaunchAgent that runs as the user and exposes capabilities the Chrome extension cannot provide on its own:
- OS-level trusted input (
interceptor act <ref> --trusted, macos type --trusted, macos keys --trusted — bypasses isTrusted checks via HID source state)
- Native macOS app control (
interceptor macos open/read/act/inspect — same surface as the browser, against any running app)
- Accessibility tree of any running app for inspection without screenshots
- Screen capture beyond Chrome (full-screen, off-tab, multi-display, occluded windows)
- VM lifecycle (
interceptor macos vm create/clone/start/exec/snapshot/restore/stop/delete — Linux + macOS guests, replaces Lume/Tart/UTM)
- Clipboard r/w, audio listen + speech recognition, system notifications, Vision OCR, NLP, Apple Intelligence, HealthKit, display info
- Apple Events dispatch to named bundle IDs without activation
- OSLogStore predicate queries, filesystem search/watching, URL fetch
- Monitor (cross-app workflow recording with optional clipboard/files/network/log/notifications/speech channels and
--frames screenshot capture)
Status check: interceptor status reports bridge: running with PID + socket when it's up, or bridge: not running with a hint when it isn't.
Lifecycle (install / verify / troubleshoot / uninstall) lives in Workflows/Update.md. The Update workflow handles binary placement, Sparkle framework install, LaunchAgent plist, launchctl bootstrap, and TCC prompts in the right order.
Security model — read before installing:
- Transport is a UNIX domain socket at
/tmp/interceptor-bridge.sock. Local-only; no network listener.
- No authentication on the socket. Any local process running as your user can connect and execute every bridge action. macOS TCC permissions (Accessibility, Input Monitoring, Screen Recording, Microphone) are granted to the bridge once and inherited by every socket client.
- Marginal risk is supply-chain: a malicious local package gains a one-step path to OS-level input/screen/clipboard without needing its own permission grants.
- Single-user Mac threat model: acceptable, since anything running as you can already do this with effort. Multi-user Macs need socket hardening.