CLAUDE.md@.claude · git:20261015.5cfa3b3 · 2026-10-15 · sha256 a18e66331f874ab3
CLAUDE.md@.claude git:20261015.5cfa3b3A
Immutable. This exact content never changes and is served at /api/v1/blob/a18e66331f874ab3.
# PairBrowse
A Claude Code and Codex plugin: Claude (or Codex) fills web forms in the PairBrowse browser (Playwright's Chromium, a
persistent, visible window the user can watch and step into; inside the Claude desktop workspace
through the live view, docked or in the Browser pane).
## Layout
- `.claude-plugin/`: Claude Code plugin and marketplace manifests. `.mcp.json`: the `browser` and `runs` MCP servers. `.codex-plugin/plugin.json` + `mcp.json`: the Codex plugin (servers `pairbrowse_browser`, `pairbrowse_runs`); Codex also reads `hooks/hooks.json`. Keep both manifests' versions equal.
- `scripts/launch.mjs` (fixed entry) + `scripts/launcher/`: stdio bridge Claude Code runs (`bridge.mjs`); `daemon-connect.mjs` reaches the helper's socket, starting the daemon when needed (shared with `open.mjs`); installs the pinned runtime and starts or reconnects to the daemon.
- `scripts/daemon.mjs`: the helper's entry point and composition root. It owns the browser (Playwright persistent context over a pipe), serves one session over a private socket, enforces `scripts/rules/policy.mjs` and secret domains (`scripts/rules/secrets.mjs`). Its parts live in `scripts/daemon/`, each a `createX(deps)` factory: `serve.mjs` (tool calls, one connection per agent; its parts in `serve/`: `connection.mjs` its state, `wire.mjs` tool sets and message checks, `turns.mjs` its tab and turns, `refuse.mjs` refusal checks, `rewrite.mjs` a call's way to the browser server, `tools/` its own tools by family, `collaboration.mjs`, `result.mjs` each result, `pageread.mjs`), `context.mjs` (the browser facade over `browser/` (state, launch, tabs, downloads) and `sessions/` (picker, switch)), `follow.mjs` (joining someone else's session; its parts in `join/`: state, hosts (the join-host allowlist), agents (remote agent calls), screens (shared browser picture pages), mirror (follow mode)), `board.mjs` (who does what across a shared session, messages), `toolstab.mjs` (the Tools tab), `presence.mjs` (people using the browser), `hud.mjs` (badge, bar, spark, cursor), `screenshot.mjs`, `drag.mjs` (browser_drag as a hand does it), `output.mjs` (masking what Claude reads, main-frame refs plain), `panel.mjs` (side panel), `sharing.mjs` (invites, join codes; `tunnel-pool.mjs` the sharing tunnels), `page.mjs` (in-page helpers). Also there: `approvals.mjs` (approval cards, the approve role), `joinprompt.mjs` (join requests in the bar), `activitylog.mjs` + `journal.mjs` (Activity and what each agent did), `timeline.mjs` + `recorder.mjs` (timeline pictures, `pairbrowse_record`), `screenshare.mjs` (joiners' shared screens), `cobrowse.mjs`, `pick.mjs` (session picker posts), `pause.mjs`, `reveal.mjs` (fast mode's safe disclosure scan), `preview.mjs` (file previews), `capture.mjs` (the native build's window capture), `diagnostics.mjs` (scrubbed reports), `taborder.mjs`, `tablabels.mjs`, `forms.mjs`, `fields.mjs`, `outline.mjs`, `linkwait.mjs`, `exec-contexts.mjs`, `remote-agents.mjs`.
- `scripts/liveview.mjs` + `scripts/liveview/` (request checks and the one body reader in `http.mjs`, invites, input replay, joiners' masked frames and server): key-protected local live view with input passthrough. `liveview.mjs` keeps the streams, the shown tab and joiners and dispatches; its routes live in `scripts/liveview/routes/` (`viewer`, `owner`, `sessions`, `pages`, `timeline`, `tools`), each a `{ method, path, right, needs?, handler }` table. Every event it sends is listed in `ui/events.js` with who gets it. The page is `liveview.html` + `.css` + `.js`, served from a fixed allowlist under a strict CSP; code it shares with the side panel is in `ui/common.js`.
- `scripts/sharing/`: the joiner's and the host's sharing code: `join.mjs` (join codes, the join-host allowlist, approvals, name and text cleaning), `join-client.mjs` (the joiner's connection to a host, moving to a standby tunnel when one goes down), `sharing/tabsync.mjs` (what crosses to joiners: tabs, forms, order, pointers), `sharing/devshare.mjs`.
- `scripts/tunnels/`: public addresses. `quick.mjs` (Cloudflare Quick Tunnels: the pinned cloudflared, start, adopt after a restart), `keeper.mjs` (the keeper process that stops a tunnel once no helper beats; its entry stays `scripts/tunnel-keeper.mjs`, the name keepers are adopted by), `config.mjs` + `start.mjs` (named tunnels).
- `scripts/sharing/devshare.mjs`: sharing a localhost dev server with joiners (`pairbrowse_invite` `share_port`): its own Quick Tunnel (`scripts/tunnels/quick.mjs`) to a proxy that checks each joiner's key and rewrites Host/Origin to localhost; tabs on it cross as that address (`sharing/tabsync.mjs` `mapUrl`).
- `scripts/browser.mjs` + `scripts/browser/` (`choice.mjs`: the one pure `chooseBrowser(config)`, binary (Chromium, custom path, native) apart from shell (extension or own pages); icon files; the extension's pages are in `ui/`, its root during the bridge period; colors are Chromium's own color theme set in the profile): the PairBrowse browser: on macOS a pinned ungoogled-chromium build (version + SHA-256 in `browser.mjs`, branded into `~/.pairbrowse/browser/PairBrowse.app`, profile button hidden; `PairBrowse Chromium.app` when a native build is installed), elsewhere Playwright's Chromium. `scripts/hud.js`: in-page badge, bottom bar and the spark on Claude's tab icon. Always headed; there is no headless mode.
- `ui/`: the browser's own pages, loaded as the side panel extension during the bridge period: `tokens.css` (the one sheet), `common.js` (shared with the live view), `manifest.json`, `background.js` (the worker), `logo.svg`, icons, `base.css` (the rules every page sheet repeated), flat shared modules at the root (`helper.js` the one helper-address lookup and `postJson`, `stream.js` `helperStream`/`helperConnection`, `events.js` every event and who gets it, `urls.js`, `limits.js`, `vocab.js`, `words.js`, `dom.js`, `motion.js`, `panels.js`, `popover.js`, `people.js`, `bubbles.js`, `checklist.js`, `report.js`, `pairbrowse-api.js` + `pairbrowse-adapter.js` (`window.pairbrowse` over the native build's bindings); `common.js` re-exports most of them), and one directory per page (`newtab/`, `picker/`, `screen/`, `capture/`, `right/`, `left/`, `strip/`, `activity/`, `approve/`, `sessions/`, `settings/`, `timeline/`), each an `index.html` with its own css/js. The native build rejects a `ui/<dir>` without `index.html`, so shared code stays flat at the root; every file the live view serves is in its allowlist (tested).
- Native builds (the private PairBrowse Pro repo, `../pairbrowse-native`, builds them; `BINARY-LICENSE.md` is their license). `scripts/native-pack.mjs` (fixed: the release check attests it): `NATIVE` pins each platform's build (sha256 null = none yet) and the engine pack, all downloaded from the public GitHub release `browser-<version>`; `scripts/native/pack.mjs` downloads them, and checks the engine pack (launch switches, humanized input, host collector) by archive and per-file SHA-256 before every load. `scripts/native-install.mjs` (fixed CLI entry) + `scripts/native/install.mjs`: install step, `nativeLayout()` picks the build for this platform and chip; SHA-256, manifest platform/arch, on macOS signature + branding (`scripts/native/macos-app.mjs`), swap with rollback, profile prep, then `scripts/native/check.mjs` (launch self-check). `scripts/native-engine.mjs`: launch adapter (settings under `"pairbrowse"` in config; its path is fixed, the Pro repo's engine test reads its `pack.*` calls); `scripts/native/humanize.mjs`: humanized typing and form fills. Never name the third-party project the engine pack and patches come from in this repo.
- `scripts/guard.mjs` (PreToolUse), `scripts/log.mjs` (PostToolUse), `scripts/session-start.mjs`: hooks (Claude Code and Codex). Codex accepts only `deny` from PreToolUse, so the daemon applies `decide()` itself for every client other than `claude-code`.
- `scripts/tools/catalog.mjs`: every tool both MCP servers offer or pass through, its definition and its rules, in one place; the browser server (`daemon/serve/wire.mjs`), the guard (`rules/decide.mjs`), `rules/policy.mjs`, the bridge and `validate-skills.mjs` read it.
- `scripts/hookline.mjs`: `HOOK_MARK` and `requestLine`, the one "send a line, read a line" client on the helper's private socket (the PermissionRequest hook in `guard.mjs`, the opener `open.mjs`).
- `scripts/rules/`: the rules every client is held to, pure and stdlib-only: `decide.mjs` (the guard's verdicts; `guard.mjs` is the hook around it), `clicks.mjs` (click classes, clickRule, dialogRule), `policy.mjs` (blocked tools, navigation, masking), `secrets.mjs` (secret domains), `upload.mjs` (which files upload), `network.mjs` (local-network addresses).
- `scripts/core.md`: the always-on PairBrowse core that `session-start.mjs` adds to every session (worded for Claude Code or Codex by `scripts/surface.mjs`). Keep it compact (size and tool names are tested).
- `scripts/runs.mjs`: dependency-free MCP server for saved runs and pre-submit reviews.
- `scripts/runner.mjs`: fast mode (`pairbrowse_run`): `runSteps`, `preflight`, `outline`; its parts in `scripts/runner/`: `steps.mjs` (stepsIn, each step kind), `risk.mjs` (click risk), `locate.mjs` (field finding), `choices.mjs` (dropdowns, suggestions), `dates.mjs`, `verify.mjs` (after-run checks), `playbooks.mjs`. `scripts/sessions.mjs`: browser sessions. `scripts/tabs.mjs`: tab memory and the "Opening tabs" restore.
- `scripts/display.mjs`: private virtual screen (Xvfb) for a Linux machine without a screen.
- `scripts/testenv.mjs`: every `PAIRBROWSE_TEST_*` switch the helper reads, listed in `SWITCHES` and read through `testOn`/`testMs`/`testValue` (never `process.env` directly), plus the test gate (`scripts/test-gate.mjs`, imported first by `daemon.mjs`, removes them unless the test harness started the helper).
- Homes and instances: `scripts/home.mjs` picks this process's home (named instances via `PAIRBROWSE_INSTANCE`, no side effects, so `open.mjs` can call it before `paths.mjs` loads); `scripts/install-lock.mjs`: one install at a time in the shared installs' home (runtime, browser app, engine pack, cloudflared); `scripts/opener.mjs` (the record a hand-opened browser finds `open.mjs` by) + `scripts/openers.mjs` (the opener registry, one record per instance) + `scripts/open.mjs` (fixed: what the browser runs when opened by hand); `scripts/focus.mjs` (tab changes on macOS keep the app you were using in front).
- `scripts/paths.mjs`: every path under `~/.pairbrowse` (`paths.*`, `sessionProfile`, `sessionFile`) and `config.json`'s defaults; no other module joins onto the home (a test checks).
- `scripts/util.mjs`: shared helpers (JSON files and the one atomic write `writeJsonAtomic`, SHA-256, `sleep`, `pidAlive`, `urlHost`/`urlOrigin`, pinned downloads, timeouts); use them rather than local copies. `within(ms, promise)` resolves `null` when the time is up (callers handle null); every await on a page (evaluates on element handles, key presses, locator reads) must be bounded: a page whose scripts never pause answers nothing.
- Also at the top: `popups.mjs` (dialogs and pop-ups handled by the browser), `upload.mjs` (`pairbrowse_upload`), `facts.mjs`, `motion.mjs` (the pointer's hand-like path), `ws.mjs` (the join push channel's WebSocket), `runtime.mjs` (the pinned runtime install), `engine.mjs` + `driver.mjs`, `collaboration.mjs`, `dock.mjs` + `scripts/dock/` (the desktop-app pane on macOS). `scripts/dev/`: developer tools only (window preview, UI look checks, surface references), never loaded by the helper.
- Moved modules leave a small re-export at the old path for one release (`policy.mjs`, `secrets.mjs`, `clickrule.mjs`, `join.mjs`, `tabsync.mjs`, `devshare.mjs`, `relay.mjs`, `tunnel.mjs`, `daemon-connect.mjs`, `native-check.mjs`, `macos-app.mjs`, `daemon/session.mjs`, `daemon/tools.mjs`, `daemon/contexts.mjs`); new code imports the new home.
- `test/`: unit tests in `test/*.test.mjs` (no browser, no helper child); `test/integration/` (a real helper and browser, skipped without `PAIRBROWSE_TEST_RUNTIME`); `test/optin/` (each behind its own switch: real tunnel, Docker, a native build); `test/fuzz/*.test.js`; `test/helpers/` (`home.mjs` the home guard every test imports first, `util.mjs` sleep/until/text/rpc, `daemon.mjs` start and stop a helper, `live.mjs` + `live-helper.mjs` the live harness, the UI page harness). `test/bench.mjs` is a benchmark, not a test.
- `runtime/`: pinned `@playwright/mcp` with lockfile, installed with `npm ci --ignore-scripts`.
- `skills/pairbrowse/SKILL.md`: the reference behind the core (tool details, edge cases). The step-by-step task skills (`pairbrowse-signup`, `-listing`, `-test-site`, `-together`) sit next to it in `skills/`; `scripts/validate-skills.mjs` (run by `test/skills.test.mjs`) checks every skill names only real tools.
## Checks
```bash
npm test # unit (test/*.test.mjs) + integration (test/integration/); integration skips without
# PAIRBROWSE_TEST_RUNTIME=~/.pairbrowse/runtime; native launch tests: PAIRBROWSE_TEST_ENGINE=~/.pairbrowse/engine
npm run test:unit # unit only; npm run test:live: integration only (helper and browser per file)
npm run test:optin # test/optin/: each skips without its own switch (PAIRBROWSE_TEST_REAL_TUNNEL=1, PAIRBROWSE_TEST_DOCKER=1,
# a native build in PAIRBROWSE_TEST_EXECUTABLE); every PAIRBROWSE_TEST_* is in docs/development.md
npm ci && npm run lint # ESLint (eslint.config.js), no warnings; CI runs it
npm run test:fuzz # fast-check fuzzing of the security checks (test/fuzz/*.test.js: .js so Scorecard sees it)
claude plugin validate .
# Codex: install into a throwaway CODEX_HOME (codex plugin marketplace add . && codex plugin add pairbrowse@pairbrowse)
```
## Rules
- Security is the product. Don't add a TCP debugging port, unrestricted file access, the
run-code or WebMCP tools, or a CAPTCHA solver. Keep the Security table in `docs/security.md` true: update
it in the same change as any behaviour it describes.
- Hooks and the daemon use only Node's standard library; new runtime dependencies go in
`runtime/` with a regenerated lockfile.
- A version bump (package.json and both plugin manifests) also adds its entry to `CHANGELOG.md` and an
annotated tag `vX.Y.Z` on that commit, pushed with it.
- Never commit anything from `~/.pairbrowse` (profile, secrets, facts, runs, logs).