Customization
Before executing, check for user customizations at:
~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/
If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults.
MANDATORY: Voice Notification (REQUIRED BEFORE ANY ACTION)
You MUST send this notification BEFORE doing anything else when this skill is invoked.
Send voice notification:
curl -s -X POST http://localhost:31337/notify \
-H "Content-Type: application/json" \
-d '{"message": "Running the WORKFLOWNAME workflow in the Interceptor skill to ACTION"}' \
> /dev/null 2>&1 &
Output text notification:
Running the **WorkflowName** workflow in the **Interceptor** skill to ACTION...
This is not optional. Execute this curl command immediately upon skill invocation.
Interceptor — Real-Browser Automation + macOS Computer Use
First rule, above everything else: if you catch yourself about to ask the operator to do something in a browser — open a URL, click, fill a form, log in, paste a value, approve an OAuth/consent page — that urge IS the trigger to use Interceptor. Drive it yourself. The only exception is a step that needs a secret you genuinely don't hold (an unknown password, a hardware 2FA tap); even then, drive the flow up to that exact point, then surface only the one human-only action. Narrating clickwork a human has to perform is the precise failure this skill exists to prevent.
What It Does
Interceptor drives the real Chrome/Brave browser from inside it, and drives native macOS apps when the bridge is installed. Six capability classes, each its own verb tree: visual capture, DOM read, JS eval, network capture, input, and record/replay. It stays logged into your real sessions, passes every major bot-detection check, and is the mandatory tool for visual deploy verification.
The Problem
Headless browser tools speak CDP, which sites can fingerprint and block — so the automated browser sees a different page than a logged-in human does, or gets blocked outright. They also run as a separate browser instance with no auth, so they can't reach anything you're signed into. And when a page breaks — a blank screen after mount, a hydration mismatch, cards that flash and vanish — a screenshot tells you nothing about why. Interceptor solves all three: it operates through the actual browser UI (zero CDP fingerprint), uses your real signed-in sessions, and reads the live DOM, console errors, and network traffic to explain failures a screenshot can't.
How It Works
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/Interceptor
Install: the signed Interceptor-Browser-<v>.pkg / Interceptor-Full-<v>.pkg from the upstream releases page, or interceptor upgrade --full on an existing install. Building from source is optional — see Workflows/Update.md.
Chrome extension: the signed installer registers it for you. If you build from source instead, Tools/Pin.sh copies the built extension/dist into ~/.claude/skills/Interceptor/Extension/ — that directory does not ship with the skill, Pin.sh creates it — and Chrome loads it via "Load unpacked". Provenance is recorded in Extension/PINNED_FROM.txt (source path, manifest version, content SHA256, timestamp). Chrome disables unpacked extensions on every manifest bump, so after any source rebuild the copy must be re-pinned and reloaded. It does NOT auto-follow upstream.
Pinned binary: any interceptor >= 0.23.16 works with this skill. (Maintainer machine note — does not describe public installs: the maintainer's binary is built from a local branch local-0.23.16, upstream tag v0.23.16 plus a cherry-picked screenshot --save fix that honors --out and defaults to ~/Downloads; re-cherry-pick on every upgrade until upstreamed, and build from source rather than interceptor update since the self-updater would drop the patch. On a stock install, screenshot --save writes to cwd — pass --out explicitly.) Upstream is a three-surface control plane: Browser + macOS + iOS (drive an owned, Developer-Mode iPhone via an on-device XCUITest runner over WiFi — interceptor ios *). Tab Lifecycle Policy (new in 0.23.3): named-group reuse is default-on and idle managed groups auto-close after 10 min — see Tab hygiene below.
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 (DOM-render default — works backgrounded), screenshot --region, screenshot --pixel --full (window must be visible) |
PNG/WebP at any size, selector, region, or scroll-and-stitch full page. Route through Tools/Capture.sh, never raw interceptor screenshot. Zoom into fine detail with Tools/Zoom.ts. |
| 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. click --selector <css> clicks by CSS selector (0.23.6+), returns the clickable e<ref>, and auto-escalates to an OS-level click when the synthetic one is rejected |
| 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.
New surfaces & verbs (0.17 → 0.22.37)
What 0.22.2 → 0.22.37 added (2026-07 releases):
- Screenshot auto-fallback (0.22.37) — a failed DOM-render capture now automatically retries as
--pixel (side effects disclosed in the result; --no-fallback opts out). This covers render failures (injection-blocked pages, mid-navigation), NOT silent mis-composites — the animated-page --pixel/DOM-geometry cross-check rule in OPERATIONAL_RULES still stands.
- Half-open WebSocket detection (0.22.37) — daemon keepalive-ack catches dead-but-open extension connections that previously hung silently.
- Deterministic tab targeting (0.22.9–10) —
tab close <id> / tab switch <id> act on the id you pass (strictly numeric), never the working tab.
- File upload overhaul (0.22.21) — any size, dropzones, native pickers.
- Safari surface (0.22.32) and MCP server (0.22.35,
interceptor mcp install) — both deliberately unused here: we drive Chrome test-profile via CLI.
- a11y widening (0.22.37) — zero-area inline wrapper elements are no longer pruned from the tree.
The six browser verb trees above are still the core. What the jump from 0.16.9 added:
- iOS (
interceptor ios *) — drive an owned, unlocked, Developer-Mode iPhone over WiFi via an on-device XCUITest runner (not WebDriverAgent). Verbs: tree, find, click, type, scroll, screenshot, app launch|activate|terminate. Setup is Xcode self-service or a no-Xcode login that re-signs the runner with your Apple ID.
interceptor diagnose — one post-failure snapshot: daemon (with its real exec path), every connected context probed in parallel, monitor state. Catches the daemon split-brain — Chrome spawns one daemon binary while the CLI talks to another, previously a silent 15s timeout. Run it first when anything acts wedged.
interceptor manifest — machine-readable specs for 50+ verbs (usage, flags, returns semantics). Discover the contract without scraping help text.
- Per-agent tab groups (
--group <label> / INTERCEPTOR_GROUP) — pen each agent into its own colored, hard-isolated tab group so several agents share one browser with no cross-bleed. interceptor group list|close <label>. A second isolation layer alongside --context.
interceptor save — pull raw bytes (Blob / ArrayBuffer / blob:) straight off a live page to disk without a downloads folder; returns a sha256.
interceptor ocr / canvas ocr — offline Tesseract pixel OCR, bundled into the extension (no bridge, no macOS needed).
interceptor macos cdp * — drive the web contents of Electron / Chromium desktop apps (Slack, VS Code, Notion, Descript) the same way as a browser tab.
CLI contract (0.22.1): arguments are now order-independent (open --text-only <url> parses correctly), and browser-only installs hide the macOS/iOS verbs (--all-surfaces / INTERCEPTOR_ALL_SURFACES overrides). Bare interceptor / --help print a concise capability card; use help <cmd> or interceptor manifest for the full contract.
Why Interceptor?
CDP-based browser automation gets detected by sites. 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.
Interceptor vs Apify vs BrightData — which one, by scale × auth
Three tools touch "get data from a page," but they don't overlap much. Pick by how many pages and whether being logged in matters, not by which you reached for last time.
- Interceptor — one real, signed-in browser you drive. Use it when being logged into your own session matters, the target actively blocks bots, or the job is interactive: a deploy to verify, an OAuth/login/form flow, a debug pass, or extraction from a handful of auth-gated pages. The tell: you could open each page by hand and it'd look like you.
- Apify — managed cloud actors for bulk extraction from a named platform (Instagram, LinkedIn, TikTok, YouTube, Facebook, Google Maps, Amazon). Use it when you want hundreds-to-thousands of items, in parallel, structured, with proxy rotation. The tell: you'd never open these by hand, and there's a per-platform actor for it.
- BrightData — 4-tier progressive scraping of an arbitrary site (no named actor) that auto-escalates to residential proxy for heavy bot-detection/CAPTCHA at crawl scale.
One-line test: one page you could log into → Interceptor; N items on a named platform → Apify; a whole arbitrary site behind hard bot-detection → BrightData. Interceptor is one browser on this Mac — it does not do managed parallelism, proxy pools, or per-platform extractors, and pushing thousands of profiles through it is serial, slow, and risks your real accounts. Don't reach for it to replace Apify's scale, or for Apify when you need to be the logged-in user on a page or two.
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 the gate. Prefer the auto-recovering entry point — it runs the gate and, if the test profile window just isn't open, launches it and re-verifies before returning:
bash ~/.claude/skills/Interceptor/Tools/EnsureTestProfile.sh # runs the gate; auto-launches the test profile on exit 5/6; prints READY on success
EnsureTestProfile.sh wraps PreflightIsolation.sh (the raw gate — still callable directly when you want no auto-launch). Both exit non-zero on any unrecoverable failure; on non-zero, STOP and surface — never fall back to Default. The gate asserts these invariants:
- Binary version >= 0.16.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.
- The pinned test context is connected — matched whole-field against the UUID column (not a substring grep, so a header or partial collision can't false-pass). Without it, the operator's Default profile is the only available target.
- Target is not Default. The context the next command will hit is resolved and checked against Default and the
INTERCEPTOR_WORKING_PROFILE_IDS deny-list before any tab is touched. A Default/working-profile match is a hard stop (exit 7).
- Extension freshness (graceful).
Extension/PINNED_FROM.txt (manifest version + content SHA256) is compared against the upstream $INTERCEPTOR_SRC/extension/dist if present. Mismatch → fail with re-pin remediation. Upstream absent (currently true) → WARN and continue. This does NOT key off status --verbose (that command exposes no extension-build field).
If any 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. Do not fall back to operating against the Default profile, ever. Do not "try anyway." Do not use screencapture or osascript as a substitute.
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 — pinned test context missing (one-time profile setup needed)
7 — resolved target is Default or a working profile (hard stop)
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.
Isolation Doctrine (CRITICAL — hard rule, enforced in code)
Every browser command runs against the pinned, isolated Interceptor test context — ALWAYS that context, NEVER the operator's Default profile, NEVER their working/monitoring profiles. This is a constitutional rule, not a preference.
- The target context is
INTERCEPTOR_TEST_CONTEXT_ID from preferences.env. It is commonly pinned to a raw context UUID. Durable fix: replace the raw UUID with the friendly name interceptor-test set in the extension popup — friendly names survive reloads; raw UUIDs rot on every extension reload (see UUID-rot below).
- 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, tabs, and window. It IS signed into the operator's accounts (Google, GitHub, Cloudflare, blog admin, other admin dashboards) — 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.
- The operator's Default profile is read-only by default. Never open a tab, click, type, navigate, or record in Default 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.
- "Different app" is NOT a safety net — the operator's own browser may be Chrome too. When the operator browses in a different application, mistaking their window for the test context is self-correcting. It isn't when both are Chrome. Then the ONLY thing separating their browsing from your automation is the profile pin, so treat every unrecognized Chrome context as theirs, and hand them a URL with
open -a "Google Chrome" "<url>" (their normal profile) rather than by navigating a context you control.
- One-time setup lives in
Workflows/LaunchTestProfile.md — operator clicks Chrome's avatar menu → Add profile → signs in → loads the Interceptor extension → names the context in the popup.
RETIRED behavior — never re-derive it. The old "fall back to the first available / Default context when the pinned context isn't found" rule is DELETED. A missing or stale pinned context is a hard stop with remediation, never a fallback. There is no code path that auto-routes to Default.
Why bare commands are unsafe. With 2+ contexts connected the daemon hard-errors multiple extensions connected, use --context <id> — that fail-fast is the only thing protecting bare commands today. The moment the operator closes their other browser window (1 context left), a bare command silently auto-routes to whatever single context remains. So --context "$INTERCEPTOR_TEST_CONTEXT_ID" (or routing through Tools/Capture.sh) is mandatory on every browser verb, not optional.
UUID rot — durable fix. Context IDs are profile-stable chrome.storage.local UUIDs that change ONLY on extension reinstall/reload (not on Chrome restart). The durable fix is to set the friendly name interceptor-test in the extension popup once and pin that name. Until then, Tools/Capture.sh performs a guarded auto-rebind — only when exactly one non-Default test context is connected AND Default is provably excluded. A stale pin NEVER falls through to Default.
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 one stray test tab in their working window — a click, an unexpected redirect — is permanent and disruptive.
Auto-recovery is sanctioned for context-not-connected — via EnsureTestProfile.sh, never a bare launch. When the test profile window simply isn't open (PreflightIsolation.sh exits 5 = no contexts, or 6 = pinned context not connected), Tools/EnsureTestProfile.sh launches the CONFIGURED test profile and re-runs the gate, polling until the pinned context connects. This is safe because of one invariant: it only ever proceeds after PreflightIsolation.sh itself exits 0 — which whole-field-matches the connected context against INTERCEPTOR_TEST_CONTEXT_ID and hard-denies Default/working profiles. Launching the wrong --profile-directory therefore can never cause the agent to drive it; preflight would still fail and EnsureTestProfile would still stop. The safety lives in the post-launch re-verification loop, NOT in trusting the profile arg. Exit 7 (resolved target IS Default/working) and exit 8 (test context unset) NEVER trigger a launch — those surface and stop. And a bare LaunchTestProfile.sh with no re-verification is still unsafe on its own: always go through EnsureTestProfile.sh. If the launch succeeds but the pinned context never connects (UUID rot after an extension reload), EnsureTestProfile surfaces the one-time durable fix (name the context interceptor-test in the popup) and stops — it does not guess.
Rendering-Lifecycle Gate — the hidden-tab trap (MANDATORY for motion/responsive work)
Chrome suspends the entire rendering lifecycle for any tab whose window is not visible — minimized, fully occluded by another window, or on an inactive Space. In that state:
| Still works (so measurements look fine) |
Silently dead |
setTimeout / setInterval (throttled to ~1/s) |
requestAnimationFrame |
getBoundingClientRect() — forces layout on demand |
ResizeObserver |
| CSS media queries / static layout |
IntersectionObserver |
DOM reads, screenshot (DOM-render path) |
CSS transitions + animations, lazy-load, scroll-reveal |
This is the dangerous kind of failure: nothing errors. A responsive or animated page returns confident, well-formed numbers showing nothing happened, and the natural conclusion is "the feature doesn't fire" or "it works, no change detected." Measured live 2026-07-30: over 5s with two real element resizes, the pinned test context reported visibilityState: hidden, 0 rAF ticks and 0 ResizeObserver callbacks, while setInterval ticked 5 times.
The rule: any claim about animation, transitions, ResizeObserver/IntersectionObserver behavior, lazy-loading, scroll-reveal, or viewport-responsive layout MUST come from Tools/VerifyViewport.ts, never from the standard test context. Driving a component's own recompute by hand to "prove" it works is testing your code with your own hand, not verifying the browser.
bun ~/.claude/skills/Interceptor/Tools/VerifyViewport.ts check
bun ~/.claude/skills/Interceptor/Tools/VerifyViewport.ts probe <url> --widths 1440,1100,880 --expr @probe.js
bun ~/.claude/skills/Interceptor/Tools/VerifyViewport.ts shot <url> --width 880 --out ~/Downloads/x.png
bun ~/.claude/skills/Interceptor/Tools/VerifyViewport.ts stop
How it works, and why it can't be done any other way. The anti-throttling switches (--disable-backgrounding-occluded-windows, --disable-renderer-backgrounding, --disable-background-timer-throttling) only take effect at browser-process launch, and one browser process serves one --user-data-dir. They therefore can never be applied to an already-running Chrome, and --profile-directory shares that process — so no amount of profile juggling fixes the operator's browser. VerifyViewport runs a separate headless Chrome in its own --user-data-dir with those flags. Headless creates no window at all, so it puts nothing on the operator's desktop and there is no window state left to depend on — while running the same renderer, with the full lifecycle live. INTERCEPTOR_VERIFY_HEADFUL=1 gives a real (off-to-the-side) window when you need to watch a flow with your own eyes.
It is driven over CDP (interceptor macos cdp) rather than the extension, because --load-extension is dead in current Chrome (silently ignored — confirmed on 152, feature-flag workarounds included) and a custom --user-data-dir gets its own empty NativeMessagingHosts. Viewport size is set with Emulation.setDeviceMetricsOverride, a real layout-viewport change that fires the page's own observers without moving, resizing, or focusing any window — which is the whole point: verification must not depend on where the operator left their windows.
check and every probe/shot assert the lifecycle is live (visibilityState === visible AND rAF actually ticking) and exit non-zero otherwise. There is no degraded mode; a lifecycle failure is a hard stop, never a quiet measurement.
This is additive and touches nothing that worked. The operator's Chrome, the pinned test profile, the isolation gate, and the zero-CDP-fingerprint stealth path are all unchanged. Authenticated pages, real sessions, and bot-detection-sensitive work stay on the extension-driven test context — the verification instance has no logins and no stealth guarantees. Use it for public pages whose behavior over time is what's under test.
Install Modes (0.16.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 — the signed
.pkg installer registers it; on a from-source build, load it once via chrome://extensions/ → Developer Mode → "Load unpacked" → the Extension/ directory Tools/Pin.sh creates
interceptor CLI in PATH (/opt/homebrew/bin/interceptor)
interceptor-daemon in PATH (/opt/homebrew/bin/interceptor-daemon)
- Native messaging manifest registered (the signed
.pkg does this; from source, bash "$INTERCEPTOR_SRC"/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.23.16 (<hash>, <date>)" — 0.23.16+ is what matters
interceptor status # → daemon: running, bridge: running, mode: full|browser-only
interceptor status --verbose # → adds extension reachability (NO extension-build field; with 2+ contexts it nags "multiple extensions connected" even with --context)
interceptor contexts # → list of connected browser contexts (multi-profile)
interceptor init # → one-time write of ~/.config/interceptor/config.toml
Background-First Contract (0.16.x)
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 (0.16.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_CONTEXT_ID" # 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 — and with one context left that means a bare command silently hits whatever remains. Zero or 2+ contexts fail fast with a structured error. Always pass --context "$INTERCEPTOR_TEST_CONTEXT_ID" (or route through Tools/Capture.sh); never rely on auto-route.
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. Set the friendly name interceptor-test here to end UUID rot.
Standing default: --context "$INTERCEPTOR_TEST_CONTEXT_ID". 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, Screen Recording, Microphone — the
trust probe keys are accessibility / screenRecording / microphone, no inputMonitoring field) 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.
Compound Commands (Preferred)
These collapse multi-step patterns into single invocations — fewer tool calls, fewer tokens:
interceptor open <url> # Open + wait + return tree + text
interceptor open <url> --reuse # Navigate existing managed tab
interceptor open <url> --activate # Bring new/reused tab to front (explicit opt-in)
interceptor open <url> --tree-only|--text-only|--full|--no-wait|--include-frames
interceptor read # Tree + text for active tab
interceptor read <ref> # Subtree
interceptor read --markdown # Render page as markdown (preserves headings/tables/emphasis)
interceptor read --markdown --text-only # Markdown prose only, no tree
interceptor read --tree-only --tree-format compact # Actionable refs only
interceptor read --include-style|--include-frames
interceptor act <ref> # Click + wait + return updated tree + diff
interceptor act <ref> "value" # Type + wait + return updated tree
interceptor act <ref> --trusted # OS-level HID-sourced input (was --os; --os is deprecated alias)
interceptor act <ref> --keys "Enter" # Send keyboard shortcut
interceptor act <ref> --no-read # Skip post-action tree read
interceptor inspect # Tree + text + network log + headers
interceptor inspect --net-only|--filter <pattern>
--trusted vs --os: v0.13.3 renamed --os to --trusted (canonical). --os is kept as a deprecated alias and emits a warning. Lead with --trusted in new code.
Command Reference (moved)
The full per-verb CLI listing — macOS Native (Computer Use), VM Lifecycle, Core Browser Commands, Network/Exports, Recording (Session Monitor), Canvas, Scene Graph, LinkedIn, ChatGPT Agentic Bridge, Batch + Meta — lives in References/CommandReference.md. Quick pointers:
| Verb tree |
Reference section |
interceptor macos * (native apps, AX, trusted input, Vision/Speech/NLP, Apple Events, logs, fs, overlay) |
macOS Native (Computer Use) |
interceptor macos vm * (Linux + macOS guests, gold image, clone/snapshot) |
VM Lifecycle — plus Workflows/VmLifecycle.md |
state, tree, find, click, type, navigate, tabs, screenshot, eval, style, cookies |
Core Browser Commands |
net log/headers/export, override, network *, sse *, headers * |
Network — Passive, CDP, and Exports |
monitor * (record/replay) |
Recording (Session Monitor) |
canvas *, scene * |
Canvas / Scene Graph |
linkedin *, chatgpt *, batch, status, contexts, init, upgrade |
LinkedIn / ChatGPT Bridge / Batch + Meta |
Key Rules
- Requires Chrome/Brave running for browser commands — it's an extension, not a standalone binary.
- Requires bridge running for
act --trusted, macos *, full-screen capture, VM lifecycle — interceptor status confirms.
- Refs use eN syntax —
e12 not @e12. Treat refs as short-lived; re-read or find after navigation, rerenders, or DOM mutations.
- Cross-frame refs —
read --include-frames returns refs like e<frameId>_<n> for non-top frames.
- Plain text by default.
--json only when piping into a script. Prose-trained models comprehend tree/text output better than dense JSON.
- Daemon auto-starts — first command launches it; no manual start needed.
- Prefer compound commands (
open, read, act, inspect) over manual tab new + wait + tree chains.
- Prefer structured reads over screenshots unless the task is explicitly visual or pixel-based — tree/text/network/scene/AX data is faster, smaller, and more deterministic.
--trusted is canonical; --os is a deprecated alias. v0.13.3+. New code uses --trusted.
--markdown is the structured-prose surface — preserves headings, tables, emphasis. Use instead of --text-only when visual hierarchy disambiguates the answer.
- Background-first by contract. Only
--activate / app activate / explicit tab switch move focus.
- Verify with
frontmost before/after. Native workflows that promise no focus change should prove it.
- Never
screencapture. Never osascript for Chrome focus, bounds, tabs, or windows. Forbidden under Hard Prohibitions. Survives Interceptor failures.
- Anything lifecycle-dependent goes through
Tools/VerifyViewport.ts — animation, transitions, ResizeObserver/IntersectionObserver, lazy-load, scroll-reveal, viewport-responsive layout. The standard test context silently reports zero activity for all of these whenever its window isn't visible. See the Rendering-Lifecycle Gate section.
- Screenshots go through
Tools/Capture.sh, never raw interceptor screenshot. Capture.sh runs the preflight gate, enforces the not-Default target check, handles UUID-rot rebind, prefers the DOM-render path, and writes review artifacts to $LIFEOS_DOWNLOADS_DIR (default ~/Downloads/ when unset).
- Tab hygiene — upstream now cleans up after itself (Tab Lifecycle Policy, new in 0.23.3);
CleanupTabs.sh is the backstop, not the primary mechanism. Two built-in behaviors, both configured only from the extension popup (the popup is the sole writer): (1) named-group reuse (default on) — interceptor open <url> --group <label> navigates that group's most-recent tab instead of opening a new one (address-bar semantics), so a 40-step automation leaves one tab, not forty. Named groups only: in the s
…(truncated)
1---2name: interceptor3description: Real Chrome/Brave + macOS Computer Use from inside the browser — zero CDP fingerprint, real sessions; mandatory for visual deploy verification. Drive clickwork yourself. USE WHEN verify deploy, confirm UI, screenshot verification, computer use, macos automation, debug web, troubleshoot, visual check, motion/animation bug, jank, transition stutter, scrub a flow, console logs/errors, runtime/JS/react errors, mismatch warning, network traffic/log, HAR/pcapng export, hydration/blank-page debug, flash then blank/page broken, why is this not working/what's happening on the page, authenticated page, bot detection bypass, reproduce bug, drive native app, about to ask the operator to click/navigate/fill a form/log in/approve OAuth, OAuth consent flow, complete a web login, do this in your browser. NOT FOR residential-proxy crawling (BrightData) or social actor scraping (Apify).4---5
6## Customization
7
8**Before executing, check for user customizations at:**
9`~/.claude/LIFEOS/USER/CUSTOMIZATIONS/SKILLS/Interceptor/`
10
11If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults.
12
13## MANDATORY: Voice Notification (REQUIRED BEFORE ANY ACTION)
14
15**You MUST send this notification BEFORE doing anything else when this skill is invoked.**
16
171. **Send voice notification**:
18 ```bash
19 curl -s -X POST http://localhost:31337/notify \
20 -H "Content-Type: application/json" \
21 -d '{"message": "Running the WORKFLOWNAME workflow in the Interceptor skill to ACTION"}' \
22 > /dev/null 2>&1 &
23 ```
24
252. **Output text notification**:
26 ```
27 Running the **WorkflowName** workflow in the **Interceptor** skill to ACTION...
28 ```
29
30**This is not optional. Execute this curl command immediately upon skill invocation.**
31
32# Interceptor — Real-Browser Automation + macOS Computer Use
33
34> **First rule, above everything else: if you catch yourself about to ask the operator to do something in a browser — open a URL, click, fill a form, log in, paste a value, approve an OAuth/consent page — that urge IS the trigger to use Interceptor. Drive it yourself. The only exception is a step that needs a secret you genuinely don't hold (an unknown password, a hardware 2FA tap); even then, drive the flow up to that exact point, then surface only the one human-only action. Narrating clickwork a human has to perform is the precise failure this skill exists to prevent.**
35
36## What It Does
37
38Interceptor drives the real Chrome/Brave browser from inside it, and drives native macOS apps when the bridge is installed. Six capability classes, each its own verb tree: visual capture, DOM read, JS eval, network capture, input, and record/replay. It stays logged into your real sessions, passes every major bot-detection check, and is the mandatory tool for visual deploy verification.
39
40## The Problem
41
42Headless browser tools speak CDP, which sites can fingerprint and block — so the automated browser sees a different page than a logged-in human does, or gets blocked outright. They also run as a separate browser instance with no auth, so they can't reach anything you're signed into. And when a page breaks — a blank screen after mount, a hydration mismatch, cards that flash and vanish — a screenshot tells you nothing about why. Interceptor solves all three: it operates through the actual browser UI (zero CDP fingerprint), uses your real signed-in sessions, and reads the live DOM, console errors, and network traffic to explain failures a screenshot can't.
43
44## How It Works
45
46Interceptor 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.
47
48**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.
49**Repo:** https://github.com/Hacker-Valley-Media/Interceptor
50**Install:** the signed `Interceptor-Browser-<v>.pkg` / `Interceptor-Full-<v>.pkg` from the upstream releases page, or `interceptor upgrade --full` on an existing install. Building from source is optional — see `Workflows/Update.md`.
51**Chrome extension:** the signed installer registers it for you. If you build from source instead, `Tools/Pin.sh` copies the built `extension/dist` into `~/.claude/skills/Interceptor/Extension/` — that directory does **not** ship with the skill, `Pin.sh` creates it — and Chrome loads it via "Load unpacked". Provenance is recorded in `Extension/PINNED_FROM.txt` (source path, manifest version, content SHA256, timestamp). Chrome disables unpacked extensions on every manifest bump, so after any source rebuild the copy must be **re-pinned and reloaded**. It does NOT auto-follow upstream.
52**Pinned binary:** any `interceptor >= 0.23.16` works with this skill. (Maintainer machine note — does not describe public installs: the maintainer's binary is built from a local branch `local-0.23.16`, upstream tag `v0.23.16` plus a cherry-picked `screenshot --save` fix that honors `--out` and defaults to `~/Downloads`; re-cherry-pick on every upgrade until upstreamed, and build from source rather than `interceptor update` since the self-updater would drop the patch. On a stock install, `screenshot --save` writes to cwd — pass `--out` explicitly.) Upstream is a **three-surface** control plane: **Browser** + **macOS** + **iOS** (drive an owned, Developer-Mode iPhone via an on-device XCUITest runner over WiFi — `interceptor ios *`). **Tab Lifecycle Policy (new in 0.23.3):** named-group reuse is default-on and idle managed groups auto-close after 10 min — see Tab hygiene below.
53
54### Capabilities Overview — Six Verb Trees
55
56Each 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.**
57
58| Verb tree | Top-level verbs | What you get |
59|-----------|-----------------|--------------|
60| **VISUAL** | `screenshot` (DOM-render default — works backgrounded), `screenshot --region`, `screenshot --pixel --full` (window must be visible) | PNG/WebP at any size, selector, region, or scroll-and-stitch full page. Route through `Tools/Capture.sh`, never raw `interceptor screenshot`. Zoom into fine detail with `Tools/Zoom.ts`. |
61| **DOM READ** | `read [--markdown]`, `tree`, `text`, `html <ref>`, `find` | Accessibility tree, structured markdown, raw markup, refs |
62| **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** |
63| **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 |
64| **INPUT** | `click`, `type`, `keys`, `act <ref> [--trusted]`, `drag`, `scroll`, `select`, `focus` | Browser + native macOS input; `--trusted` for OS-level HID source state. `click --selector <css>` clicks by CSS selector (0.23.6+), returns the clickable `e<ref>`, and auto-escalates to an OS-level click when the synthetic one is rejected |
65| **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 |
66
67**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.
68
69**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.
70
71### New surfaces & verbs (0.17 → 0.22.37)
72
73What 0.22.2 → 0.22.37 added (2026-07 releases):
74
75- **Screenshot auto-fallback (0.22.37)** — a failed DOM-render capture now automatically retries as `--pixel` (side effects disclosed in the result; `--no-fallback` opts out). This covers render *failures* (injection-blocked pages, mid-navigation), NOT silent mis-composites — the animated-page `--pixel`/DOM-geometry cross-check rule in OPERATIONAL_RULES still stands.
76- **Half-open WebSocket detection (0.22.37)** — daemon keepalive-ack catches dead-but-open extension connections that previously hung silently.
77- **Deterministic tab targeting (0.22.9–10)** — `tab close <id>` / `tab switch <id>` act on the id you pass (strictly numeric), never the working tab.
78- **File upload overhaul (0.22.21)** — any size, dropzones, native pickers.
79- **Safari surface (0.22.32)** and **MCP server (0.22.35, `interceptor mcp install`)** — both deliberately unused here: we drive Chrome test-profile via CLI.
80- **a11y widening (0.22.37)** — zero-area inline wrapper elements are no longer pruned from the tree.
81
82The six browser verb trees above are still the core. What the jump from 0.16.9 added:
83
84- **iOS** (`interceptor ios *`) — drive an owned, unlocked, Developer-Mode iPhone over WiFi via an on-device XCUITest runner (not WebDriverAgent). Verbs: `tree`, `find`, `click`, `type`, `scroll`, `screenshot`, `app launch|activate|terminate`. Setup is Xcode self-service or a no-Xcode `login` that re-signs the runner with your Apple ID.
85- **`interceptor diagnose`** — one post-failure snapshot: daemon (with its real exec path), every connected context probed in parallel, monitor state. **Catches the daemon split-brain** — Chrome spawns one daemon binary while the CLI talks to another, previously a silent 15s timeout. Run it first when anything acts wedged.
86- **`interceptor manifest`** — machine-readable specs for 50+ verbs (usage, flags, returns semantics). Discover the contract without scraping help text.
87- **Per-agent tab groups** (`--group <label>` / `INTERCEPTOR_GROUP`) — pen each agent into its own colored, hard-isolated tab group so several agents share one browser with no cross-bleed. `interceptor group list|close <label>`. A second isolation layer alongside `--context`.
88- **`interceptor save`** — pull raw bytes (Blob / ArrayBuffer / `blob:`) straight off a live page to disk without a downloads folder; returns a sha256.
89- **`interceptor ocr`** / **`canvas ocr`** — offline Tesseract pixel OCR, bundled into the extension (no bridge, no macOS needed).
90- **`interceptor macos cdp *`** — drive the web contents of Electron / Chromium desktop apps (Slack, VS Code, Notion, Descript) the same way as a browser tab.
91
92**CLI contract (0.22.1):** arguments are now order-independent (`open --text-only <url>` parses correctly), and browser-only installs hide the macOS/iOS verbs (`--all-surfaces` / `INTERCEPTOR_ALL_SURFACES` overrides). Bare `interceptor` / `--help` print a concise capability card; use `help <cmd>` or `interceptor manifest` for the full contract.
93
94### Why Interceptor?
95
96CDP-based browser automation gets detected by sites. 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.
97
98### Interceptor vs Apify vs BrightData — which one, by scale × auth
99
100Three tools touch "get data from a page," but they don't overlap much. Pick by **how many pages** and **whether being logged in matters**, not by which you reached for last time.
101
102- **Interceptor** — one real, signed-in browser you drive. Use it when being logged into your own session matters, the target actively blocks bots, or the job is interactive: a deploy to verify, an OAuth/login/form flow, a debug pass, or extraction from a *handful* of auth-gated pages. The tell: you could open each page by hand and it'd look like you.
103- **Apify** — managed cloud actors for **bulk** extraction from a named platform (Instagram, LinkedIn, TikTok, YouTube, Facebook, Google Maps, Amazon). Use it when you want hundreds-to-thousands of items, in parallel, structured, with proxy rotation. The tell: you'd never open these by hand, and there's a per-platform actor for it.
104- **BrightData** — 4-tier progressive scraping of an *arbitrary* site (no named actor) that auto-escalates to residential proxy for heavy bot-detection/CAPTCHA at crawl scale.
105
106One-line test: **one page you could log into → Interceptor; N items on a named platform → Apify; a whole arbitrary site behind hard bot-detection → BrightData.** Interceptor is one browser on this Mac — it does not do managed parallelism, proxy pools, or per-platform extractors, and pushing thousands of profiles through it is serial, slow, and risks your real accounts. Don't reach for it to replace Apify's scale, or for Apify when you need to *be* the logged-in user on a page or two.
107
108### Hard Prohibitions — Operative on Every Invocation
109
110**Visual verification goes through Interceptor only. The following are FORBIDDEN with zero exceptions:**
111
112- **`screencapture`** — the raw macOS screenshot binary. Not as a primary tool, not as a fallback when Interceptor wedges, not "just for one screenshot." Forbidden.
113- **`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.
114- **`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.
115- **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.
116- **Any window-state mutation** — moving, resizing, repositioning, or reordering Chrome windows is forbidden. The operator owns their window arrangement; the agent never touches it.
117- **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.
118
119**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.
120
121**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.
122
123### Preflight Isolation Gate (MANDATORY)
124
125**Every browser workflow's first step. No exceptions.**
126
127Before any `interceptor open|read|act|inspect|screenshot|navigate|tab|monitor|net|cookies|scroll|click|type` lands in Chrome, the workflow runs the gate. Prefer the **auto-recovering entry point** — it runs the gate and, if the test profile window just isn't open, launches it and re-verifies before returning:
128
129```bash
130bash ~/.claude/skills/Interceptor/Tools/EnsureTestProfile.sh # runs the gate; auto-launches the test profile on exit 5/6; prints READY on success
131```
132
133`EnsureTestProfile.sh` wraps `PreflightIsolation.sh` (the raw gate — still callable directly when you want no auto-launch). Both exit non-zero on any unrecoverable failure; on non-zero, STOP and surface — never fall back to Default. The gate asserts these invariants:
134
1351. **Binary version >= 0.16.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.
1362. **The pinned test context is connected** — matched whole-field against the UUID column (not a substring grep, so a header or partial collision can't false-pass). Without it, the operator's Default profile is the only available target.
1373. **Target is not Default.** The context the next command will hit is resolved and checked against Default and the `INTERCEPTOR_WORKING_PROFILE_IDS` deny-list before any tab is touched. A Default/working-profile match is a hard stop (exit 7).
1384. **Extension freshness (graceful).** `Extension/PINNED_FROM.txt` (manifest version + content SHA256) is compared against the upstream `$INTERCEPTOR_SRC/extension/dist` **if present**. Mismatch → fail with re-pin remediation. Upstream absent (currently true) → WARN and continue. This does NOT key off `status --verbose` (that command exposes no extension-build field).
139
140If any 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. **Do not fall back to operating against the Default profile, ever.** Do not "try anyway." Do not use `screencapture` or `osascript` as a substitute.
141
142Exit codes (for handlers that need to discriminate):
143- `2` — interceptor binary not on PATH
144- `3` — version string unparseable
145- `4` — version below minimum (upgrade via `Workflows/Update.md`)
146- `5` — no browser contexts connected (Chrome closed or extension dead)
147- `6` — pinned test context missing (one-time profile setup needed)
148- `7` — resolved target is Default or a working profile (hard stop)
149
150The 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.
151
152### Isolation Doctrine (CRITICAL — hard rule, enforced in code)
153
154**Every browser command runs against the pinned, isolated Interceptor test context — ALWAYS that context, NEVER the operator's Default profile, NEVER their working/monitoring profiles.** This is a constitutional rule, not a preference.
155
156- **The target context is `INTERCEPTOR_TEST_CONTEXT_ID`** from `preferences.env`. It is commonly pinned to a raw context UUID. Durable fix: replace the raw UUID with the friendly name `interceptor-test` set in the extension popup — friendly names survive reloads; raw UUIDs rot on every extension reload (see UUID-rot below).
157- **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, tabs, and window. It IS signed into the operator's accounts (Google, GitHub, Cloudflare, blog admin, other admin dashboards) — 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.
158- **The operator's Default profile is read-only by default.** Never open a tab, click, type, navigate, or record in Default 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.
159- **"Different app" is NOT a safety net — the operator's own browser may be Chrome too.** When the operator browses in a different application, mistaking their window for the test context is self-correcting. It isn't when both are Chrome. Then the ONLY thing separating their browsing from your automation is the profile pin, so treat every unrecognized Chrome context as theirs, and hand them a URL with `open -a "Google Chrome" "<url>"` (their normal profile) rather than by navigating a context you control.
160- **One-time setup lives in `Workflows/LaunchTestProfile.md`** — operator clicks Chrome's avatar menu → Add profile → signs in → loads the Interceptor extension → names the context in the popup.
161
162**RETIRED behavior — never re-derive it.** The old "fall back to the first available / Default context when the pinned context isn't found" rule is DELETED. A missing or stale pinned context is a **hard stop with remediation**, never a fallback. There is no code path that auto-routes to Default.
163
164**Why bare commands are unsafe.** With 2+ contexts connected the daemon hard-errors `multiple extensions connected, use --context <id>` — that fail-fast is the only thing protecting bare commands today. The moment the operator closes their other browser window (1 context left), a bare command silently auto-routes to whatever single context remains. So `--context "$INTERCEPTOR_TEST_CONTEXT_ID"` (or routing through `Tools/Capture.sh`) is mandatory on every browser verb, not optional.
165
166**UUID rot — durable fix.** Context IDs are profile-stable `chrome.storage.local` UUIDs that change ONLY on extension reinstall/reload (not on Chrome restart). The durable fix is to set the friendly name `interceptor-test` in the extension popup once and pin that name. Until then, `Tools/Capture.sh` performs a **guarded** auto-rebind — only when exactly one non-Default test context is connected AND Default is provably excluded. A stale pin NEVER falls through to Default.
167
168**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 one stray test tab in their working window — a click, an unexpected redirect — is permanent and disruptive.
169
170**Auto-recovery is sanctioned for context-not-connected — via `EnsureTestProfile.sh`, never a bare launch.** When the test profile window simply isn't open (`PreflightIsolation.sh` exits 5 = no contexts, or 6 = pinned context not connected), `Tools/EnsureTestProfile.sh` launches the CONFIGURED test profile and re-runs the gate, polling until the pinned context connects. This is safe because of one invariant: **it only ever proceeds after `PreflightIsolation.sh` itself exits 0** — which whole-field-matches the connected context against `INTERCEPTOR_TEST_CONTEXT_ID` and hard-denies Default/working profiles. Launching the wrong `--profile-directory` therefore can never cause the agent to drive it; preflight would still fail and `EnsureTestProfile` would still stop. The safety lives in the post-launch re-verification loop, NOT in trusting the profile arg. **Exit 7 (resolved target IS Default/working) and exit 8 (test context unset) NEVER trigger a launch** — those surface and stop. And a *bare* `LaunchTestProfile.sh` with no re-verification is still unsafe on its own: always go through `EnsureTestProfile.sh`. If the launch succeeds but the pinned context never connects (UUID rot after an extension reload), `EnsureTestProfile` surfaces the one-time durable fix (name the context `interceptor-test` in the popup) and stops — it does not guess.
171
172### Rendering-Lifecycle Gate — the hidden-tab trap (MANDATORY for motion/responsive work)
173
174**Chrome suspends the entire rendering lifecycle for any tab whose window is not visible** — minimized, fully occluded by another window, or on an inactive Space. In that state:
175
176| Still works (so measurements look fine) | Silently dead |
177|---|---|
178| `setTimeout` / `setInterval` (throttled to ~1/s) | `requestAnimationFrame` |
179| `getBoundingClientRect()` — forces layout on demand | `ResizeObserver` |
180| CSS media queries / static layout | `IntersectionObserver` |
181| DOM reads, `screenshot` (DOM-render path) | CSS transitions + animations, lazy-load, scroll-reveal |
182
183This is the dangerous kind of failure: nothing errors. A responsive or animated page returns confident, well-formed numbers showing *nothing happened*, and the natural conclusion is "the feature doesn't fire" or "it works, no change detected." Measured live 2026-07-30: over 5s with two real element resizes, the pinned test context reported `visibilityState: hidden`, **0 rAF ticks and 0 ResizeObserver callbacks**, while `setInterval` ticked 5 times.
184
185**The rule: any claim about animation, transitions, ResizeObserver/IntersectionObserver behavior, lazy-loading, scroll-reveal, or viewport-responsive layout MUST come from `Tools/VerifyViewport.ts`, never from the standard test context.** Driving a component's own recompute by hand to "prove" it works is testing your code with your own hand, not verifying the browser.
186
187```bash
188bun ~/.claude/skills/Interceptor/Tools/VerifyViewport.ts check
189bun ~/.claude/skills/Interceptor/Tools/VerifyViewport.ts probe <url> --widths 1440,1100,880 --expr @probe.js
190bun ~/.claude/skills/Interceptor/Tools/VerifyViewport.ts shot <url> --width 880 --out ~/Downloads/x.png
191bun ~/.claude/skills/Interceptor/Tools/VerifyViewport.ts stop
192```
193
194**How it works, and why it can't be done any other way.** The anti-throttling switches (`--disable-backgrounding-occluded-windows`, `--disable-renderer-backgrounding`, `--disable-background-timer-throttling`) only take effect at **browser-process launch**, and one browser process serves one `--user-data-dir`. They therefore can never be applied to an already-running Chrome, and `--profile-directory` shares that process — so no amount of profile juggling fixes the operator's browser. VerifyViewport runs a **separate headless Chrome in its own `--user-data-dir`** with those flags. Headless creates no window at all, so it puts nothing on the operator's desktop and there is no window state left to depend on — while running the same renderer, with the full lifecycle live. `INTERCEPTOR_VERIFY_HEADFUL=1` gives a real (off-to-the-side) window when you need to watch a flow with your own eyes.
195
196It is driven over **CDP** (`interceptor macos cdp`) rather than the extension, because `--load-extension` is dead in current Chrome (silently ignored — confirmed on 152, feature-flag workarounds included) and a custom `--user-data-dir` gets its own empty `NativeMessagingHosts`. Viewport size is set with `Emulation.setDeviceMetricsOverride`, a real layout-viewport change that fires the page's own observers **without moving, resizing, or focusing any window** — which is the whole point: verification must not depend on where the operator left their windows.
197
198`check` and every `probe`/`shot` **assert** the lifecycle is live (`visibilityState === visible` AND rAF actually ticking) and exit non-zero otherwise. There is no degraded mode; a lifecycle failure is a hard stop, never a quiet measurement.
199
200**This is additive and touches nothing that worked.** The operator's Chrome, the pinned test profile, the isolation gate, and the zero-CDP-fingerprint stealth path are all unchanged. Authenticated pages, real sessions, and bot-detection-sensitive work stay on the extension-driven test context — the verification instance has no logins and no stealth guarantees. Use it for public pages whose *behavior over time* is what's under test.
201
202### Install Modes (0.16.x)
203
204Two install modes, same CLI binary. Confirm with `interceptor status` and read the `mode:` line:
205
206| Mode | What's installed | What unlocks |
207|------|------------------|--------------|
208| **`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** |
209| **`mode: browser-only`** | CLI + daemon + extension | Browser automation only. `interceptor macos *` returns a structured `setup_required` error in under 1s. No TCC prompts. |
210
211Promote a browser-only install with `interceptor upgrade --full`. Downgrade with `bash scripts/uninstall.sh --bridge-only`.
212
213**Install channels** (pkg installers landed v0.11+):
214- `Interceptor-Browser-<v>.pkg` → `mode: browser-only`
215- `Interceptor-Full-<v>.pkg` → `mode: full`
216- `bash scripts/install.sh --browser-only|--full` → dev path
217- Linux browser-only supported (Microsoft Edge + Vivaldi also recognized as of v0.13.4)
218
219Operating 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.
220
221### Prerequisites
222
223- Chrome or Brave (or Edge/Vivaldi on supported platforms) running with the Interceptor extension loaded — the signed `.pkg` installer registers it; on a from-source build, load it once via `chrome://extensions/` → Developer Mode → "Load unpacked" → the `Extension/` directory `Tools/Pin.sh` creates
224- `interceptor` CLI in PATH (`/opt/homebrew/bin/interceptor`)
225- `interceptor-daemon` in PATH (`/opt/homebrew/bin/interceptor-daemon`)
226- Native messaging manifest registered (the signed `.pkg` does this; from source, `bash "$INTERCEPTOR_SRC"/scripts/install.sh --chrome --skip-extension`)
227- **macOS bridge** as a LaunchAgent (full mode only) — see `Workflows/Update.md`
228- **Sparkle.framework** at `/usr/local/Frameworks/Sparkle.framework` (full mode, v0.10.0+) — bridge depends on it for auto-update
229
230Quick health check:
231
232```bash
233interceptor --version # → "interceptor 0.23.16 (<hash>, <date>)" — 0.23.16+ is what matters
234interceptor status # → daemon: running, bridge: running, mode: full|browser-only
235interceptor status --verbose # → adds extension reachability (NO extension-build field; with 2+ contexts it nags "multiple extensions connected" even with --context)
236interceptor contexts # → list of connected browser contexts (multi-profile)
237interceptor init # → one-time write of ~/.config/interceptor/config.toml
238```
239
240### Background-First Contract (0.16.x)
241
242The whole product is background-first. Routine work never moves the user's focus.
243
244| Surface | Verbs that move focus | Everything else |
245|---|---|---|
246| **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. |
247| **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. |
248
249If you call any verb not listed in the "moves focus" column and the frontmost changes, that's a bug.
250
251**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.
252
253### Multi-Context Routing (0.16.x)
254
255When multiple browser profiles are connected (e.g., personal Chrome + isolated test profile + work Brave), commands need to know which one to drive.
256
257```bash
258interceptor contexts # List connected context IDs
259interceptor open <url> --context "$INTERCEPTOR_TEST_CONTEXT_ID" # Route to the isolated test profile (DEFAULT)
260interceptor open <url> --context <main-id> # Route to the operator's personal Chrome (only when explicitly requested)
261```
262
263Without `--context`, browser commands auto-route only when exactly one context is connected — and with one context left that means a bare command silently hits whatever remains. Zero or 2+ contexts fail fast with a structured error. Always pass `--context "$INTERCEPTOR_TEST_CONTEXT_ID"` (or route through `Tools/Capture.sh`); never rely on auto-route.
264
265Context IDs are set via the Interceptor extension popup (click the toolbar icon → Context ID field → Save). One-time setup; the daemon remembers across restarts. Set the friendly name `interceptor-test` here to end UUID rot.
266
267**Standing default:** `--context "$INTERCEPTOR_TEST_CONTEXT_ID"`. See `Workflows/LaunchTestProfile.md` for one-time setup.
268
269### Computer Use — macOS Native Helper
270
271The bridge is a Swift LaunchAgent that runs as the user and exposes capabilities the Chrome extension cannot provide on its own:
272
273- **OS-level trusted input** (`interceptor act <ref> --trusted`, `macos type --trusted`, `macos keys --trusted` — bypasses `isTrusted` checks via HID source state)
274- **Native macOS app control** (`interceptor macos open/read/act/inspect` — same surface as the browser, against any running app)
275- **Accessibility tree** of any running app for inspection without screenshots
276- **Screen capture beyond Chrome** (full-screen, off-tab, multi-display, occluded windows)
277- **VM lifecycle** (`interceptor macos vm create/clone/start/exec/snapshot/restore/stop/delete` — Linux + macOS guests, replaces Lume/Tart/UTM)
278- **Clipboard r/w**, audio listen + speech recognition, system notifications, Vision OCR, NLP, Apple Intelligence, HealthKit, display info
279- **Apple Events dispatch** to named bundle IDs without activation
280- **OSLogStore predicate queries**, filesystem search/watching, URL fetch
281- **Monitor** (cross-app workflow recording with optional clipboard/files/network/log/notifications/speech channels and `--frames` screenshot capture)
282
283**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.
284
285**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.
286
287**Security model — read before installing:**
288
289- Transport is a **UNIX domain socket** at `/tmp/interceptor-bridge.sock`. Local-only; no network listener.
290- **No authentication on the socket.** Any local process running as your user can connect and execute every bridge action. macOS TCC permissions (Accessibility, Screen Recording, Microphone — the `trust` probe keys are `accessibility` / `screenRecording` / `microphone`, no `inputMonitoring` field) are granted to the bridge once and inherited by every socket client.
291- **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.
292- Single-user Mac threat model: acceptable, since anything running as you can already do this with effort. Multi-user Macs need socket hardening.
293
294---
295
296## Compound Commands (Preferred)
297
298These collapse multi-step patterns into single invocations — fewer tool calls, fewer tokens:
299
300```bash
301interceptor open <url> # Open + wait + return tree + text
302interceptor open <url> --reuse # Navigate existing managed tab
303interceptor open <url> --activate # Bring new/reused tab to front (explicit opt-in)
304interceptor open <url> --tree-only|--text-only|--full|--no-wait|--include-frames
305interceptor read # Tree + text for active tab
306interceptor read <ref> # Subtree
307interceptor read --markdown # Render page as markdown (preserves headings/tables/emphasis)
308interceptor read --markdown --text-only # Markdown prose only, no tree
309interceptor read --tree-only --tree-format compact # Actionable refs only
310interceptor read --include-style|--include-frames
311interceptor act <ref> # Click + wait + return updated tree + diff
312interceptor act <ref> "value" # Type + wait + return updated tree
313interceptor act <ref> --trusted # OS-level HID-sourced input (was --os; --os is deprecated alias)
314interceptor act <ref> --keys "Enter" # Send keyboard shortcut
315interceptor act <ref> --no-read # Skip post-action tree read
316interceptor inspect # Tree + text + network log + headers
317interceptor inspect --net-only|--filter <pattern>
318```
319
320**`--trusted` vs `--os`:** v0.13.3 renamed `--os` to `--trusted` (canonical). `--os` is kept as a deprecated alias and emits a warning. Lead with `--trusted` in new code.
321
322## Command Reference (moved)
323
324The full per-verb CLI listing — macOS Native (Computer Use), VM Lifecycle, Core Browser Commands, Network/Exports, Recording (Session Monitor), Canvas, Scene Graph, LinkedIn, ChatGPT Agentic Bridge, Batch + Meta — lives in `References/CommandReference.md`. Quick pointers:
325
326| Verb tree | Reference section |
327|-----------|-------------------|
328| `interceptor macos *` (native apps, AX, trusted input, Vision/Speech/NLP, Apple Events, logs, fs, overlay) | macOS Native (Computer Use) |
329| `interceptor macos vm *` (Linux + macOS guests, gold image, clone/snapshot) | VM Lifecycle — plus `Workflows/VmLifecycle.md` |
330| `state`, `tree`, `find`, `click`, `type`, `navigate`, `tabs`, `screenshot`, `eval`, `style`, `cookies` | Core Browser Commands |
331| `net log/headers/export`, `override`, `network *`, `sse *`, `headers *` | Network — Passive, CDP, and Exports |
332| `monitor *` (record/replay) | Recording (Session Monitor) |
333| `canvas *`, `scene *` | Canvas / Scene Graph |
334| `linkedin *`, `chatgpt *`, `batch`, `status`, `contexts`, `init`, `upgrade` | LinkedIn / ChatGPT Bridge / Batch + Meta |
335
336## Key Rules
337
338- **Requires Chrome/Brave running** for browser commands — it's an extension, not a standalone binary.
339- **Requires bridge running** for `act --trusted`, `macos *`, full-screen capture, VM lifecycle — `interceptor status` confirms.
340- **Refs use eN syntax** — `e12` not `@e12`. Treat refs as short-lived; re-`read` or `find` after navigation, rerenders, or DOM mutations.
341- **Cross-frame refs** — `read --include-frames` returns refs like `e<frameId>_<n>` for non-top frames.
342- **Plain text by default.** `--json` only when piping into a script. Prose-trained models comprehend tree/text output better than dense JSON.
343- **Daemon auto-starts** — first command launches it; no manual start needed.
344- **Prefer compound commands** (`open`, `read`, `act`, `inspect`) over manual `tab new` + `wait` + `tree` chains.
345- **Prefer structured reads over screenshots** unless the task is explicitly visual or pixel-based — tree/text/network/scene/AX data is faster, smaller, and more deterministic.
346- **`--trusted` is canonical; `--os` is a deprecated alias.** v0.13.3+. New code uses `--trusted`.
347- **`--markdown` is the structured-prose surface** — preserves headings, tables, emphasis. Use instead of `--text-only` when visual hierarchy disambiguates the answer.
348- **Background-first by contract.** Only `--activate` / `app activate` / explicit `tab switch` move focus.
349- **Verify with `frontmost` before/after.** Native workflows that promise no focus change should prove it.
350- **Never `screencapture`. Never `osascript` for Chrome focus, bounds, tabs, or windows.** Forbidden under Hard Prohibitions. Survives Interceptor failures.
351- **Anything lifecycle-dependent goes through `Tools/VerifyViewport.ts`** — animation, transitions, ResizeObserver/IntersectionObserver, lazy-load, scroll-reveal, viewport-responsive layout. The standard test context silently reports zero activity for all of these whenever its window isn't visible. See the Rendering-Lifecycle Gate section.
352- **Screenshots go through `Tools/Capture.sh`, never raw `interceptor screenshot`.** Capture.sh runs the preflight gate, enforces the not-Default target check, handles UUID-rot rebind, prefers the DOM-render path, and writes review artifacts to `$LIFEOS_DOWNLOADS_DIR` (default `~/Downloads/` when unset).
353- **Tab hygiene — upstream now cleans up after itself (Tab Lifecycle Policy, new in 0.23.3); `CleanupTabs.sh` is the backstop, not the primary mechanism.** Two built-in behaviors, both configured only from the extension popup (the popup is the sole writer): **(1) named-group reuse (default on)** — `interceptor open <url> --group <label>` navigates that group's most-recent tab instead of opening a new one (address-bar semantics), so a 40-step automation leaves one tab, not forty. Named groups only: in the s
354
355…(truncated)