Imported from ianb/beebox (
beebox-clerk/AGENTS.md). Install upstream withnpx skills add ianb/beebox --skill beebox-clerk. Copyright stays with the author.
Bee Box Clerk
Chrome (MV3) extension companion for Bee Box. Clerk is a surface, not
an engine: it routes content (memos, saved pages, tab snapshots) into a
box's clerk API (the clerk tRPC router,
beebox/src/webapp/trpc/routers/clerk.ts); the box does the
thinking. Design record of the 2026 refresh:
beebox/docs/implemented-plans/refresh-clerk.md.
How association works
The box frontend advertises itself via a <meta name="beebox"> tag
(useBoxIdentityMeta); the popup reads it from the active tab, validates it
(same-origin spoof guard), and offers to enable the box. Enabling requests a
per-origin host permission — that prompt is the consent. Enabled boxes can
span servers; one is active and receives all actions (save/do page, memo,
tab sync, context-menu save).
Layout
wxt.config.ts— WXT config; the manifest is generated from here (no hand-written manifest.json).src/entrypoints/— WXT entrypoints, kept thin:background.ts,commentary-capture.content.ts(Defuddle+DOMPurify+Turndown extraction shim, plus the page-freeze capture),popup/,sidepanel/,options/(the settings page — the popup's gear opens it viachrome.runtime.openOptionsPage();open_in_tabis set by a<meta name="manifest.open_in_tab">in its index.html, since WXT generatesoptions_uiitself and overwrites a wxt.config manifest key).src/platform/— browser-API and DOM code:clerk-api.ts(clerk tRPC HTTP client),config-storage.ts(chrome.storage config load/save, clears legacy Dropbox-relay keys),detect-box.ts(reads the box-identity meta from the active tab),enable-box.ts(requests the per-origin host permission and persists the box),open-box.ts(navigates to a box's chat, reusing a tab already on that box),extract-readable.ts(Defuddle isolates content, DOMPurify sanitizes, Turndown converts to markdown), andfreeze-page.ts(single-file-core page freeze into self-contained HTML).src/domain/— pure logic (config schema, URL building) with tap tests.src/ui/— React components.public/— static assets copied into the build (icons).
This layering follows the readntalk precedent: domain code never imports React, WXT, or chrome.*.
Commands
pnpm dev— WXT dev mode (opens a Chrome profile with the extension loaded, HMR).pnpm build— production build todist/chrome-mv3/; load that directory unpacked via chrome://extensions.pnpm test/pnpm typecheck/pnpm lint— tap, tsc, eslint. All must stay green; the root pre-commit hook runs lint-staged + typecheck.wxt prepare(runs on install) generates.wxt/types;tsconfig.jsonextends.wxt/tsconfig.json.
Auto-build on main. When a commit/merge lands clerk source changes on
main, the root post-commit/post-merge hooks run build/auto-build.sh,
which rebuilds dist/chrome-mv3/ in the background (log:
.last-build.log). It's gated on beebox-clerk/ actually changing, so
box-only commits don't trigger it. Chrome picks up the refreshed unpacked
build automatically the next time the extension reloads.
Conventions
Lint/style comes from @ianbicking/personal-vibe-check (workspace package) —
the eslint config is vibeCheck({ react: true }) with no overrides; fix
code, never weaken rules (see monorepo AGENTS.md). Monorepo root owns git
hooks; this package has no husky setup.
Box relay (Track C — silent captureVisibleTab)
An enabled box's frontend can ask for a real screenshot (no picker) via a page↔extension relay. Wiring:
src/entrypoints/box-relay.content.ts— dynamically-registered content script (registration: "runtime", injected viachrome.scripting.registerContentScripts, NOT in the manifest). It holds no authority: it verifies the page is an enabled box page, announcesrelay-ready, answers arelay-ping, and forwardscapture-requestto the background.src/platform/relay-registration.ts—syncRelayRegistration(config)registers/updates/unregisters the script with match patterns derived from each enabled box's full boxUrl. Called on enable, disable, and background startup (drift reconciliation);persistAcrossSessions: true.src/domain/relay-auth.ts—boxUrlToMatchPatterns/isUrlUnderBoxUrl/captureErrorReason(pure, unit-tested).src/domain/relay-messages.ts— the postMessage + runtime message shapes and guards (pure, unit-tested).background.tshandleRelayCapture(sender)— the real gate. Usessenderonly to identify the tab id, then re-queries the live tab (chrome.tabs.get) and authorizes on its CURRENT url (under an enabled boxUrl, full origin incl. port) — never the send-timesender.tab.urlsnapshot, which a same-document navigation can move off the box (TOCTOU). Before AND aftercaptureVisibleTabit re-checks that the tab is still active and still under the SAME box; quota→busy. Stateless across messages (MV3 workers die unpredictably).
Match-pattern semantics (important): Chrome match patterns cannot express a
port — a pattern's host is portless and matches ALL ports. On the dev router
many boxes share localhost:3210 and are distinguished only by path, so the
content script is path-scoped (http://localhost/main/test1/*) and the
background's port-aware isUrlUnderBoxUrl prefix check is the real
authorization boundary. See the module comments.
Driving the extension in a browser: BROWSE_CLERK=1 bin/browse loads the
test build in dist-browse/ (pnpm build:browse), rebuilt when a source is
newer; close the browser to load a rebuild. It grants http://localhost/* at
install, since automation cannot click Chrome's permission prompt or the
toolbar button. bin/browse clerk-popup <tab> opens the popup acting on that
tab. dev/clerk-send/feed.html is a synthetic feed page to send.
Manual verification of the relay (the chrome. paths are verified by hand):*
- Run a headed
BROWSE_CLERK=1 bin/browsesession. - Open a worktree box page (e.g.
http://localhost:3210/<worktree>/test1/) and enable it from the popup (grants the per-origin host permission). - At
chrome://extensions→ the clerk's service worker → confirm a registered content script (chrome.scripting.getRegisteredContentScripts()in the worker console showsbeebox-clerk-box-relaywith the box's match patterns). - In the box page's devtools console, probe the relay:
addEventListener("message", (e) => e.data?.source === "beebox-clerk-relay" && console.log("relay:", e.data)); postMessage({ source: "beebox-app", type: "relay-ping" }, location.origin); // → relay-ready postMessage({ source: "beebox-app", type: "capture-request", correlationId: "1" }, location.origin); // → capture-response {ok:true,dataUrl} - Switch to another tab immediately after sending a capture-request → expect
{ ok: false, reason: "not-capturable" }. Fire >2 requests/sec → expect{ ok: false, reason: "busy" }. Try the same postMessage on a non-box page (relay not injected there) → norelay-ready, no response. - Disable the box → the registration disappears (
getRegisteredContentScriptsempty).
Auth model (for Tracks B/C)
No credentials are stored in the extension. The user's browser session with
the box (bbx_session cookie) is the authentication; fetches use
credentials: "include" plus a per-origin host permission granted when the
user enables a box. Enabling happens in-situ: the box's frontend emits a
beebox identity meta tag, the popup detects it on the active tab and
offers to enable.
