Passing Chrome Web Store Review

Fix the most common Chrome Web Store rejection causes — excessive permissions, remote code, unclear purpose and missing privacy disclosures — and navigate the review workflow.

Published June 19, 2026 Updated June 19, 2026 9 min read
Table of Contents

Submitting a Manifest V3 extension only to receive a rejection email a few days later is a frustrating but very common experience. The Chrome Web Store runs every submission through an automated policy scan followed by a human review queue, and a single violation in any category — permissions, code practices, listing content, or privacy — is enough to block publication. This page walks through the most frequent rejection causes in MV3 extensions and the concrete steps to fix each one before you resubmit. For broader context on the permissions model, see the store submission and permissions compliance reference.

Why MV3 extensions get rejected

The review process exists to protect users from extensions that over-reach, deceive, or execute untrusted code. MV3 tightened the technical floor — background pages are gone, remote code execution is blocked at the platform level — but the policy layer applies additional human judgment on top. Rejections fall into two buckets: technical violations that the automated scanner flags (eval usage, suspicious network calls, declared-but-unused APIs) and policy violations that a human reviewer flags (vague purpose, broken privacy policy URL, misleading screenshots). Understanding which bucket your rejection falls into determines whether a quick code fix or a listing rewrite is the right response.

Step-by-step: fixing the common rejection causes

1. Audit and tighten permissions

The most common automatic rejection is requesting broader permissions than the extension demonstrably needs. Reviewers look for two patterns: host permissions scoped to <all_urls> or *://*/* when the extension only ever talks to one domain, and the tabs permission when activeTab would suffice.

Before:

1{
2  "permissions": ["tabs", "storage"],
3  "host_permissions": ["<all_urls>"]
4}

Execution context: this manifest is parsed by the browser at install time and evaluated by the CWS policy scanner at submission time — both enforce the declared scope.

After:

1{
2  "permissions": ["activeTab", "storage"],
3  "host_permissions": ["https://api.example.com/*"],
4  "optional_permissions": ["tabs"],
5  "optional_host_permissions": ["<all_urls>"]
6}

Execution context: activeTab grants temporary access to the current tab only when the user invokes the extension; optional_permissions lets you request broader access at runtime only when a specific feature needs it. See requesting optional permissions at runtime for the full pattern.

Also grep your source for every permission you have declared and confirm it maps to at least one API call:

1# List declared permissions, then search for usages
2grep -r "chrome\.tabs\." src/
3grep -r "chrome\.history\." src/
4grep -r "chrome\.bookmarks\." src/

Execution context: run this in your local shell before packaging; any declared permission that produces zero grep hits is an orphaned entry the reviewer will flag.

Remove every orphaned permission from the manifest before resubmitting.

2. Remove remote code execution patterns

MV3 disallows eval(), new Function(string), and externally-loaded scripts. The automated scanner searches for these patterns in your bundled output, not just your source. A common hidden source is a third-party library that internally uses eval for feature detection.

Replace dynamic evaluation with a bundled function reference:

1// Before — will trigger rejection
2const fn = new Function("return " + userExpression)();
3
4// After — bundle the logic, pass data not code
5import { evaluateExpression } from "./expression-engine.js";
6const result = evaluateExpression(userExpression);

Execution context: both the before and after run in a service worker or content script context; only the after form is permitted under MV3’s Content Security Policy, which blocks unsafe-eval in extension pages.

For chrome.scripting.executeScript, never pass a string. Always reference a function defined in your bundle:

 1// Before — rejected
 2chrome.scripting.executeScript({
 3  target: { tabId },
 4  code: "document.body.style.background = 'red';"
 5});
 6
 7// After — permitted
 8function paintBackground() {
 9  document.body.style.background = "red";
10}
11
12chrome.scripting.executeScript({
13  target: { tabId },
14  func: paintBackground
15});

Execution context: func must reference a function that exists in the service worker’s own scope at the time of the call — it is serialized and injected, so it cannot close over variables from the outer scope.

Audit your node_modules for eval usage before bundling:

1npx is-eval-free ./dist/bundle.js
2# or manually:
3grep -n "eval\|new Function" dist/bundle.js

Execution context: run against the final built artifact in your local shell, not your source files, because bundlers sometimes inline eval from dependencies.

3. Complete the CWS data use disclosure form

The Chrome Web Store requires a filled-out data disclosure form for every extension that collects, uses, or shares user data. A broken privacy policy URL or a policy that is too generic (“we may collect data”) causes a human reviewer to reject the listing.

Log in to the Chrome Web Store Developer Dashboard, open your extension, navigate to Privacy practices, and fill out every section that applies:

  • Personally identifiable information — check if you collect name, email, address, or phone.
  • Health information — check if any feature touches medical data.
  • Financial and payment information — check if you interact with payment pages or store financial data.
  • Authentication information — check if you store passwords or authentication tokens.
  • Personal communications — check if you read emails, messages, or call logs.
  • Location — check if you use the Geolocation API or derive location from IP.
  • Web history — check if you use chrome.history or read the URLs of tabs the user visits.
  • User activity — check if you log clicks, keystrokes, or page interactions.
  • Website content — check if you read or transmit page content.

For each checked category, you must state whether data is shared with third parties and link to your privacy policy. Host the policy at a stable URL on a domain you control — a Google Doc or GitHub Gist is not sufficient. The policy must describe what data is collected, why, how long it is retained, and how users can request deletion.

Execution context: this is a form in the developer dashboard, not code; the URL you provide must return a 200 response and render readable policy text at review time.

4. Package correctly

Packaging mistakes cause rejections that look like content policy violations but are actually submission format issues.

1# Use web-ext to produce a clean zip
2npx web-ext build \
3  --source-dir ./dist \
4  --artifacts-dir ./artifacts \
5  --ignore-files "node_modules/**" "src/**" "*.map" ".env*"

Execution context: run in your local shell from the project root; web-ext build validates the manifest before zipping, which catches missing required fields before the CWS scanner does.

If you prefer a manual zip:

1cd dist && zip -r ../extension.zip . -x "*.map" -x "node_modules/*"

Execution context: run in the directory that contains only the production build output — never zip the project root, which would include source files, node_modules, and credentials.

Key packaging rules:

  • Do not include node_modules/ — bundle all dependencies into your output files.
  • Do not include .env files, API keys, or test fixtures.
  • Keep the total unpacked size under 10 MB where possible; very large packages trigger additional manual scrutiny.
  • If your output contains minified code, the CWS may request source maps or an unminified build. Firefox Add-ons (AMO) requires you to upload source code separately for review if any file is minified.

Cross-browser variation

  • Chrome: automated scanner runs first; human review follows for new submissions and updates that change permissions. Rejection emails include a reason code but minimal detail. The appeals form is at support.google.com/chrome_webstore/contact/one_stop.
  • Firefox (AMO): source code submission is mandatory for minified extensions. AMO reviewers read the source directly. Remote code is also blocked but the CSP is slightly more permissive on the options page — still avoid eval regardless. Firefox review times vary from hours to several weeks.
  • Safari / App Store Connect: Safari extensions are reviewed as part of a macOS or iOS app submission. The App Store privacy nutrition label replaces the CWS data disclosure form. Remote code in a Web Extension context is blocked under the same WebKit CSP rules.
  • Edge Add-ons: Microsoft mirrors Chrome Web Store policy closely but runs its own review queue. Extensions already approved on CWS are not automatically approved on Edge.

Verification

Before submitting, run through this checklist to confirm each fix is in place:

  1. Open your built manifest and verify no permission in "permissions" or "host_permissions" is broader than your code requires.
  2. Run grep -rn "eval\|new Function\|document\.write" dist/ and confirm zero hits.
  3. Load the extension in Chrome via chrome://extensions with Developer Mode enabled, open the service worker DevTools, and exercise every feature — confirm no CSP errors appear in the console.
  4. Visit your privacy policy URL in an incognito window and confirm it loads and reads correctly.
  5. Run web-ext lint --source-dir ./dist and resolve any reported issues.

Expected output from a clean lint run:

Validation Summary:
0 errors
0 warnings
0 notices

Execution context: web-ext lint runs in your local shell and does not require a browser; it checks the manifest schema, file references, and common policy patterns.

FAQ

How long does a re-submission take after fixing a rejection?

Re-submissions typically enter the same review queue as new submissions. Expect one to three business days for automated review and up to a week for human review. Repeated resubmissions for the same violation can increase review time, so fix all identified issues before resubmitting rather than addressing them one at a time.

Can I appeal a rejection I disagree with?

Yes. Use the appeal form linked in the rejection email or navigate to the CWS developer support page. Provide a clear explanation of why the flagged item does not violate policy and, if applicable, a code snippet or video demonstrating the intended behavior. Appeals are reviewed by a separate team from the initial reviewers.

What counts as “single purpose”?

The CWS single-purpose policy means each distinct user-facing feature should follow logically from one clearly stated goal in your store listing. An extension that blocks ads and also rewrites the appearance of every page and also manages bookmarks is likely to be flagged. If your extension genuinely does multiple unrelated things, split it into separate extensions. If the features are related, tighten the store description so the connection is obvious.

Do I need a privacy policy if my extension collects no data?

You still need to complete the data disclosure form and answer “no” to each data category. If you genuinely collect nothing, you may not need a hosted policy document, but the form must be submitted and accurate. Leaving it blank causes an automatic rejection.

Other MV3 Architecture & Extension Lifecycle Resources