This file provides guidance to coding agents (Claude Code, Cursor, Codex, Aider, etc.) when working with code in this repository.
This repo follows GitHub flow. For any new task, create a feature branch off
main, push it, and open a PR — do not commit directly to main.
Prototyping browser extension capabilities for improving browser use agent performance:
- Token efficiency
- Security
- Compliance: e.g., exposure to PII
- Accuracy
Ideas explored in this repository:
- Masking/Redacting sensitive information on the webpage
- Blocking/Modifying dark patterns on the webpage
- Preprocessing webpage content to be more agent-friendly, e.g., hiding irrelevant content, hiding user-generated comments which could contain prompt-injection attacks, etc.
Load-bearing decisions are recorded as MADR 4.0
ADRs under decisions/. Read the index
before making changes that touch a recorded decision — rule taxonomy, frame
execution, DOM markers, the lib/ ↔ rules/ boundary, SPA re-scan,
scrub-don't-detach, shadow-DOM coverage, rule defaults, telemetry posture,
injection-pattern handling, lint stack, background-worker purity, CSS-first
hide, and the release flow.
- Upload extension for use with browserbase: browser-extensions
- Chromium Extension: Manifest V3, TypeScript, bun (bundling) assume Chrome 148+ for modern JS features
- Python: use
uvfor all package management
Two linters run on extension/: Biome owns formatting plus its recommended rule
set; ESLint runs only rules Biome doesn't have. The split is mechanical — do not
duplicate a rule between them.
- Biome config:
extension/biome.json. - ESLint config:
extension/eslint.config.js(flat config) — pulls in@eslint/js,typescript-eslint, andeslint-plugin-unicorn, then disables unicorn rules that overlap with Biome or are too opinionated for this repo. - Custom rules:
extension/eslint-rules/*.js. Each rule is one file, exported viaextension/eslint-rules/index.jsunder theagent-browser-shield/<rule-name>namespace. Add a rule by dropping a file ineslint-rules/, exporting it fromindex.js, and enabling it ineslint.config.js. bun run checkruns Biome then ESLint;bun run check:fixruns both with--fix/--write.
demo-site/ has a smaller ESLint config (demo-site/eslint.config.js) that
just enforces unicorn/prevent-abbreviations so the React surface stays
grep-friendly with the same naming conventions as the extension. The replacement
allowlist (props, ref, args, loop counters, etc.) mirrors the extension's.
bun run check in demo-site/ runs Biome + ESLint.
Rule IDs follow <target>-<verb> and the verb names what the rule does to the
DOM. Five canonical verbs:
annotate— adds agent-readable info; page content unchanged (cart-addon-annotate,roach-motel-annotate)hide— visually conceals viadisplay: none; element stays in DOM (ads-hide,cookie-banner-hide,chat-widget-hide,newsletter-modal-hide)redact— replaces content with a click-to-reveal placeholder (pii-redact,secrets-redact,comments-redact,prompt-injection-redact)sanitize— keeps the element and cleans attributes/text/state (json-ld-sanitize,attribute-injection-sanitize,confirmshame-sanitize,checkout-checkbox-sanitize)strip— removes the agent-readable content from the DOM. Most strip rules blank the data carrier (attribute value, text node, comment data) rather than detach the element, so SPA frameworks (React 19 metadata, Vue Teleport, Astro view transitions) keep live references to the rendered node and reconcile cleanly on route change.svg-sprite-stripis the exception — its targets are inline sprite definitions the page framework does not own, so it detaches the element outright. (noscript-strip,html-comment-strip,hidden-text-strip,svg-sprite-strip,svg-text-strip,meta-injection-strip)
-helper is reserved for non-defensive agent affordances (search-url-helper).
See CONTRIBUTING.md → Adding a new rule → Rule ID naming for the longer
write-up and the hide-vs-redact decision rule.
Rule apply runs once at document_idle. Client-side route changes in SPAs
(React Router, Vue Router, etc.) swap subtrees in and out without a new page
load, so anything that only ran in apply will never see post-navigation
content — and most of our targets (PII, secrets, scarcity badges, hidden text)
are exactly the kind of late-mounted content SPAs are built on.
Default to building any rule that mutates the DOM with the createScanRule
factory (extension/src/lib/scan-rule.ts). Pass it the rule's id / label /
description, a scanAndX(root) function, and skipPlaceholderSubtrees: true
when the rule inserts placeholders; the factory owns the watcher lifecycle — the
shared createSubtreeWatcher, the apply that scans then starts it, and the
teardown that stops it — so callers don't re-hand-roll that skeleton (and
can't forget the teardown or the placeholder skip). scarcity-redact,
json-ld-sanitize, and hidden-text-strip are representative call sites.
Reach for createSubtreeWatcher directly only when the lifecycle is more than
scan-on-apply-and-rescan: a second watcher / head router
(meta-injection-strip), a route / focus subscription
(form-prefill-annotate), a deferred snapshot reconcile
(countdown-timer-redact), an onSubtrees that re-scans document.body
instead of the surfaced root (cross-origin-frame-redact, svg-sprite-strip),
or a teardown that restores page state (confirmshame-sanitize). Text-node
redaction rules (pii-redact, secrets-redact, encoded-payload-redact) have
their own factory, defineInlineTextRedactRule. Skip a watcher entirely only
when there is nothing to re-scan after initial load — e.g., a one-shot landmark
injection — and call that out in a comment.
Every data-abs-* attribute the engine or any rule writes onto the page is
declared and exported from extension/src/lib/dom-markers.ts — the single
canonical registry. Engine-level markers are named <PURPOSE>_ATTR; per-rule
markers are <RULE>_<PURPOSE>_ATTR. Import the constant; do not inline the
literal. An ESLint no-restricted-syntax rule (eslint.config.js) blocks raw
"data-abs-…" string and template literals everywhere except the registry
itself, so name collisions and convention drift surface at lint time.
The initial enabled/disabled state for each rule lives in
extension/src/rules/rule-metadata.ts, not on the rule modules themselves. This
hand-edited file is the source of truth for RuleId, RULE_IDS, and
RULE_DEFAULTS; extension/src/lib/storage.ts imports it. It is kept out of
rules/index.ts so the service-worker bundle doesn't pull rule files' top-level
DOM access. Adding a rule means appending an entry both here and in
rules/index.ts; the catalog test in
extension/src/rules/__tests__/catalog.test.ts enforces that the two stay in
sync.
extension/build.ts accepts a --defaults <path> CLI flag (or
EXTENSION_DEFAULTS_FILE env var) pointing at a JSON file in the same shape as
the Options-page export. Validated overrides are injected into the bundle via
process.env.EXTENSION_DEFAULT_OVERRIDES. Override only affects fresh
chrome.storage; existing user toggles persist.
Selectors and URL recipes for individual sites live in
extension/data/sites/*.yaml, not inline in the rule TS files. The codegen in
extension/scripts/build-site-data.ts validates each YAML against the zod
schema at extension/data/site-rules.schema.ts and emits
extension/src/rules/site-data.generated.ts, which the rule files import.
The generated file is committed (matching the easylist-generic.generated.ts
precedent). bun run build invokes codegen automatically; for a manual run use
bun run build-site-data. To add or change site coverage, edit the YAML and
rebuild — do not edit the generated file.
Regex sources for prompt-injection-redact live base64-encoded in
extension/data/injection-patterns.yaml. The codegen in
extension/scripts/build-injection-patterns.ts decodes them and emits
extension/src/rules/injection-patterns.generated.ts with plaintext RegExp
literals — this keeps the shipped bundle free of atob-decoded strings (which
Chrome Web Store review treats as obfuscated code) while still keeping literal
adversarial phrasing out of files a coding agent is likely to read.
The generated file is committed and bun run build regenerates it; for a manual
run use bun run build-injection-patterns. To add or change a pattern, edit the
YAML and rebuild — do not edit the generated file.
Benchmark runs accumulate under output/results/<run_id>/ (raw events, traces,
manifest) and output/reports/<run_id>*.html (diff viewers). Both directories
are gitignored. To prune old artifacts:
uv run scripts/clean_artifacts.py # dry-run, keeps 3 most recent
uv run scripts/clean_artifacts.py --keep 5 --apply
uv run scripts/clean_artifacts.py --orphans-only --apply # only HTMLs whose run is goneDry-run by default; pass --apply to actually delete. See the script's --help
for the full flag list.
When adding, removing, or renaming a defense rule (anything in
extension/src/rules/ registered in extension/src/rules/index.ts), update
skills/agent-browser-shield-config/SKILL.md so its rule ID list stays in sync.
If the change also affects DOM markers or required agent behavior, update
skills/agent-browser-shield/SKILL.md as well.
When changing the trace bundle layout, file names, or step schema produced by
scripts/build_traces.py, update
skills/agent-browser-shield-diagnose/SKILL.md so the diagnostic workflow it
describes still matches what's on disk.
When changing the site-rule schema (SITE_DATA_RULE_IDS in
extension/data/site-rules.schema.ts) or the Playwright MCP setup
(.mcp.json), update skills/agent-browser-shield-site-rules/SKILL.md so its
rule-type list and workflow stay in sync.
When changing build-time inputs in extension/build.ts (CLI flags, env vars,
define: substitutions) — especially anything that affects how operators
configure a deployed build — update
skills/agent-browser-shield-install/SKILL.md and
docs/src/content/docs/install.md so the build-time customization workflow
stays accurate.