---
name: "browser_extension_publishing"
description: "Publish a browser extension without a store rejection: Manifest V3 package checks, least-privilege permission review with written justifications, listing copy and screenshot rules, privacy disclosures, and a rejection fix playbook keyed to real store violation IDs. Use when submitting or updating an extension on the Chrome Web Store, Firefox Add-ons (AMO), Microsoft Edge Add-ons, or Safari, when a submission was rejected, or when planning permissions before a build."
---

# Browser Extension Publishing

## Purpose

Get an extension through store review on the first submission, and fix a rejection fast when one lands. The work is mostly decided before upload: a Manifest V3 package with no remotely hosted code, the smallest permission set that still does the job, a listing that matches the code exactly, and reviewer notes that let a stranger test the whole product. This skill covers Chrome Web Store, Firefox Add-ons, Microsoft Edge Add-ons, and the Safari App Store route, plus the pre-submission check that catches the common rejection reasons while they are still cheap to fix.

## Workflow

1. **Name the single purpose in one sentence.** Write: this extension does one job, for this user, on these sites. Chrome and Edge both enforce a single purpose rule. Two unrelated tools in one package, or an ad injector attached to a utility, get split into separate extensions before anything else happens. Every feature, permission, and listing claim gets tested against that sentence.

2. **Freeze the Manifest V3 shape.** Confirm `manifest_version: 3`, a service worker under `background.service_worker` (no background scripts, page, or persistent flag), `action` instead of `browser_action` or `page_action`, host access in `host_permissions`, and network blocking done with `declarativeNetRequest` rules shipped in the package instead of blocking `webRequest`. State that must survive the service worker sleeping goes in `chrome.storage`, never in a variable that dies with the worker.

3. **Run the permission review.** For every entry in `permissions`, `host_permissions`, `optional_permissions`, and `optional_host_permissions`, write one sentence: permission, user-facing feature it serves, why a narrower option will not work. Then apply the cuts in order:
   - No visible API use in the code means the permission comes out. Stores reject for unused permissions by name.
   - Replace broad host access with `activeTab` when the extension only acts on the current page after a click, a context menu pick, or a keyboard command. `activeTab` is temporary and ends on navigation or tab close, which is why reviewers prefer it.
   - Narrow any remaining host pattern to the exact origins the feature calls. One API origin beats a wildcard.
   - Move permissions for secondary features to `optional_permissions` and `optional_host_permissions`, and request them at runtime with `chrome.permissions.request` from a user gesture, with the reason shown in the UI at that moment. Some permissions cannot be optional in Chrome, including `debugger`, `declarativeNetRequest`, `devtools`, `geolocation`, `proxy`, and `tts`, so core features built on those stay required and get a stronger justification.
   - Watch the documented traps: `tabs` only reads URL, title, and favicon fields, `cookies` only covers the `chrome.cookies` API (not `document.cookie`), and `storage` only covers `chrome.storage` (not `localStorage` or IndexedDB). Requesting one for the wrong API is a standard excessive-permission rejection.

4. **Kill remotely hosted code and hidden code.** Search the package for script tags with an `http` source, `eval(`, `new Function(`, and any fetch that returns text which then gets executed or interpreted as commands. Bundle every library into the package. Remote calls may carry data only: JSON config where every selectable behavior already exists in the shipped code, images, and sync payloads. Remove obfuscation. Minification for file size is accepted across stores, code that hides its purpose is not, and AMO can demand original source for any bundled output.

5. **Run the bundled preflight.** Run `python3 bin/preflight.py <extension_dir>` from this skill folder against the unpacked package that will be zipped. It checks MV3 shape, MV2 leftovers, files named in the manifest that are missing on disk, a CSP that tries to allow eval, broad host patterns, high-risk permissions, permissions with no matching API call in the source scan, and remote script tags. Fix every FAIL. For every WARN, either fix it or keep the written justification from step 3, because a reviewer will ask the same question. The API-use scan is a heuristic: code reached through a wrapper library or WebAssembly can scan as unused, so a WARN there means prepare the proof, not blindly delete a live permission.

6. **Test the exact package, not the dev folder.** Zip the directory with `manifest.json` at the root of the archive. Unzip into a clean folder, load that copy, and run the core flow start to finish: fresh install, first run with no account, the main job, options or settings, and behavior on a slow or failed network. Confirm case-sensitive file paths match the manifest exactly. A file named in the manifest and missing from the package is the top packaging rejection. Confirm the backend the extension needs will stay up for the whole review window, and prepare a test account with seed data if any feature sits behind a login.

7. **Write the listing from the code outward.** Title states the task with no keyword stuffing and no third party brand used as bait. Description follows the structure in `references/listing-and-privacy.md`: what it does in the first two lines, one bullet per real feature, how it works, a plain data section, and honest limits. Screenshots show the real UI doing the real task on a real page, in description order, at the exact store size, with test account data only. Full per-store asset specs and account steps are in `references/store-matrix.md`.

8. **Complete the privacy surface before submitting.** Host a privacy policy at a stable HTTPS URL that loads without a login, and put that URL in the designated dashboard field, never only in the description text. Make the dashboard data disclosures match behavior item by item. On Chrome, certify Limited Use: data serves the single purpose only, no sale of data, no personalized ads use, no transfer to data brokers, no credit or lending use. On Firefox, declare `browser_specific_settings.gecko.data_collection_permissions` in the manifest, including an explicit none declaration when nothing is collected. Any store: if the code collects nothing and calls no analytics, say so in the policy and in the listing, and keep the code that clean.

9. **Write reviewer notes as a test script.** Include the single purpose sentence, test credentials with the account role and seed data, numbered steps for the core flow starting from the toolbar icon, how to reach any hidden or toggled feature, one line per permission with its feature, the hosts the extension contacts and what each call carries, and for updates, what changed. Edge calls this field Notes for certification and treats an untestable product as a policy failure. Chrome reviewers who cannot reach a feature treat it as not working.

10. **Submit per store, in a sane order.** Submit to the strictest target first, usually Chrome, fix what comes back, then reuse the corrected package and notes for Edge (Partner Center, certification can take up to seven business days per Microsoft) and AMO (listed review is human, source upload ready if the build is bundled). Safari goes last unless Safari is the point: it needs a native wrapper app from `xcrun safari-web-extension-converter`, Xcode signing, and App Store review, so it is a separate release track. Details per store are in `references/store-matrix.md`.

11. **Handle the verdict without thrashing.** Approval: stage or publish per the release plan, and record the live version, store URLs, and the exact ZIP hash that shipped. Rejection: open `references/rejection-playbook.md`, match the violation ID or policy number, fix only that issue, and resubmit with a short note naming the violation, the change, and the files touched. Appeal instead of resubmitting only when the reviewer premise is factually wrong and the proof sits inside the submitted package, for example a permission flagged as unused that is called through a wrapper. Never bundle unrelated changes into a fix resubmission, AMO says so directly and it slows every store.

12. **Run updates like small first submissions.** Bump the manifest `version`, re-run the preflight, re-check every new permission as if it were new (new required permissions can disable an extension for existing users on upgrade, optional ones do not), update the privacy policy before shipping a collection change, and keep release notes factual. A percentage rollout on Chrome applies to existing users, only moves up, and does not trigger a new review.

## Output Contract

A submission-ready run produces:
- The preflight output for the exact package, with zero FAIL lines and a written disposition for every WARN.
- A permission table: each permission and host pattern, the feature it serves, required or optional, and the one-sentence justification that will be pasted into the store form.
- The single purpose sentence, final title, description, and screenshot list in store order.
- The privacy policy URL plus a disclosure checklist showing dashboard answers match code behavior.
- Reviewer notes ready to paste, including test account details when a login exists.
- After the verdict: the outcome per store, any violation ID with its fix, and the shipped version and package identifier.

## Operating Rules

- Quality over speed on review claims. Review times quoted by stores are guidance, not promises. Report the range the store states and the factors that stretch it (broad permissions, new account, sensitive category), never a guaranteed approval date.
- The listing is a contract. Any behavior the code performs and the listing never mentions is a deceptive behavior risk, and any feature the listing promises that the code lacks is a functionality rejection. Keep the two in lockstep on every version.
- Narrow beats broad at every decision point: one origin over a wildcard, `activeTab` over standing host access, optional over required, local storage over a server call. Each narrowing is one less reviewer question.
- No remotely hosted code, no exceptions for convenience. A CDN copy of a library is still remote code. Vendor it, pin the version in the repo, and move on.
- Secrets never ship. API keys, tokens, and private endpoints do not belong in an extension package, which any user can unzip and read. Server-side work that needs a real secret happens on a server.
- Test accounts are part of the product during review. A login wall with no working credentials in the notes reads to a reviewer as a broken extension.
- One fix per resubmission. Bundled changes restart reviewer suspicion and, on AMO, are called out in policy as a cause of delay and rejection.
- Verify store specs live at submission time. Asset sizes, form fields, and data declaration requirements change. The numbers in the references were verified against store docs at writing time and are a checklist to confirm in the dashboard, not a substitute for reading it.
- Keep a rejection log per extension: store, date, violation ID or policy clause, fix, outcome. The second extension from the same team should never repeat the first one.
