host_permissions vs permissions in Manifest V3: which key takes what
7 min readModerok team
Why Manifest V3 splits permissions and host_permissions, what the optional keys add, and the match pattern and update rules that still break after the fix.
In Manifest V3, permissions holds API names and host_permissions holds match patterns. They are different keys, and in Chrome a URL pattern in the wrong one buys you nothing. Chrome's migration guide says it in one sentence: "Host permissions in Manifest V3 are a separate field; you don't specify them in "permissions" or in "optional_permissions"." The working shape:
{
"manifest_version": 3,
"permissions": ["storage", "tabs"],
"optional_permissions": ["topSites"],
"host_permissions": ["https://api.example.com/*"],
"optional_host_permissions": ["https://*/*"]
}
Two axes: API permission or host permission, required at install or requested at runtime.
The split, in the words of the docs
Chrome's "Declare permissions" page defines five keys, and nothing else decides where a string goes:
"permissions": "Contains items from a list of known strings. Changes may trigger a warning.""host_permissions": "Contains one or more match patterns that give access to one or more hosts. Changes may trigger a warning.""content_scripts.matches": "Contains one or more match patterns that allows content scripts to inject into one or more hosts. Changes may trigger a warning.""optional_permissions"and"optional_host_permissions": "Granted by the user at runtime, instead of at install time."
The "list of known strings" is the permissions reference list: storage, tabs, scripting, alarms, cookies and the rest. Anything not on it does not belong in permissions.
Put a pattern there anyway and Chrome raises an install warning. Oliver Dunk of Chrome DevRel quotes it on the chromium-extensions group: "Permission '<all_urls>' is unknown or URL pattern is malformed." He hedges what follows: "this should mean the permission is ignored and the extension continues to function", and "you should definitely avoid putting URLs in the permissions array."
What a host permission buys, and what it does not
Host permissions are not a generic unlock. Chrome's list of what needs them is specific: fetch() from the extension service worker and extension pages, reading the sensitive tabs properties (url, title, favIconUrl), programmatic content script injection, webRequest, cookies, and redirecting or modifying requests and response headers with declarativeNetRequest.
The same page lists cases where they are not required, and two cut across that list: "Using the chrome.tabs API to access and modify tabs, including access to the tab's URL if you have the tabs permission", and "Allowing the injection of a content script declared in the manifest." So the sensitive tab properties have two routes, a host pattern or the tabs permission, and a declared content script needs neither.
Two more traps:
An API permission alone is often not enough. The docs: "Some Chrome APIs require host permissions in addition to their own API permissions, which are documented on each reference page." cookies with no matching host pattern is the classic case, covered in chrome.cookies.get returns null.
The fetch() entry is about CORS, not permission to connect. MDN's host_permissions page describes the grant as "XMLHttpRequest and fetch access to those origins without cross-origin restrictions, but not for requests from content scripts." Two things follow: an endpoint that already returns the right CORS headers needs no pattern, and a content script's own requests stay subject to the page's rules whatever you declare, so cross-origin work belongs in the service worker.
Match pattern rules that catch people out
Each entry is a match pattern, and the syntax has rules worth knowing:
- The path is required and then thrown away. The docs: "For host permissions, the path is required but ignored. The wildcard (
/*) should be used by convention." The page listshttps://*/*andhttps://*/as equivalent, so a bare slash is enough, and a narrow path narrows nothing:"https://api.example.com/v2/*"grants the whole origin. - No top level domain wildcards. "Chrome doesn't support match patterns for top Level domains (TLD). Specify your match patterns within individual TLDs, as in
http://google.es/*andhttp://google.fr/*." The underlying rule is that a host wildcard "must be the first or only character, and it must be followed by a period (.) or forward slash (/)", sohttp://google.*/*is malformed, not merely unmatched.
Optional, and the permissions that cannot be
The real reason to care about the optional keys is upgrades: "Easier upgrades: When you upgrade your extension, Chrome won't disable it for your users if the upgrade adds optional rather than required permissions."
Request them in a click handler, never at startup: "Permissions must be requested from inside a user gesture, like a button's click handler".
document.querySelector('#enable-suggestions').addEventListener('click', async () => {
// topSites is in optional_permissions; the origin is a subset of
// the optional_host_permissions pattern declared above.
const granted = await chrome.permissions.request({
permissions: ['topSites'],
origins: ['https://suggest.example.com/*']
});
if (granted) enableSuggestions();
});
Three rules govern what you can pass. Nothing undeclared: the rule for request() is written about permissions in general: they "must either be defined in the optional_permissions field of the manifest or be required permissions that were withheld by the user", and in Manifest V3 an optional host pattern goes in optional_host_permissions instead. Subsets are accepted: "You can request subsets of optional origin permissions; for example, if you specify *://*/* in the optional_permissions section of the manifest, you can request http://example.com/", which is why the docs suggest declaring "https://*/*" to "request hosts that you only discover at runtime". And paths are ignored here too: "Paths on origin patterns will be ignored."
Some permissions cannot be optional at all. "Most Chrome extension permissions can be specified as optional, with the following exceptions", and the table lists debugger, declarativeNetRequest, devtools, geolocation, mdns, proxy, tts, ttsEngine and wallpaper. So declarativeNetRequest behind a runtime prompt is not an option in Chrome.
What still breaks after you move the key
Declared is not granted. Users control site access per extension, and request() explicitly handles "required permissions that were withheld by the user". So check first with chrome.permissions.contains({ origins: ['https://api.example.com/*'] }) and treat false as a normal state, not an error. Since Chrome 133, chrome.permissions.addHostAccessRequest() can signal a request to the user instead, though it "will only be signaled to the user if extension can be granted access to the host in the request" and the "Request will be reset on cross-origin navigation."
Changing a pattern can disable your extension mid-update. "Adding or changing match patterns in the "host_permissions" and "content_scripts.matches" fields of the manifest file will also trigger a warning", and the consequence: "When an extension adds a new permission that triggers a warning it may temporarily disable it. The extension will be re-enabled only after the user agrees to accept the new permission." The rule is any change, so narrowing is not automatically safe either.
activeTab may remove the question for page access. Chrome scopes it as "an alternative for many uses of "<all_urls>"", not a replacement for host permissions in general. It "grants temporary host permission to the site the user is on" and "displays no warning message during installation", enabled by an action, a context menu item, a commands shortcut or an omnibox suggestion. Access "is revoked when the user navigates away or closes the tab". We compared the options in the "read and change all your data" warning.
Firefox differences
Firefox accepts the same keys with three differences that matter.
Host permissions are revocable rather than fixed, which MDN states for browsers generally: "Users can grant or revoke host permissions on an ad hoc basis. Therefore, most browsers treat host_permissions as optional." What is Firefox-specific is the prompt. "Until Firefox 126, a Manifest V3 extension's requested host permissions weren't displayed in the install prompt. From Firefox 127, host permissions listed in host_permissions and content_scripts are displayed in the install prompt. However, if an extension update requests new host permissions, these are not shown to the user."
The optional key arrived later, and the old one still works there. "Firefox introduced optional_host_permissions in release 128", and "allows the continued use of optional_permissions to specify optional hosts. Use of optional_host_permissions, however, is recommended." So the rule at the top of this post, that a host pattern in optional_permissions does nothing, is Chrome's alone.
And Firefox allows as optional several permissions Chrome does not. MDN lists debugger, declarativeNetRequest, devtools, geolocation and proxy among its optional API permissions, every one of which sits on Chrome's cannot-be-optional table. Firefox also grants some silently, including activeTab, cookies, idle, tabGroups and webRequest, so the same manifest can produce a different consent flow in each browser.
Auditing the rest of your manifest
Once the keys are sorted, ask which feature each entry exists for: every pattern in host_permissions is a string a reviewer and a user read. As above, an endpoint that already sends the right CORS headers needs no pattern. Moderok is one example: its setup asks for the storage permission and no host pattern, as the manifest and permissions guide walks through.