Manifest V3 Permissions: Best Practices
Best practices for requesting and managing permissions in Manifest V3 Chrome extensions. Covers required vs optional permissions, host permissions, and review implications.
Table of Contents
Every permission your Chrome extension requests is a decision that affects install rates, review timelines, and user trust. In Manifest V2, developers could lump everything into a single permissions array and move on. MV3 changes that. Permissions are now split across three distinct manifest keys, each with different behavior, different user-facing implications, and different review consequences.
Getting permissions right is not just a compliance checkbox. Extensions that request too much get rejected. Extensions that request too broadly see install conversion plummet when Chrome shows a scary warning dialog. And extensions that request too little end up with broken features and confused support threads.
This guide covers the full permission landscape in Manifest V3: the three permission categories, the least-privilege principle in practice, runtime permission flows, the activeTab strategy, permission warning strings, and what the Chrome Web Store review team actually looks for.
3
Permission Categories
permissions, optional_permissions, host_permissions
~15%
Install Drop per Warning
Each additional warning string reduces conversion
90%+
activeTab Coverage
Most extensions can use activeTab instead of broad host access
2-5x
Review Delay for <all_urls>
Broad permissions trigger deeper manual review
The Three Permission Keys in Manifest V3#
MV3 splits what was a single permissions array in MV2 into three separate manifest keys. Understanding the distinction between them is the foundation of everything else in this guide.
permissions — Required API Permissions#
The permissions key declares Chrome APIs your extension needs at install time. These are granted once and cannot be revoked by the user without uninstalling the extension. They trigger warning strings during installation.
optional_permissions — Runtime API Permissions#
The optional_permissions key declares Chrome APIs your extension might need but does not require at install time. These do not trigger warning strings during installation. Instead, your extension requests them at runtime via chrome.permissions.request(), and Chrome shows a permission prompt at that moment.
host_permissions — Site Access#
The host_permissions key is new in MV3. In MV2, host patterns like "https://*.google.com/*" lived inside the permissions array alongside API permissions. MV3 separates them into their own key, and crucially, Chrome now lets users modify host permissions independently. A user can choose to grant your extension access to all sites, specific sites, or no sites — regardless of what you declared.
Here is a well-structured manifest that uses all three:
{
"manifest_version": 3,
"name": "DataSync Pro",
"version": "1.0.0",
"permissions": [
"storage",
"alarms",
"activeTab"
],
"optional_permissions": [
"tabs",
"bookmarks",
"history"
],
"host_permissions": [
"https://api.datasyncpro.com/*"
]
}Notice: storage, alarms, and activeTab are always available because they are in permissions. The tabs, bookmarks, and history APIs are only available after the user explicitly grants them at runtime. And host access is scoped to the extension's own API domain, not to arbitrary websites.
The Least-Privilege Principle in Practice#
"Request only what you need" is advice you will find everywhere, but it is rarely explained in concrete terms. Here is what least-privilege actually looks like for common extension patterns.
Pattern: Reading the Current Page#
If your extension needs to read or modify the page the user is actively looking at, you almost certainly do not need broad host permissions. Use activeTab instead.
activeTab grants temporary access to the current tab when the user performs a deliberate gesture — clicking your extension icon, using a keyboard shortcut, or selecting a context menu item. The access lasts only until the user navigates away or switches tabs. No warning string. No permission prompt. No review friction.
Compare this to requesting <all_urls>, which tells users "This extension can read and change all your data on all websites" and adds days to your review time.
Pattern: Calling Your Own API#
If your extension only makes requests to your own server, declare that single origin in host_permissions:
"host_permissions": ["https://api.yourextension.com/*"]This generates no scary warning because Chrome recognizes it as a narrow, specific host pattern. Requesting "https://*/*" to "keep your options open" is one of the most common mistakes developers make — it is the single fastest way to tank your install rate and delay your review.
Pattern: Working with Multiple Third-Party APIs#
If your extension integrates with services like GitHub, Notion, and Slack, you have two options. You can declare all three in host_permissions upfront:
"host_permissions": [
"https://api.github.com/*",
"https://api.notion.com/*",
"https://slack.com/api/*"
]Or you can declare them as optional_host_permissions (supported since Chrome 114) and request access only when the user activates each integration:
"optional_host_permissions": [
"https://api.github.com/*",
"https://api.notion.com/*",
"https://slack.com/api/*"
]The second approach is almost always better. Users who only use the GitHub integration never see a prompt about Notion or Slack. Users who use all three grant access progressively as they configure each one. Every integration you defer to optional is one fewer warning string at install time.
Optional Permissions and the Runtime Request Flow#
Optional permissions are the most underused tool in the MV3 permission system. Developers avoid them because they add code complexity — you have to handle the case where the user says no. But the conversion and trust benefits far outweigh the extra work.
The Request Flow#
The chrome.permissions.request() API must be called from a user gesture context (a click handler, not a background script timer). Here is a complete implementation:
const githubToggle = document.getElementById("github-toggle") as HTMLInputElement;
githubToggle.addEventListener("change", async () => {
if (githubToggle.checked) {
// Request the permission when the user enables the integration
const granted = await chrome.permissions.request({
permissions: ["tabs"],
origins: ["https://api.github.com/*"],
});
if (granted) {
// Permission granted — initialize the GitHub integration
await chrome.storage.local.set({ githubEnabled: true });
initializeGitHubSync();
} else {
// User declined — revert the toggle, do not nag
githubToggle.checked = false;
showToast("GitHub integration requires site access to api.github.com.");
}
} else {
// User is disabling the integration — remove the permission
await chrome.permissions.remove({
origins: ["https://api.github.com/*"],
});
await chrome.storage.local.set({ githubEnabled: false });
}
});There are a few critical details here. First, chrome.permissions.request() must happen inside a direct user gesture handler — a click, a change event, a keypress. If you call it from a timeout, a Promise chain that is no longer in gesture context, or a background script, Chrome will reject the request silently.
Second, when the user declines, do not nag. Show a brief explanation of why the permission is needed and move on. Repeatedly prompting for permissions after a user says no is a violation of Chrome Web Store policy and will get your extension flagged.
Third, calling chrome.permissions.remove() when the user disables a feature is a trust signal that sophisticated users notice and appreciate. It also means their permission state accurately reflects what your extension can actually do.
Checking Permissions Before Using Them#
Before calling any API that requires an optional permission, always verify it is still granted. Users can revoke permissions through Chrome's extension settings at any time:
async function getGitHubData(): Promise<GitHubRepo[]> {
const hasPermission = await chrome.permissions.contains({
origins: ["https://api.github.com/*"],
});
if (!hasPermission) {
// Permission was revoked — notify the UI and bail
await chrome.storage.local.set({ githubEnabled: false });
return [];
}
const response = await fetch("https://api.github.com/user/repos", {
headers: { Authorization: `Bearer ${token}` },
});
return response.json();
}This guard is especially important for host permissions in MV3, because users can modify host access at any time through the Chrome toolbar. Your extension might have been granted https://api.github.com/* during setup and had it revoked an hour later. Without a permission check, your fetch call will fail with a generic network error that is difficult to debug.
activeTab vs Broad Host Permissions#
This is the single most impactful permission decision most extension developers will make. The choice between activeTab and a broad host pattern like <all_urls> or *://*/* affects your warning strings, review timeline, install conversion, and user trust.
What activeTab Actually Grants#
When the user clicks your extension icon (or triggers your extension via a keyboard shortcut or context menu), activeTab grants temporary access to:
- The URL of the active tab
- The ability to inject scripts and CSS into the active tab via
chrome.scripting - The ability to capture a screenshot of the active tab via
chrome.tabs.captureVisibleTab
This access expires as soon as the user navigates to a different page or switches to another tab. It does not grant access to any other tab, and it does not persist in the background.
When You Cannot Use activeTab#
activeTab does not work for extensions that need to:
- Run content scripts automatically on page load (without a user gesture)
- Monitor network requests across all tabs
- Access tab URLs in the background to track browsing patterns
- Inject scripts into tabs the user is not actively looking at
If your extension falls into these categories, you genuinely need host permissions. But be as specific as possible. If your extension only needs to run on GitHub, request "https://github.com/*" — not "<all_urls>".
- Use activeTab for any feature triggered by a user clicking your icon, shortcut, or context menu
- Declare narrow host patterns (specific domains) instead of broad wildcards
- Move host permissions to optional_host_permissions when the feature is not essential at install
- Check permissions.contains() before every call to an API that requires optional permissions
- Call permissions.remove() when a user disables a feature that required extra access
- Request <all_urls> or *://*/* unless your extension genuinely needs to run on every website
- Put host patterns in permissions just to avoid writing the runtime request flow
- Nag users repeatedly after they decline a permission request
- Assume a granted permission is still active — users can revoke host access at any time
- Request the tabs permission just to get tab URLs — activeTab gives you the active tab URL for free
Permission Warning Strings and Install Friction#
Every permission in your manifest maps to a warning string that Chrome shows during installation. These strings are written to be understood by non-technical users, and they are often alarming. Understanding exactly which strings each permission triggers lets you make informed tradeoffs.
High-Friction Warning Strings#
These permissions trigger the most alarming warnings and have the highest impact on install conversion:
<all_urls>/*://*/*— "Read and change all your data on all websites." This is the nuclear option. It is appropriate for ad blockers, password managers, and accessibility tools. For most other extensions, it is overkill.bookmarks— "Read and change your bookmarks." Sounds invasive to users who do not expect your extension to touch bookmarks.history— "Read your browsing history." Even if you only use this to search recent history for a feature, the warning sounds like surveillance.tabs— "Read your browsing activity." This one surprises developers because it sounds worse than whattabsactually does (expose tab URLs and titles). But from a user's perspective, "browsing activity" sounds like tracking.
Low-Friction or No-Warning Permissions#
These permissions generate no warning string at all:
activeTab— No warning. This alone makes it worth restructuring your extension to use.storage— No warning.alarms— No warning.contextMenus— No warning.notifications— No warning (the notification itself serves as disclosure).scripting— No warning (but requires host permissions or activeTab to actually do anything).
The Conversion Math#
Industry data across thousands of Chrome Web Store listings shows a roughly 15% drop in install conversion for each additional warning string. An extension with zero warning strings and one with three warning strings can see a 2x to 3x difference in install rates from the same amount of search impressions.
This is why the runtime permission flow matters so much. If you can move even one high-friction permission from permissions to optional_permissions, you remove a warning string from the install dialog and recover a meaningful chunk of lost installs.
Review Implications of Each Permission#
The Chrome Web Store review team uses permissions as a risk signal. Extensions with broader permissions receive deeper scrutiny, longer review times, and more detailed justification requirements.
What Triggers Extended Review#
- Broad host permissions (
<all_urls>,*://*/*, or patterns covering many unrelated domains) almost always trigger manual review. Expect 2 to 5 days instead of hours. webRequestcombined with broad host permissions is a red flag for data exfiltration. Be prepared to explain exactly why you need to observe network traffic.cookiespermission combined with broad host access will get your extension flagged for potential session hijacking concerns. The review team will look at your code carefully.debuggerpermission is the highest-scrutiny API permission. It grants access to the Chrome DevTools protocol, which can read anything on any page. Very few consumer extensions have a legitimate need for it.declarativeNetRequestWithHostAccessis scrutinized because it allows request modification on specific hosts.
The Justification Requirement#
For certain sensitive permissions, the Chrome Web Store now requires a written justification during submission. You must explain why your extension needs each sensitive permission and how it is used. Vague justifications like "needed for core functionality" get rejected. Specific justifications like "the bookmarks permission is required to export the user's bookmarks to a JSON file when they click the Export button in the popup" get approved.
The review team cross-references your justification with your actual code. If you say you need history to show recent pages in a search feature, they will check that your code actually implements that feature and does not exfiltrate history data.
Migrating Permissions from MV2 to MV3#
If you are migrating an existing MV2 extension, the permission changes are one of the trickiest parts of the transition. Here is a systematic approach.
Step 1: Inventory Your Current Permissions#
List every permission in your MV2 manifest. For each one, document which feature uses it and whether that feature works without the permission.
Step 2: Separate Host Patterns from API Permissions#
In MV2, your permissions array might look like this:
{
"manifest_version": 2,
"permissions": [
"tabs",
"storage",
"bookmarks",
"https://api.example.com/*",
"https://*.github.com/*",
"<all_urls>"
]
}In MV3, this needs to be split. But do not just move things mechanically — this is your opportunity to tighten:
{
"manifest_version": 3,
"permissions": [
"storage",
"activeTab"
],
"optional_permissions": [
"bookmarks"
],
"host_permissions": [
"https://api.example.com/*"
],
"optional_host_permissions": [
"https://*.github.com/*"
]
}Notice what changed. tabs was replaced with activeTab because the extension only needed tab access on user gesture. bookmarks moved to optional because it powers a secondary feature. <all_urls> was removed entirely because the extension only actually needs two specific domains. And the GitHub host pattern moved to optional because it powers an integration that not all users enable.
Step 3: Implement Runtime Permission Flows#
For every permission you moved to optional_permissions or optional_host_permissions, add a runtime request flow in your UI. This is the most code-intensive part of the migration, but it directly improves install conversion and review outcomes.
Step 4: Test Permission Revocation#
In MV3, users can revoke host permissions at any time through Chrome's toolbar. Test every feature with permissions revoked and verify your extension degrades gracefully instead of crashing.
Checklist
- Audit every permission in your manifest — remove any that are not actively used in code
- Replace broad host patterns with the narrowest patterns that still work
- Use activeTab instead of tabs or host permissions wherever user gesture is the trigger
- Move non-essential permissions to optional_permissions and implement runtime request flows
- Move non-essential host patterns to optional_host_permissions
- Add chrome.permissions.contains() checks before every optional permission API call
- Handle permission revocation gracefully in the UI instead of crashing
- Write specific justifications for each sensitive permission before submission
- Test the install flow in an incognito profile to see exact warning strings
- Remove the <all_urls> pattern unless your extension genuinely operates on every website
Common Permission Mistakes and How to Fix Them#
Requesting tabs When You Only Need the Active Tab URL#
The tabs permission exposes the url and title properties of all tabs, which triggers the "Read your browsing activity" warning. If you only need the URL of the tab the user is interacting with, activeTab gives you that for free with zero warnings.
Keeping Permissions from Deleted Features#
Extensions accumulate permissions over time as features are added. When features are removed, the permissions often stay. Audit your manifest before every submission.
Using <all_urls> for a Content Script That Runs on Specific Sites#
If your content script only targets github.com and gitlab.com, declare those specific match patterns instead of <all_urls>. You can do this in both static content script declarations and optional_host_permissions for dynamic registration.
Not Testing the Install Experience#
Developers rarely see their own install dialog because they load their extension unpacked during development. Before every submission, create a packaged .crx file or use an unlisted Chrome Web Store listing and go through the actual install flow. See the warning strings with fresh eyes. If they would make you hesitate, they are making your users hesitate too.
Putting It All Together#
The permission model in Manifest V3 rewards precision. Extensions that request exactly what they need, defer non-essential permissions to runtime, and handle revocation gracefully get faster reviews, higher install rates, and fewer support complaints about trust.
The core workflow is simple: start with activeTab and storage, add required permissions only when a feature truly cannot work without them, move everything else to optional, and test the install experience yourself. Every permission you can defer to runtime is a warning string removed from the install dialog, and that directly translates to more users reaching the install button.
For related guides, see our posts on the complete guide to Manifest V3 in 2026, Chrome extension permissions explained, and why your Chrome extension got rejected.
Interactive tool
Permission Preview
Paste your manifest.json and instantly see the exact warning strings Chrome will show users during installation. Identify high-friction permissions before you submit.
Open tool
Interactive tool
Submission Checklist
Walk through every required field, permission justification, and asset before you submit to the Chrome Web Store. Catches permission issues, missing metadata, and common rejection triggers.
Open tool
Continue reading
Related articles
Data Handling Compliance for Extensions
Navigate GDPR, CCPA, and Chrome Web Store data policies for your extension. Covers consent flows, data deletion, privacy manifests, and third-party data sharing rules.
Chrome Web Store Policy Changes in 2026
Stay compliant with every Chrome Web Store policy change in 2026. Privacy Sandbox impact, Manifest V3 deadlines, new disclosure rules, and action checklists.
Inside the Chrome Web Store Review Process in 2026
A detailed look inside Chrome Web Store's review process in 2026. Learn what reviewers check, common rejection reasons, review timelines, and how to pass review on the first try.