forge-extension
Forge: browser extensions (Chrome/Edge MV3): chrome extension, manifest v3, service worker, content script, permissions, CSP, Web Store, store policy.
SKILL.md.claude/skills/forge-extension/SKILL.md
GitHubForge playbook — Browser extension (Chrome MV3)
build-boss leads; security-boss co-owns the permission + messaging + CSP surface (this is a security-heavy domain and a real Codex/security trigger). Popup/options UI defers to forge-website (real content, responsive, a11y); anything that calls a backend defers to forge-integration. Scope is Chrome/Edge Manifest V3 unless the owner asks for Firefox/Safari (those are separate ports). The honesty rule: an extension that "loads unpacked" must have its core flow actually exercised, and every permission requested must be one the code genuinely uses.
Hard rules (non-negotiable)
- Least-privilege permissions. Request only the
permissions+host_permissionsthe code actually uses; preferactiveTabandoptional_permissionsover a blanket<all_urls>. Each permission is justifiable in one sentence. Over-broad scope is both a Web Store rejection risk and a user-trust failure. - No remotely-hosted code (MV3 policy). All executable JS ships inside the package — no
eval,new Functionon remote strings, or<script src>from a CDN. Remote code is a hard Chrome Web Store rejection. - Safe content-script + messaging. Content scripts run in the page's hostile context: validate every
chrome.runtime.onMessagepayload and checksender; never trust orevalwindow.postMessagedata from the page; keepexternally_connectablenarrow.web_accessible_resourcesscoped to specific matches, never<all_urls>. - Strict CSP kept strict. MV3's default extension CSP (
script-src 'self') is not weakened — no'unsafe-eval', no'unsafe-inline', no remote script sources. Sandboxed pages only if genuinely required, narrowly. - Termination-resilient service worker. The MV3 background is an event-driven service worker, not a persistent page: listeners registered at top level, no reliance on in-memory global state surviving, durable state persisted to
chrome.storage. - No secrets in the package; store-policy compliance. No API secret baked into the extension (it's extractable); real secrets sit behind a backend, OAuth via
chrome.identitywith minimal scopes. Single clear purpose, accurate description, privacy disclosure if user data is handled, no obfuscated code.
Team (conditional by stack/level)
Lead: build-boss. Security: security-boss (permission surface, CSP, message validation, no-remote-code — the defining risk of this domain). Support: typescript-reviewer (background/content/messaging code), test-boss (load the unpacked extension in Playwright/Puppeteer for E2E), ui-boss (popup/options UI via forge-website). Backend calls: forge-integration team.
Skills / commands / MCP
Chrome Extension MV3 docs via Context7 (manifest v3, service-worker lifecycle, messaging + storage + identity APIs — get the current API, MV2 patterns are dead). forge-website for the popup/options page UI; forge-integration if it talks to an API; systematic-debugging for service-worker lifecycle bugs (the "why did my listener not fire after idle" class). browser skill / Playwright to load the unpacked build and drive the core flow. No special MCP required. Reuse a bundler (esbuild / Vite / webpack) with an MV3/CRX plugin — don't hand-assemble the package.
Fan-out & flow
L2 a single-purpose extension (popup + one content script); L3 a service worker + multiple content scripts + options page + messaging + a backend. Serial: manifest + permission model → service worker + messaging contract → content scripts → popup/options UI → CSP + security review → load-unpacked E2E → production zip. Parallel: content script ∥ popup/options UI ∥ background logic build in parallel once the message contract is fixed (the message schema is the seam).
Domain gates
- Manifest requests only permissions/
host_permissionsthe code uses;activeTab/optional preferred over<all_urls>(each justified). - No remotely-hosted code — no
eval/new Function/remote<script>; all JS bundled. - Runtime + content-script messages validate
sender+ payload; page-supplied data is nevereval'd;externally_connectableandweb_accessible_resourcesscoped to specific matches. - Default MV3 CSP kept strict (
script-src 'self', nounsafe-eval/unsafe-inline). - Service worker is event-driven and termination-resilient — listeners at top level, durable state in
chrome.storage, no global-state reliance. - No secrets in the package; OAuth via
chrome.identitywith minimal scopes. - Extension loads unpacked and the core flow works (evidence: load + a screenshot/log), and a production zip is produced.
Ship-readiness (unique)
Manifest permissions minimal and each justified; no remote code / eval; messaging validates sender + payload; MV3 CSP strict; service worker termination-resilient (state in chrome.storage); no secrets in the package; extension loaded unpacked with the core flow exercised (real output attached) and a production zip built. Store-listing prerequisites — single-purpose description, privacy policy if user data, per-permission justifications, screenshots — are noted as owner action (submission itself needs a developer account and is owner-gated). Firefox/Safari ports are out of scope unless requested. Advisory checklist; running security-boss / codex-reviewer on the permission + messaging surface is strongly recommended here — not a blocker, but say so if it was not run.