My Chrome extension declared downloads as a permission. I had plans for it. Export-to-PDF, maybe. Batch saving. Something down the road. So I added it to manifest.json early, figuring I’d use it eventually.
The Chrome Web Store reviewer didn’t care about my plans. They cared about what my extension actually did. And it didn’t use downloads. Rejection.
The “Future-Proofing” Trap
Requesting permissions early feels responsible. You’re planning ahead. You don’t want to push a new version just to add a permission later, and risk users seeing a “new permission requested” dialog and abandoning the update.
But Chrome Web Store review doesn’t evaluate your roadmap. The reviewer asks one question: does your extension currently use this permission? If the answer is no, you have a problem.
This policy exists for good reason. Users install extensions trusting that the permissions match the functionality. An extension that requests downloads but never downloads anything looks suspicious. At best, your review takes longer. At worst, you get rejected outright.
What the Reviewer Sees
When you submit a Chrome extension, you fill out a permissions justification form. For every permission in your manifest.json, you must explain why your extension needs it.
My original justification for downloads was honest but wrong: “Planned for future PDF export functionality.”
That’s not a justification. That’s a roadmap item. The reviewer needs to see current usage, not future plans. If you can’t point to a specific code path that calls chrome.downloads.*, the permission shouldn’t be in your manifest.
The Permission Audit
After this rejection, I went through every permission in my manifest and asked the same question the reviewer would ask. The process took about 20 minutes and saved me from at least one more rejection.
My manifest originally looked like this:
{
"permissions": [
"identity",
"storage",
"sidePanel",
"downloads",
"activeTab"
],
"host_permissions": [
"https://www.googleapis.com/*"
]
}I went through each one:
identity → Used for Google Sign-In (chrome.identity.getAuthToken) ✓
storage → Used for saving user preferences (chrome.storage.local) ✓
sidePanel → Extension renders in Chrome's side panel ✓
downloads → ??? ✗
activeTab → Used for reading current tab URL ✓
host_permissions:
googleapis.com → Used for Google Drive API calls ✓downloads was the only one without a matching chrome.downloads.* call anywhere in my codebase. I verified with grep:
$ grep -r "chrome.downloads" src/
# No results
$ grep -r "chrome.downloads" dist/
# No resultsGone. Removed from the manifest, removed from the justification form.
The Cascade
Removing downloads was the easy part. The harder lesson came when I asked my AI agent to do the removal and it took out the entire permissions key instead of just the one entry. (I wrote about that specific incident in the previous article.)
That cascade taught me something: permission changes are config changes, and config changes need the three-gate verification. Even a “simple” one-line removal can break everything if the tool doing the removal isn’t precise.
Writing Good Permission Justifications
The justification form is your chance to make the reviewer’s job easy. Make it easy, and your review goes faster. Make it vague, and you’re asking for follow-up questions or rejection.
Bad justifications I’ve written:
- “Needed for core functionality”
- “Required for the extension to work”
- “Planned for future use”
Good justifications that passed review:
- identity: “Used to authenticate users via Google Sign-In (chrome.identity.getAuthToken). Required so the extension can access the user’s Google Drive to read/write chat export files.”
- storage: “Used to persist user preferences and session state locally (chrome.storage.local.set/get). No data leaves the browser.”
- sidePanel: “The extension’s primary UI renders in Chrome’s side panel (chrome.sidePanel.setOptions). Users interact with their chat history through this panel.”
The pattern: name the specific API call, explain what it does for the user, and mention where the data goes.
The Pre-Release Permission Checklist
This is what I run before every Chrome Web Store submission now. It takes about 10 minutes and has caught issues on every single release since I started doing it.
- List every permission. Open
manifest.jsonand write out every entry inpermissionsandhost_permissions. - Grep for usage. For each permission, search your source and build output for the corresponding Chrome API call (e.g.,
chrome.downloads,chrome.identity). - No grep match = remove it. If you can’t find a call in either
src/ordist/, the permission shouldn’t be there. - Write the justification. For each remaining permission, write one sentence: API call used, what it does, where data goes.
- Check host_permissions scope. Don’t request
*://*/*if you only needhttps://www.googleapis.com/*. Narrower is better. - Review optional_permissions. If you genuinely need a permission for a feature that isn’t in the initial flow, declare it as
optional_permissionsand request it at runtime.
optional_permissions: The Escape Hatch
If I still wanted downloads for a future PDF export feature, the correct approach is optional_permissions:
{
"permissions": ["identity", "storage", "sidePanel"],
"optional_permissions": ["downloads"]
}Then, when the user actually triggers the PDF export flow, you request the permission at runtime:
// Only request when the user clicks "Export to PDF"
const granted = await chrome.permissions.request({
permissions: ['downloads']
});
if (granted) {
// proceed with download
}The reviewer won’t flag optional permissions the same way because they’re not active by default. The user explicitly grants them. This is both better UX and better for review.
Why This Matters Beyond Review
The permission audit helps with review, but the real reason to do it is trust.
When someone installs your extension and sees “This extension can: Read and change all your data on websites you visit,” they’re deciding whether to trust you. Every unnecessary permission makes that decision harder.
ChatShuttle handles AI conversation history. People are already nervous about where that data goes. The fewer permissions we ask for, the less explaining we have to do.
Our permission set ended up minimal: sign in, store preferences locally, render a side panel, talk to Google Drive. Nothing else. That’s the list, and every item has a specific code path behind it.
For the full pre-submission workflow that includes this permission audit, see the pre-submission checklist in the remote code article. And for how I handle config file changes safely with an AI agent, the three-gate system covers that.
If you’re curious about ChatShuttle’s actual architecture and the decisions behind it, the docs go into detail.