Store Submission, Permissions & Compliance
Submit MV3 extensions to Chrome Web Store, Firefox AMO and Safari, justify permissions under least-privilege, meet privacy disclosure requirements and avoid common rejection causes.
Submit MV3 extensions to Chrome Web Store, Firefox AMO and Safari, justify permissions under least-privilege, meet privacy disclosure requirements and avoid common rejection causes.
Getting a working extension past the store review process is where many developers stall. The code compiles, the manifest validates locally, and the extension does exactly what you intended — yet the submission bounces back with a vague rejection notice about “excessive permissions” or a missing privacy policy. This guide, part of the Manifest V3 Architecture & Extension Lifecycle section, walks through the full submission pipeline for Chrome Web Store, Firefox AMO, and the Safari Extension Gallery: how to structure your package, how to justify every permission the right way, how to meet data-disclosure requirements, and how to avoid the most common rejection causes before you hit submit.
.DS_Store or editor config files in the zip.manifest.json passes web-ext lint (Firefox) and Chrome’s built-in manifest validator without warnings."permissions" and "host_permissions" is actually used by the shipped code — unused entries are a fast-path to rejection.tabs, broad host patterns, history, bookmarks).chrome.permissions.request() rather than declared in the static "permissions" array.MAJOR.MINOR.PATCH.BUILD with no leading zeros.Before you can submit anywhere, the extension must be zipped correctly. The zip root must contain manifest.json — not a subdirectory wrapping it.
1// manifest.json — minimum required fields for all three stores
2{
3 "manifest_version": 3,
4 "name": "My Extension",
5 "version": "1.2.0",
6 "description": "One concise sentence. Used verbatim in store listings.",
7 "icons": {
8 "16": "icons/icon-16.png",
9 "48": "icons/icon-48.png",
10 "128": "icons/icon-128.png"
11 },
12 "permissions": ["storage"],
13 "host_permissions": ["https://api.example.com/*"],
14 "action": {
15 "default_popup": "popup.html",
16 "default_icon": "icons/icon-48.png"
17 }
18}
Execution context: Parsed by the browser extension host at install time. The "description" field is read by both the browser and each store’s review system; keep it factual and specific to your extension’s single purpose.
For Firefox, use the web-ext CLI to produce a validated package:
1# Install once
2npm install --global web-ext
3
4# Lint before packaging
5web-ext lint --source-dir ./dist
6
7# Build the zip (output goes to ./web-ext-artifacts/)
8web-ext build --source-dir ./dist --artifacts-dir ./web-ext-artifacts
Execution context: Runs in your local shell or CI environment. web-ext lint catches AMO-specific issues including missing browser_specific_settings.gecko.id, which Firefox requires for self-hosted or signed extensions. Chrome and Safari do not need an equivalent; they infer identity from the upload.
For Safari, you convert the extension with Xcode’s safari-web-extension-converter tool before any zip is involved:
1# Requires macOS + Xcode 12+
2xcrun safari-web-extension-converter ./dist \
3 --project-location ./SafariExtension \
4 --app-name "My Extension" \
5 --bundle-identifier com.example.myextension
Execution context: Runs on macOS in a local terminal. The converter wraps the extension in a native macOS/iOS app that you then build in Xcode and submit to App Store Connect. You will need an Apple Developer Program membership and must pass the App Store entitlements review.
Chrome Web Store reviewers are trained to flag any permission that seems broader than the stated purpose of the extension. The guiding rule is simple: only declare a permission if removing it would break a core user-facing feature.
MV3 introduced a cleaner permission model than MV2. There are now three distinct slots:
"permissions" — API permissions ("storage", "tabs", "alarms", "scripting", etc.) required at install time."host_permissions" — URL patterns the extension can access without user interaction. Reviewed more strictly than in MV2; broad patterns like "<all_urls>" or "*://*/*" require explicit justification in the submission form."optional_permissions" and "optional_host_permissions" — permissions you request at runtime, only when the user triggers a flow that needs them. These are not shown in the install dialog and do not count against your base footprint.The correct pattern for a non-core permission is to move it to optional_permissions and request it at the point of use. The full mechanics of this flow — including the chrome.permissions.request() call, the user-gesture requirement, and how to check for permission before acting — are covered in depth in Requesting optional permissions at runtime.
1{
2 "manifest_version": 3,
3 "permissions": [
4 "storage", // always needed: settings persistence
5 "scripting" // always needed: content script injection
6 ],
7 "host_permissions": [
8 "https://api.example.com/*" // specific origin, not <all_urls>
9 ],
10 "optional_permissions": [
11 "bookmarks", // only needed if user enables bookmark sync feature
12 "history" // only needed if user enables history analysis feature
13 ],
14 "optional_host_permissions": [
15 "*://*.third-party-service.com/*" // only if user connects this integration
16 ]
17}
Execution context: Parsed at install time by the extension host. optional_permissions and optional_host_permissions are never shown in the install prompt; they appear only when the extension calls chrome.permissions.request() and the user accepts the in-context dialog.
When you do need a broad host pattern, be ready to justify it in writing. The CWS submission form has a “Permission justification” field where you explain — in plain language — exactly why each sensitive permission is necessary. A one-sentence justification like “The extension needs <all_tabs> to display the word-count overlay on any page the user visits” is far more persuasive than leaving the field blank.
All three stores require a privacy policy URL before your extension can go live. The policy must:
Chrome Web Store additionally requires you to complete a data use disclosure form in the developer dashboard. You must categorize every type of user data the extension handles (personal information, authentication info, financial info, health info, website content, etc.) and declare whether it is sold, used for advertising, or shared with third parties. Inaccurate disclosures — even inadvertent ones — can result in removal after approval.
Firefox AMO requires a privacy policy URL in the submission metadata and will flag an extension for human review if it collects any user data without a linked policy. AMO also has a source code submission requirement: if your code is minified, bundled, or otherwise obfuscated, you must upload a readable copy of the source (as a separate zip) alongside the built artifact. Reviewers use it to verify that the shipped code does what the listing claims. This is one of the most commonly overlooked AMO requirements.
Safari follows the App Store’s general data-collection disclosure via App Store Connect’s “App Privacy” section. You declare each data type collected, the purpose, and whether it is linked to the user’s identity. Apple’s review team audits these declarations against the extension’s actual network activity.
Plan your release schedule around realistic review windows:
Chrome Web Store runs automated scanning immediately on upload. For new publishers, human review is common and can take anywhere from one to three business days for straightforward extensions, up to several weeks if the extension touches sensitive APIs or requests broad host permissions. Established publishers with a good history tend to see shorter turnarounds. The Chrome Web Store review process detail explains how to prepare a submission that clears the automated phase cleanly and gives human reviewers exactly what they need.
Firefox AMO relies heavily on volunteer reviewers in addition to automated linting. Timelines are variable — a simple extension with no source-code submission may clear in a day; a complex extension requiring source review can take a week or more. AMO offers two tracks: “Listed” (full review, publicly searchable) and “Unlisted” (self-distribution, lighter-touch review but not discoverable in the store). Choose “Listed” unless you have a specific reason for self-distribution.
Safari Extension Gallery / App Store mirrors the standard App Store review process: typically one to three business days. Apple’s reviewers test the extension on a real device and may reject for reasons that have no Chrome or Firefox analogue — for example, an extension that does nothing visually distinctive from a built-in Safari feature, or one that does not pass through its native app wrapper cleanly.
Understanding why submissions fail is the fastest path to getting through on the first attempt.
Excessive permissions is the leading cause of CWS rejection. If your extension declares "tabs", "history", "bookmarks", "<all_urls>", or remote code execution capabilities, each one needs a documented justification. Audit your code before submitting: search for every chrome.* API call you actually make, derive the minimal permission set from that list, and move everything else to optional_permissions.
Remote code execution is a hard block in MV3 across all three stores. You cannot fetch JavaScript from a remote server and execute it via eval(), new Function(), or document.createElement('script') with a remote src. This includes dynamically assembled content-security-policy bypasses. The CSP for MV3 extensions forbids unsafe-eval and unsafe-inline in the extension’s own pages, and stores enforce it at review time.
Unclear purpose or deceptive behavior covers a wide range of issues: extensions that claim one function in the description but request permissions for an unrelated one, hidden affiliate-link injection, silent data exfiltration, or UI that mimics a browser dialog to trick users. Write your listing description and screenshots to accurately represent every major feature.
Missing or non-compliant privacy policy blocks publication at the final step after an otherwise passing review. Set up your policy URL early in development, link it in the developer dashboard, and make sure the URL resolves correctly before you submit.
Versioning errors — duplicate version strings, versions lower than the currently published one, or non-numeric segments — cause immediate automated rejection before a human ever sees the submission.
eval(), new Function(), and dynamic <script src="…"> pointing at remote URLs are prohibited. All logic must be bundled with the extension.unsafe-eval in CSP: MV3 extensions use a strict Content-Security-Policy by default. You cannot relax it to allow eval, and stores will reject manifests that attempt to.host_permissions reviewed more strictly than in MV2: broad patterns (<all_urls>, *://*/*) require written justification and are increasingly subject to mandatory user-gesture restriction.optional_host_permissions is MV3-only: there is no equivalent in MV2; if you support both manifest versions in the same codebase, gate optional host permission logic on the manifest version.| Topic | Chrome Web Store | Firefox AMO | Safari App Store |
|---|---|---|---|
| Review type | Automated + human (Google staff) | Automated + volunteer human | Apple staff |
| Typical timeline (new publisher) | 1–3 business days, up to weeks | Days to a week+ | 1–3 business days |
| Source code requirement | No | Yes, for minified/obfuscated code | No (but Xcode project required) |
| Privacy disclosure mechanism | CWS data use disclosure form | Privacy policy URL | App Store Connect “App Privacy” |
| Broad host permission handling | Justification text required; CWS may show warning to users | Flagged for human review | App review tests network activity |
| Extension identity | Google account + one-time $5 developer fee | Firefox account, free | Apple Developer Program ($99/yr) |
optional_host_permissions support | Full support | Full support (Firefox 128+) | Full support (Safari 17+) |
| Obfuscation policy | Prohibited | Source upload required | Prohibited |
Firefox 128 shipped full optional_host_permissions support; earlier versions fall back to treating optional host permissions as required ones declared in the manifest. Test against the minimum Firefox version you declare in browser_specific_settings.gecko.strict_min_version.
Safari’s Xcode wrapper requirement means you cannot use web-ext build as your final artifact — the .app bundle produced by Xcode is what goes to App Store Connect. Factor Xcode build time into your CI pipeline if you ship cross-browser from the same source tree.
chrome.permissions.request() patterns, user-gesture requirements, and graceful degradation when permissions are denied.Fix the most common Chrome Web Store rejection causes — excessive permissions, remote code, unclear purpose and missing privacy disclosures — and navigate the review workflow.
Use chrome.permissions.request and optional_host_permissions in MV3 to gate features behind user consent, handle the user-gesture requirement, and degrade gracefully when denied.