Writing a Permission Justification That Passes

Write MV3 permission justifications reviewers accept: the structure that works, one worked example per permission, the phrases that trigger rejection, and reducing the request instead.

Published July 25, 2026 Updated July 25, 2026 9 min read
Table of Contents

A justification is read by someone with your manifest on one side and your bundle on the other, checking whether the sentence you wrote explains the capability you asked for. “Required for the extension to function” fails that check immediately; so does a paragraph of marketing. The reviewable form is narrow and mechanical, and it is worth writing before you request the permission rather than after. This page is part of Store Submission & Permissions Compliance and covers the sentence, the alternatives, and the requests to drop.

Root cause: the reviewer is checking a claim against code

The justification is not persuasion, it is evidence. The reviewer wants to know which user-visible feature needs the capability, what the extension does with the data, and that a narrower permission would not have worked. A sentence that names none of those three cannot be verified, so it cannot be accepted regardless of how true it is.

What a reviewable justification containsThe feature, the API call it needs, what happens to the data and why a narrower permission was insufficient.The featurewhat the user sees and doesnames a real UI affordanceThe API it requiresthe specific callgreppable in the bundleWhat happens to the datastored locally, never transmittedmust match the privacy formWhy not something narroweractiveTab or a match patternpre-empts the obvious question
Four short clauses, all checkable against the bundle — that is the whole format.

Step 1. Write the four clauses in one or two sentences

Keep it short. A reviewer reading fifty of these values a sentence that answers the question over a paragraph that surrounds it.

 1tabs
 2  Weak:   "Needed to work with tabs."
 3  Strong: "The 'Save this page' toolbar action reads the active tab's title and URL to create a
 4           bookmark record stored in chrome.storage.local. Nothing is transmitted. activeTab is
 5           insufficient because the same list is refreshed on a schedule while the popup is closed."
 6
 7scripting
 8  Weak:   "To inject our script."
 9  Strong: "Clicking 'Highlight terms' injects highlight.js into the current tab via
10           chrome.scripting.executeScript to mark matching text nodes. The extension reads no page
11           content beyond the matches, which stay in the tab. A declarative content script is
12           insufficient because injection is user-initiated and site-agnostic."
13
14storage
15  Strong: "Stores the user's term list, theme choice and per-site enablement in chrome.storage.sync
16           so settings follow the user between devices. No page content is stored."

Execution context: Written into the store listing’s permission fields, one per declared permission. Naming the exact API call is what makes the claim checkable — a reviewer greps the bundle for chrome.scripting.executeScript and finds it, or does not. The final clause matters most for tabs and host permissions, because those are the two the reviewer will otherwise ask about.

Step 2. Justify host permissions by naming origins and behaviour

Broad host access is the single most-rejected request. If you cannot narrow it, the justification has to explain what happens on a site the extension does not recognise — because that is the reviewer’s real question.

1host_permissions: https://*/*
2  Weak:   "The extension works on all websites."
3  Strong: "The reader view is invoked by the user from the toolbar on any article page, so the set
4           of origins is not known in advance. On invocation the extension reads the article body
5           from the current tab to render a simplified view locally. On pages where the user has
6           not invoked it, no code runs and no data is read. Optional host permissions are used
7           where the user pins a site, and the broad grant covers only the on-demand path."

Execution context: Store listing, host permission field. The sentence that does the work is the one describing the non-invoked case: it converts “this extension can read every site” into “this extension reads a site when the user asks it to”, which is a claim the reviewer can verify by looking for the gesture. If your extension genuinely does run on every page unprompted, say so plainly and expect a longer review — an inaccurate justification is worse than an expensive one.

Common requests and the narrower alternative reviewers expect you to have consideredFor each broad permission, the narrower option, what it costs you, and whether it is usually accepted without further questions.RequestedNarrower optionWhat it costsFast review?host_permissions https://*/*activeTabNeeds a gestureYeshost_permissions https://*/*optional_host_permissionsHandle refusalYestabsactiveTab + sender.tabNo background enumerationYeswebRequestdeclarativeNetRequestStatic rules onlyYes<all_urls> content scriptExplicit match patternsEnumerate the sitesYescookiesAsk the site's own APIMay not be possibleDepends
The right-hand column is the fast path — most of these swaps cost a few lines of code and remove the review question entirely.

Step 3. Delete the permissions nothing uses

The cheapest justification is the one you do not have to write. Auditing the built bundle against the manifest routinely finds one or two leftovers from features that were removed, and each of those is a free rejection avoided.

 1// scripts/justify-audit.mjs — pair each permission with evidence, or drop it
 2import { readFile } from "node:fs/promises";
 3import { globby } from "globby";
 4
 5const manifest = JSON.parse(await readFile("dist/chrome/manifest.json", "utf8"));
 6const code = (await Promise.all((await globby(["dist/chrome/**/*.js"])).map((f) => readFile(f, "utf8")))).join("\n");
 7
 8const EVIDENCE = {
 9  tabs: /chrome\.tabs\.(query|get|create|update|move|remove)/,
10  scripting: /chrome\.scripting\.(executeScript|insertCSS|registerContentScripts)/,
11  storage: /chrome\.storage\.(local|sync|session)/,
12  alarms: /chrome\.alarms\.(create|onAlarm)/,
13  notifications: /chrome\.notifications\.create/,
14  offscreen: /chrome\.offscreen\.createDocument/,
15  contextMenus: /chrome\.contextMenus\.create/,
16};
17
18for (const permission of manifest.permissions ?? []) {
19  const probe = EVIDENCE[permission];
20  if (!probe) { console.log(`?  ${permission}: no probe defined — check by hand`); continue; }
21  console.log(probe.test(code) ? `ok ${permission}` : `DROP ${permission}: no call site in the bundle`);
22}

Execution context: Node on a developer machine or in CI, reading the built output. Running this against dist rather than src is what catches a permission whose last call site was removed by tree-shaking — the source may still contain a reference in dead code that never ships. Each DROP line is a permission you can delete instead of defending.

Step 4. Make the justification, the privacy form and the listing agree

The three are cross-checked. A justification saying data never leaves the device, alongside a privacy form declaring transmission to a remote server, is a contradiction the reviewer resolves by rejecting.

The three statements a reviewer comparesThe permission justification, the data-use disclosure and the store description are read against each other and against the bundle.Justificationwhy the capabilityPrivacy disclosurewhat data, where it goesStore descriptionwhat the extension doesall three checked against the shipped bundleThe built extensionthe only ground truth
Any two of these disagreeing is a rejection, regardless of which one is correct.

Write the justification last, once the bundle is built, and read the privacy form immediately afterwards with the same text in front of you. A useful test: if the justification mentions a network call, the privacy form must declare it; if the privacy form declares transmission, the justification should say what is transmitted and why.

Step 5. Answer a rejection with a change, not an argument

A rejection names a reason. The productive response is nearly always a narrower manifest and a resubmission, not a longer explanation of the same request.

1Rejection: "Broad host permissions are not adequately justified."
2
3Weak reply:   "As explained, the extension needs access to all sites to function."
4Strong reply: "We have removed host_permissions entirely. The reader view now uses activeTab, which
5               is granted by the toolbar click that invokes it, and users who want the feature to
6               run automatically on a site grant that origin through optional_host_permissions from
7               the options page. Version 4.3.0 contains this change."

Execution context: The store’s reviewer correspondence. Naming the version that contains the change matters: a reviewer looking at a resubmission wants to know which build to check. If the capability genuinely cannot be narrowed, say what you tried and why it failed — that is a different conversation from repeating the original request, and it is one that sometimes succeeds.

Cross-browser variation

  • Chrome Web Store: requires a justification per permission and a separate one for host permissions, plus a data-use disclosure. Broad host access routes the submission into manual review.
  • Firefox AMO: does not require per-permission text upfront, but reviewers ask when a permission looks unnecessary, and may request build sources to confirm the bundle matches the repository.
  • App Store (Safari): justifications go in the review notes rather than a structured form. The same standard applies, and the app container adds its own privacy declarations.
  • All stores: an unused permission is a defect in every review process, and the audit in step 3 costs nothing to run before every submission.

Verification

  1. Run the audit against the built bundle and confirm every declared permission has a matching call site.
  2. Read each justification aloud and check it names a feature, an API and a data destination. If any is missing, the sentence is not yet reviewable.
  3. Compare the justification text against the privacy disclosure line by line for contradictions.
  4. For each broad permission, write down the narrower alternative and why it does not work. If you cannot, use the narrower one.

FAQ

How long should a justification be?

One or two sentences. Reviewers read many of these; the goal is a checkable claim, not a complete account of the feature.

Does activeTab really avoid the justification?

It avoids the difficult one. activeTab is granted by a user gesture and is well understood by reviewers, so it rarely attracts questions — which is why swapping to it is the single most effective change you can make.

What if the extension genuinely needs broad access?

Then say so accurately, describe what happens on sites where the user has not invoked it, and expect manual review. An honest broad request is approvable; an inaccurate narrow-sounding one is not.

Can I reuse the same text across stores?

Mostly, yes — the substance is the same. Adjust for where it goes: a structured field in Chrome, review notes for Safari, and a reply to a question in Firefox.

Other MV3 Architecture & Extension Lifecycle Resources