AGENTS.md
September 8, 2026 · View on GitHub
Table of Contents
- Project Overview
- Technical Context
- Project Structure
- Build And Test Commands
- Contribution Instructions
- Code Guidelines
Project Overview
Popup Blocker by AdGuard is a userscript that blocks unwanted pop-up
and pop-under windows in web browsers. Instead of using filtering rules,
it wraps browser APIs (window.open, .click(), .dispatchEvent(),
etc.) with an additional verification layer that only allows calls
triggered by genuine user input. It works in any browser that supports
userscript managers (Tampermonkey, Violentmonkey, Greasemonkey, or
AdGuard).
The project also includes a standalone options page (Preact SPA) that lets users manage allowlisted and silenced domains.
Technical Context
- Language: TypeScript (ES2015 target), compiled with Rollup
- Package Manager: pnpm 10.x (>=10.33.4 <11), Node.js >= 22
- Primary Dependencies: Preact (UI),
@adguard/translate(i18n), tslib - Storage: Greasemonkey userscript API (
GM_getValue/GM_setValue) - Testing: Mocha + Chai (browser-based test runner)
- Linting: ESLint (airbnb-typescript config) + Markdownlint
- Target Platform: Web browsers via userscript managers; the options page is deployed as a static site via GitHub Pages
- Project Type: Userscript (browser extension–like)
- Performance Goals: N/A — the script must be invisible to page scripts and add negligible overhead to page load
- Constraints: Must not be detectable by other scripts on the page; must work across all major browsers
- Scale/Scope: Public userscript used by AdGuard users worldwide
Project Structure
├── AGENTS.md # AI agent guidelines (this file)
├── README.md # Project overview and setup
├── CHANGELOG.md # Release history
├── DEVELOPMENT.md # Developer setup and workflow guide
├── package.json # Dependencies and npm scripts
├── tsconfig.json # TypeScript configuration
├── rollup.config.ts # Rollup build configuration
├── postcss.config.ts # PostCSS plugin setup
├── .eslintrc.js # ESLint configuration
├── .markdownlint.json # Markdownlint configuration
├── exclusions.ts # AdGuard exclusion websites
├── locales.ts # Locale upload/download script
├── .github/workflows/ # GitHub Actions CI/CD workflows
├── src/
│ ├── main.ts # Core orchestration — wires all layers
│ ├── on-blocked.ts # Blocked-popup notification logic
│ ├── mock-window.ts # Mock window for blocked popups
│ ├── page-script-namespace.ts # Global `adguard` namespace
│ ├── init/ # Entry points (userscript, page-script)
│ ├── dom/ # Browser API wrappers (open, click, …)
│ ├── events/ # Event verification and analysis
│ ├── proxy/ # Proxy/interception service layer
│ ├── timeline/ # Heuristic timeline engine
│ ├── observers/ # DOM mutation and overlay observers
│ ├── messaging/ # Cross-frame message hub
│ ├── storage/ # Settings DAO and GM API wrapper
│ ├── pages/ # Options and notification pages (Preact)
│ ├── ui/ # Alert and toast UI components
│ ├── theme/ # Light/dark theme resolution and persistence
│ ├── i18n/ # Internationalization utilities
│ ├── shared/ # Protected APIs, utilities, constants
│ ├── types/ # TypeScript type definitions
│ ├── assets/ # Fonts and icons
│ └── locales/ # Source and translated strings (JSON)
├── tasks/ # Build scripts and metadata generation
│ ├── builder.ts # Build orchestrator (rollup invocations)
│ ├── metadata/ # Userscript metadata plugin
│ └── ... # Channels, paths, constants, utils
└── test/ # Unit tests (Mocha + Chai)
├── index.ts # Test entry point
├── events/ # Event verification tests
├── pages/ # Options-page hook tests
├── storage/ # Storage migration tests
├── theme/ # Theme helper and mirror tests
├── timeline/ # Timeline heuristic tests
├── shared/ # Shared utility tests
└── mocks/ # GM API and other mocks
Build And Test Commands
The full list of build, test, and lint commands lives in DEVELOPMENT.md. The commands that MUST be run before finishing a task are listed in the Contribution Instructions below.
Contribution Instructions
-
You MUST verify your changes with the linter and type checker.
Use the following commands:
pnpm lintto run ESLintpnpm lint:mdto run Markdownlint on Markdown filespnpm userscript-devto check for TypeScript compilation errors
-
You MUST update the unit tests for changed code.
-
You MUST build the tests with
pnpm testsand verify manually in a browser that your changes do not break existing functionality. -
When making changes to the project structure, ensure the Project Structure section in
AGENTS.mdis updated and remains valid. -
If the prompt essentially asks you to refactor or improve existing code, check if you can phrase it as a code guideline. If it's possible, add it to the relevant Code Guidelines section in
AGENTS.md. -
After completing the task you MUST verify that the code you've written follows the Code Guidelines in this file.
-
Commit messages MUST start with the ticket number (
AG-XXX) so they auto-link with the task tracker, followed by a short description in the present tense (e.g.AG-1234 Fix login redirect). Automated commits made by CI (e.g. the CHANGELOG finalization in the release PRs) use a Conventional Commits prefix such asdocs:instead.
Code Guidelines
System Design
Design for a browser extension (userscript):
- The script runs in a sandboxed environment with limited APIs —
interact with the page only through the Greasemonkey/Tampermonkey
API and standard DOM APIs. Do not assume access to browser
extension APIs (e.g.,
chrome.*,browser.*). - Keep the script lightweight — every added size slows down page load. Avoid bundling large dependencies.
- Separate concerns across contexts: the userscript entry
(
src/init/userscript-entry.ts) bootstraps the environment; the page script (src/init/page-script.ts) runs in the page's JS context and wraps native APIs; the options page is a standalone Preact SPA. Do not put business logic in the options page; it communicates with the userscript via message passing. - Handle lifecycle correctly — the userscript is injected once per
page load. Capture references to native APIs immediately in
src/shared/protected-api.tsbefore page scripts can override them. - Use message passing (
InterContextMessageHub) between parent and child frame contexts; never share mutable state directly across frames. Treat each frame context as an independent process. - React to browser events asynchronously; never block the main thread of the page.
- The script MUST be invisible to other scripts — do not expose globals, do not modify observable behavior of native APIs beyond what is necessary for popup blocking, and ensure wrapped functions pass native-function detection checks.
Architecture
Universal design principles:
- Separation of Concerns — each module handles one aspect of
the system (e.g.,
src/dom/only wraps DOM APIs,src/timeline/only runs heuristics) - Single Responsibility Principle — every file, class, or function has one reason to change
- Dependency Direction — dependencies point inward/downward; never from lower layers to higher ones
- Explicit Boundaries — module interfaces are intentional; no reaching into internals of other layers
- Data Flow Clarity — data moves through the system in a predictable, traceable path (user event → DOM wrapper → event verification → timeline check → allow/block)
- Minimize Coupling, Maximize Cohesion — modules are self-contained and interact through narrow interfaces
- Make Invalid States Impossible — use TypeScript interfaces and enums to prevent illegal combinations at compile time
- Observability Built-in — the Timeline system and debug
logging (
DEBUGflag) provide built-in tracing in dev builds. Less critical in production where theDEBUG/RECORDflags are stripped. - Keep It Boring — prefer well-understood patterns (proxy wrapping, event verification) over clever or novel solutions
The easiest way to achieve these principles is layered architecture. This project's layers, from top to bottom:
Entry Points (src/init/)
↓
Core Orchestration (src/main.ts)
↓
DOM API Wrappers (src/dom/)
↓
Event Verification (src/events/) + Observers (src/observers/)
↓
Proxy Service (src/proxy/) + Timeline Engine (src/timeline/)
↓
Messaging (src/messaging/) + Storage (src/storage/)
↓
Theme (src/theme/) + Shared Utilities (src/shared/)
Upper layers may call lower layers. No layer may depend on a layer
above it. The UI layer (src/pages/, src/ui/) is a separate
vertical slice that sits alongside the core stack, depending on
Storage, Theme, and Shared but not on DOM wrappers or Proxy.
Code Quality
- Linting: ESLint with
airbnb-typescript/baseconfig. Indentation, line length, import style, and other formatting rules are defined in.eslintrc.js— refer to that file as the single source of truth. Do not disable or modify ESLint rules without team approval. - Error handling: Wrap external/untrusted code in
ProxyServiceExternalError; let internal errors propagate. The proxy layer catches and logs errors from page scripts without crashing the blocker. - Protected APIs: Always use the safe references from
src/shared/protected-api.ts(e.g.,call,apply, native constructors) instead of directly referencingFunction.prototype .calletc., because page scripts may override them. - No
anyin UI code: The pages ESLint config (src/pages/.eslintrc.js) warns on explicitany. In the core codebase,anyis allowed but should be avoided where possible.
Testing
- How to run: Tests use Mocha + Chai in a browser; see DEVELOPMENT.md for the build and execution steps.
- Coverage: No automated coverage gate; strive to test all heuristic logic (event verification, timeline checks) and storage migration paths
- What to mock: GM API methods,
window.event, browser APIs that are unavailable in test context - No E2E tests: The project has no end-to-end integration tests; unit tests focus on individual components in isolation
Dependency Management
- Pin all dependency versions explicitly — use exact versions
(no
^or~ranges) so untested upgrades cannot slip in. When pinning, never downgrade: the exact version inpackage.jsonmust be at least the version resolved inpnpm-lock.yaml, and the lockfile must be kept in sync withpnpm install. - Prefer vanilla solutions — use the language's standard library and built-in APIs when they adequately solve the problem. Only add a dependency when it provides significant value over a vanilla implementation.
- Reputable sources only — dependencies MUST come from well-established, actively maintained projects. Evaluate by download counts, repository activity, and known maintainers.
- Avoid unpopular libraries — do NOT add niche or obscure packages with limited community adoption. These pose security risks and may become unmaintained.
- Minimize dependency count — each new dependency increases attack surface, bundle size, and maintenance burden. Justify every addition. This is especially critical for a userscript that must stay lightweight.
- Use the latest stable version — when adding a new dependency, explicitly check the package registry for the latest stable release and use it. Do not copy outdated version numbers from memory, training data, or existing lock files of other projects.
Rationale: Fewer, well-vetted dependencies reduce security vulnerabilities, supply chain risks, and long-term maintenance costs.
Configuration & Documentation
- Runtime configuration: The userscript has no config files;
user settings (allowed/silenced domains) are stored via the
Greasemonkey storage API (
GM_getValue/GM_setValue). - Build-time configuration: The build channel and debug flags are described in DEVELOPMENT.md.
- Exclusions: See Managing
Exclusions in
DEVELOPMENT.md. - Locales: See Working with
Locales in
DEVELOPMENT.md. - Documentation updates: Changes to build commands, project
structure, or public API must be reflected in
README.md,DEVELOPMENT.md, and thisAGENTS.mdfile as appropriate. - Single source of truth: Do not duplicate content across docs —
README.mdowns the user-facing overview,DEVELOPMENT.mdowns the developer workflow, and this file owns the rules and guidelines. Reference the owning doc instead of repeating it. - No hardcoded secrets: The project has no secrets or API keys.
Markdown Formatting
All Markdown files MUST follow these formatting rules:
-
Line length: Keep lines at most 120 characters (matching the project's
.markdownlint.jsonline-lengthsetting). Lines inside fenced code blocks are exempt from this limit. -
Unordered lists: Use dashes (
-) for bullet points. Indent nested list items by 4 spaces. -
Emphasis: Use asterisks (
*) for emphasis (*italic*,**bold**). Do NOT use underscores. -
Headings: Duplicate heading names are allowed only among sibling headings (same parent level). Avoid duplicates across different levels.
-
Inline HTML: Avoid raw HTML in Markdown. The only allowed elements are
<a>,<p>,<details>,<summary>, and<img>. -
Trailing spaces: Do NOT leave trailing whitespace on any line. Do NOT use two-space line breaks — use a blank line instead.
-
Bare URLs: Bare URLs are permitted and do not need to be wrapped in angle brackets.
-
Table formatting: Align table columns with padding when the table fits within 120 characters. If the table exceeds 120 characters or triggers an MD060 linter warning, switch to a compact format using single spaces only. This applies to the separator row as well — it should be written as
| --- |, not|--|.Example of correct layout:
| Col1 | Col2 | | ------ | ------ | | Value1 | Value2 |Do NOT use extra padding or alignment characters beyond single spaces.
Rationale: Uniform Markdown formatting improves readability for both humans and AI agents that consume project documentation.
Other
- Preserve Keep a Changelog style in
CHANGELOG.mdand reference related issue numbers when known. - Do not update
CHANGELOG.mdfor changes that only affect CI or tests.