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.
Table of Contents
- Root cause: the reviewer is checking a claim against code
- Step 1. Write the four clauses in one or two sentences
- Step 2. Justify host permissions by naming origins and behaviour
- Step 3. Delete the permissions nothing uses
- Step 4. Make the justification, the privacy form and the listing agree
- Step 5. Answer a rejection with a change, not an argument
- Cross-browser variation
- Verification
- FAQ
- Related
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.
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.
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.
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
- Run the audit against the built bundle and confirm every declared permission has a matching call site.
- 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.
- Compare the justification text against the privacy disclosure line by line for contradictions.
- 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.
Related
- Passing Chrome Web Store review — the other common rejection causes.
- Publishing to Firefox Add-ons and Safari — what the other two stores expect.
- Requesting optional permissions at runtime — the narrower alternative in practice.
- Store Submission & Permissions Compliance — the guide this page belongs to.