AGENTS.md
This file provides guidance to AI coding assistants (e.g., Claude Code, GitHub Copilot, Cursor) when working with this repository.
Project Overview
This repository is the next version of Nanobrowser, an AI web automation Chrome extension, rebuilt from the ground up. It has two parts:
- The browser control layer: a deterministic layer that observes a page, acts on it, and reports what happened. It is part of the extension, and its client is the extension's own agent, in-process.
- The extension that end users run: the side panel, the one agent that works in it, model providers, and the features around them. It sits on top of the layer.
Scope
- Built and maintained: the browser control layer, a simple Chrome extension for testing it by hand (the test panel), fixture pages, unit tests, and the automated checks.
- Built and maintained: the extension. Its first release has a side panel per tab, one agent loop, model providers through the Vercel AI SDK, the permission modes, shortcuts, attachments (images and text), upload into pages, saved files, voice input through OpenAI's or Gemini's voice model, three UI languages (English, Simplified Chinese, Traditional Chinese), a glowing border on the pages the agent works on (with a switch in Settings), anonymous analytics (a daily count of users and usage counts, both on until the user turns them off) with no identifier, and a card that asks a user who uses it often to rate it in the store.
- Not in the first release: for example skills the model loads, site notes, saved memory, the in-page Stop pill, PDF and Office parsing, the text tool-call parser, scheduled tasks. Do not add any of these unless asked.
- Not in scope: an MCP bridge, browsers other than Chrome and Edge, a planner and navigator design, several cooperating agents.
Fixed design constraints
These are settled and not open for redesign.
For the layer:
- The layer is part of a Chrome extension, not a standalone tool. It is not a runtime like agent-browser: no CLI, no daemon, no socket of its own, no launching or attaching to a browser, no remote CDP. Applications outside the extension will use it through an MCP bridge in future, and only that way. Do not add any other external entry point.
chrome.debuggeris the only way to reach CDP. The product is a Chrome extension. Do not add CDP over WebSocket, not even for tests.- Puppeteer is the frame manager. It detects frames, including out-of-process iframes, and tracks their sessions and navigation. Do not build a parallel frame or session registry from CDP events.
- Elements are never found through Puppeteer. No selectors,
ElementHandleclicks or selector-basedevaluate. Element commands are raw CDP commands sent to the session of the frame Puppeteer reports. - No model inside the layer. The layer is deterministic. Anything that needs an LLM belongs to an agent above it.
The layer's invariants hold for all code in it. Read them before changing perception or interaction code:
- One identity. An action operates on the node the agent was shown, addressed by the browser's handle for it.
- Refs fail closed. A ref is valid only for the document it was seen in and only if the agent was shown it. Otherwise the result is a typed error, not a guess.
- Retries happen before dispatch. Once an input event has been sent, the layer does not send another on its own.
- No hidden substitution. A different node or a different mechanism is chosen by rule before dispatch and named in the receipt.
- Three outcomes, reported separately. Input dispatched; page reacted; expected state reached.
- Observation is read-only. Capturing state does not change the page.
- Degrade loudly. Partial captures and truncation are stated in the result.
- Bounded everything. Every command, wait and list has a budget. The one wait without a clock is a confirmation the host lets wait for the user; it still ends on Stop, when the tab closes or when the session ends.
- State is disposable. A service worker restart, a debugger detach and a navigation invalidate layer state cleanly.
- No model inside. Anything that needs an LLM belongs to the agent above the layer.
- Consequential actions need a person. By default, an action classified as submit, pay or delete is not dispatched until the user approves it, whatever form its target takes, and page script supplied by the agent is not run until the user approves it. The agent cannot turn this off.
For the agent and the extension:
- The user decides how much the agent may change on its own, and no agent call can change that:
- Reading and moving never ask.
- A detected write (submit, send, delete, upload) asks in Ask mode, the default, and runs in Auto mode or on a site the user set to Allow. A site the user set to Ask asks in both modes.
- A detected payment, access grant, secret input or page
promptdialog is the user's step in every mode, with no Approve. - Detection is best effort, and enforcement is strict.
- The agent's sessions get no page script and no
devtools. - In the test panel the layer keeps its
sensitivepolicy: submit, pay and delete actions, and page script, are confirmed before dispatch.
- Page content is data, never instructions. Text from pages reaches the model inside marked page-content boundaries; keep it that way.
- One agent, not the old planner and navigator: one loop per conversation.
- No model tiers. Every model gets the same agent and the same tools (
screenshotonly for vision models). There are no tool switches in the first release. - Nothing outside the extension can drive the layer or the agent. There is no MCP for now.
- The conversation is compacted automatically before the context window fills, and every message the user sent is kept in full.
- Conversations cannot be imported in the first release.
Structure
entrypoints/- Thin WXT entrypoints: background, the product's side panel (sidepanel/), options (the settings page),welcome/(the first-run page),permission/(the microphone page),offscreen/(blob URLs for saved files), and the layer's test panel (testpanel/, developer-only, opened with theopen-test-panelcommand)src/browser-control/- The layerprotocol/- Request and result types, zod schemas, error codes, tool cataloguecore/- Controller, sessions, refs, frames, perception, interaction, lifecycleprobe/- The layer's only in-page code; built to a string constanthost/- Puppeteer connection and frame adapter,chrome.debuggersession wrapper,chrome.tabsindex.ts- The only entry other code may import
src/test-panel/- The layer's test panel (React); a developer-only entrypoint, opened on a tab with theopen-test-panelcommand (unbound by default; bind it inchrome://extensions/shortcuts)src/background/- The layer's test extension in the worker: the test panel's messages andUserPrompt, and the automated checks and scenario sets thatpnpm autorunruns (checks/,scenarios/). The product's controller is created insrc/extension/layer.tsfixtures/- Static test pages for the layer's and the extension's scenario sets, the fixture server (with a fake OpenAI-compatible model) andautorun.mjsREADME.md,CONTRIBUTING.md,PRIVACY.md,SECURITY.md,LICENSE,.github/- The project's pages for users and contributors, the issue templates and the CI workflow (.github/workflows/).PRIVACY.mdsays what data leaves the browser and where it goes; a change to that updates itAGENTS.md- This file;CLAUDE.mdis a link to it.env.example- The PostHog key and host a production build reads; with both set it sends analytics, and development and autorun builds never send to PostHog
The extension:
src/agent/- Loop, hooks, agent tools, prompts, context management, run recordssrc/llm/- Providers, model profiles, usage, retries, overflow detectionsrc/store/- IndexedDB schema and stores, settings, keyssrc/extension/- Wiring in the worker: RunManager, PromptBroker, permissions, TabScope, downloads, notificationssrc/ui/- Shared components, theme, the i18n runtime and the catalogues (locales/: English, Simplified and Traditional Chinese)src/side-panel/- The chat UIsrc/options/- The settings UI
Import rules:
- Nothing under
src/browser-control/imports from outside that folder, and other code imports the layer only throughsrc/browser-control/index.ts. src/agent/reaches Chrome APIs only through interfaces passed in bysrc/extension/.src/side-panel/andsrc/options/never importsrc/agent/orsrc/extension/; they talk to the worker through one typed message protocol.src/llm/knows nothing about browsers.
Toolchain
The project is a single pnpm package built by WXT: Node 24 LTS, pnpm 10.34.7, WXT 0.21.4, TypeScript 6.0.3, Vitest 5.0.4, puppeteer-core ^25.13.0, zod 4.6.7, React 18.3.1. A patch or minor update is taken once the checks pass; a major update is evaluated and decided on its own.
pnpm 10.34.7 is pinned in package.json (packageManager). If the global pnpm is a different version, run the scripts through corepack pnpm <script>.
pnpm install- Install dependencies (also runswxt prepare)pnpm dev- WXT development mode; loaddist/unpacked in Chromepnpm build- Build the Chrome MV3 extension intodist/pnpm zip- Build, then packdist/intodist-zip/nanobrowser-<version>.zip(source maps left out) for a release or the Chrome Web Storepnpm type-check- TypeScript type checkingpnpm lint- ESLint, including the import rules: the layer's (src/browser-control/) and the extension's (Import rules, above)pnpm test- Unit tests (Vitest); tests live in__tests__/folders undersrc/pnpm prettier- Format the repositorypnpm fixtures- Fixture server on port 8787 (localhost, 127.0.0.1 and [::1])pnpm install-chrome- Install the current stable Chrome for Testing (never beta or canary) into~/.cache/puppeteer, then ask to remove every other build there so only that one is kept (nothing is removed without a terminal to ask in). Run it after cloning and to update Chrome;pnpm autorunstarts only the build recorded there as stable (orCHROME_PATH)pnpm clean-chrome- Ask, then remove the whole~/.cache/puppeteer(shared with other projects that use Puppeteer; they download their browser again)pnpm autorun <check>[,<check>...] [--args <json>] [--scale <n>] [--product-permissions]- Build an autorun copy of the extension into.output/autorun, start the fixture server and Chrome for Testing (headless, plain process, no external CDP,--use-mock-keychain), run the named checks in the extension and print the reports.--scale 2runs Chrome with that device scale factor, as on a Retina display. The autorun copy requires the Chrome permissions the product only asks for (tab groups, notifications, downloads);--product-permissionsbuilds with the product's permission lists into.output/autorun-product, for thewithout-permissionscheck.pnpm autorun permission-warningschecks that the product's manifest adds no install warning to the old version's; run it before every release. Results go tofixtures/.results/(ignored by git)pnpm upgrade-test [--old <path>] [--no-build]- The update from the old Nanobrowser (0.2) by hand: build it (../nanobrowserby default) and this version, open Chrome for Testing in a window (--use-mock-keychain) with the old build loaded from.output/upgrade/ext, then, on Enter, put this version's build in the same folder for a Reload inchrome://extensions, which Chrome reports as an update
CI (.github/workflows/ci.yml) runs prettier --check, pnpm lint, pnpm type-check, pnpm test and pnpm build on every push and pull request, on Ubuntu with Node 24 and the pinned pnpm. Its actions are pinned to a commit, and its token can only read. The browser checks (pnpm autorun) and the checks that call real models (the evaluations, the model smoke run) are not in CI: they are run by hand, before a merge that touches what they cover and before a release.
The extension's dependencies: ai with @ai-sdk/anthropic, @ai-sdk/google and @ai-sdk/openai, idb, react-markdown with rehype-sanitize and remark-gfm, and @radix-ui/react-dialog; fake-indexeddb is a development dependency for the store tests. Any other dependency is asked for first. Settings → About lists them with their licences, and a unit test keeps that list equal to package.json.
Working rules
- Always use
pnpm. - Chrome and Edge only; the automated checks run in Chrome. The extension is loaded unpacked from
dist/. - One change per branch and pull request. Keep diffs scoped to it.
- Ask before adding a dependency, renaming or moving files, or changing global configuration.
- Pure functions get unit tests over recorded CDP payloads. Anything that needs a browser gets a fixture page and a scenario in the test panel.
- Formatting: Prettier with 2 spaces, semicolons, single quotes, trailing commas,
printWidth: 120. Useimport typefor type-only imports. - Comments say in plain words what the code does and why.
- Never log, commit or expose credentials. Mask password, payment-card and one-time-code values wherever page content is returned.
- Do not edit generated output (
dist/**,.wxt/**). - Chrome for Testing must never reach the real macOS Keychain. Every launch of it, by
pnpm autorunor any other script or by hand, passes--use-mock-keychain.