1. File structure
Once unpacked from the distribution format, a WebExtension is a directory containing a number of files.
Note: In some operating systems, filenames are case insensitive. This can lead to naming collisions.
1.1. manifest.json
A Manifest file.
1.2. _locales subdirectory
An optional directory containing strings as defined in localization.
1.3. Other files
An extension may also contain other files, such as those referenced in the § 2.2.11 Key content_scripts and § 2.2.9 Key background parts of the manifest.
2. Manifest
A WebExtension must have a manifest file at its root directory.
2.1. Manifest file
A manifest file is a [JSON] document named manifest.json. Malformed JSON files are not supported. Note that some implementors may accept comments, represented by any content following // outside of a JSON string.
2.2. Manifest keys
If manifest keys that are not defined in this specification are specified, implementors must ignore those keys.
If manifest keys that are defined in this specification are specified with a different JSON type than defined in this specification, implementors must ignore those keys.
The following keys must be considered valid:
-
manifest_version: required. -
name: required. -
version: required. -
default_locale: required under some conditions. -
background: optional -
commands: optional -
content_scripts: optional -
content_security_policy: optional -
description: optional -
icons: optional -
optional_permissions: optional -
options_ui: optional -
permissions: optional -
short_name: optional -
web_accessible_resources: optional -
devtools_page: optional -
externally_connectable: optional -
trial_tokens: optional
The following keys must be considered valid in Manifest V3:
-
host_permissions: optional -
optional_host_permissions: optional
2.2.1. Key manifest_version
This key must be present.
2.2.2. Key name
Name of the extension used in the browser’s user interface. This should be the full name used to identify the extension. See also short_name.
This key must be present. This property can be localized.
2.2.3. Key version
This key must be present.
2.2.4. Key permissions
This key may be present.
2.2.5. Key optional_permissions
This key may be present.
2.2.6. Key host_permissions
This key may be present.
2.2.7. Key optional_host_permissions
This key may be present.
2.2.8. Key default_locale
This key must be present if the _locales subdirectory is present, must be absent otherwise.
2.2.9. Key background
This key may be present.
Specify background scripts. For relevant discussion, see https://github.com/w3c/webextensions/issues/282
2.2.10. Key commands
This key may be present.
2.2.11. Key content_scripts
The content_scripts key is a list of items representing content scripts that should be registered.
2.2.12. Key content_security_policy
This key may be present.
2.2.13. Key description
This key may be present.
2.2.14. Key icons
This key may be present.
2.2.15. Key options_ui
This key may be present.
2.2.16. Key short_name
The short name of the extension. This value should be used in contexts where name is too long to use in full. If short_name is not provided, manifest consumers should use a truncated version of name.
This key may be present. This property can be localized.
2.2.17. Key web_accessible_resources
This key may be present.
2.2.18. Key externally_connectable
The externally_connectable key declares which extensions and web pages can establish connections to the extension using runtime.connect() and runtime.sendMessage(). If omitted, all extensions may connect, but no web pages can connect.
A call to runtime.connect() from an external web page or extension triggers the runtime.onConnectExternal event listener to fire. Similarly, a message sent by an external web page or extension with runtime.sendMessage() triggers the runtime.onMessageExternal event listener to fire. These events are only available in a privileged extension context, not in web pages or less-privileged contexts like content scripts.
This key may be present and may include the following optional keys:
2.2.18.1. Key ids
A list of extension IDs that specifies which extensions can communicate with the extension. To allow all extensions to connect, include the wildcard pattern "*".
Note: The default of whether an extension can connect changes based on whether the externally_connectable key is present. If the key is omitted, the default is to allow all extensions to connect. If the key is present, no extensions can connect unless specified in "ids", even if the "ids" key is ommitted.
2.2.18.2. Key matches
A list of match patterns that specifies which web pages can communicate with the extension. If left empty or omitted, no web pages can connect.
Note: Only the document’s own URL is used for matching. There is no fallback to the URL of the document that created it.
2.2.19. Key devtools_page
This key may be present.
2.2.20. Key trial_tokens
The trial_tokens key is an optional list of strings. These may be used by the user agent to enable experimental features for the § 6 Extension origin. The user agent may ignore or warn about any unrecognized, expired or malformed keys but any such entries must not prevent the extension from loading.
2.3. Reserved file names
Filenames beginning with an underscore (_) are reserved for use by user agent.
3. Execution contexts
Extensions can execute JavaScript code, in any of the following execution contexts:
-
An extension context is a realm associated with an extension origin.
-
A privileged extension context is an extension context with access to the full set of extension APIs available to the extension. For example, the background page or worker defined through § 2.2.9 Key background.
-
A content script context is an extension context and isolated world with limited access to a subset of extension APIs. This is the default execution environment of all content scripts of an extension.
-
The main world of a web page not associated with an extension origin is not an extension context. It does not have access to any extension API, except when an extension allows so through § 2.2.18 Key externally_connectable.
Some extension APIs may involve the execution of JavaScript code in contexts other than what is specified above. For example, the userScripts API allows the creation of USER_SCRIPT worlds that are isolated similarly to isolated worlds but with distinct API availability.
3.1. Isolated worlds
A world is a realm with its own global object.
The main world is the realm whose global object is the
associated document’s Window, in which the document’s own scripts run.
This is the realm implied throughout other specifications that assume a single
realm per document.
A document may also have a number of isolated worlds, created by the user agent to run content scripts in a content script context.
An isolated world is a distinct realm whose global object’s
interface is Window, associated with the main world’s document. The
platform objects of this realm are distinct from their counterparts in the
main world, but operate on the same underlying state. These operations
should maintain isolation across realms: no object in an isolated world’s
realm is observable from the main world.
CustomEvent specifies the event.detail attribute that
"must return the value it was initialized to". Event dispatch can cross worlds,
and following this requirement to the letter would result in the exposure of
an object from one realm to another. Potential resolutions include:
-
Throwing at
CustomEventconstruction when a non-primitive value is passed. -
Returning
null. -
Returning a structured clone, per HTML § 2.7 Safe passing of structured data.
-
Returning a redacted version of the object.
4. Unavailable APIs
5. The browser global
window.browser is the primary namespace hosting extension APIs, available to extension contexts.
Although the main world of a web page is not an extension context, it may also contain the browser global to offer access to functionality granted by § 2.2.18 Key externally_connectable.
6. Extension origin
The extension origin is a tuple origin consisting of an extension scheme and an extension-specific host. An extension scheme is a browser-specific scheme reserved for extension use.
7. Localization
The _locales subdirectory of a WebExtension can contain strings for internationalization purposes.
Specify localization handling. [Issue #62]
8. Host permissions
A host permission is a match pattern listed in
host_permissions, or granted at runtime per § 8.2 Granting host permissions. Together
the extension’s granted host permissions form its granted host
permissions.
An extension has access to a URL url if all of the following are true:
-
url is not restricted per § 8.1 Restricted URLs.
-
url is matched by a match pattern in the extension’s granted host permissions, or by a temporary host permission granted through § 12.3 User gestures and activeTab.
Access to a URL is the single gate used throughout this specification
and by the browser APIs for at least:
-
Injecting a content script into a document (see Inject a content script).
-
Observing requests to or from a URL, for example with
webRequest. -
Reading or modifying cookies for a URL, for example with
cookies. -
Exposing a tab’s
url,title, andfavIconUrlto the extension, unless the extension also has thetabspermission. -
Applying a declarativeNetRequest rule that modifies or redirects a matched request (
modifyHeaders,redirect); a rule that only blocks or upgrades a request’s scheme (block,upgradeScheme) does not require host access for the matched URL. Modifying or redirecting a request can be used to relax cross-origin protections (for example via CORS or CSP response headers) or otherwise affect content the extension does not otherwise have access to (w3c/webextensions#782, closed 2025-03-27).
8.1. Restricted URLs
A host permission grants access to a URL only when the URL’s
scheme is one of the schemes permitted in a match pattern;
<all_urls> is no exception. This governs every access path that is
mediated by host permissions (content scripts, and API access such as
webRequest, cookies, and tabs). It does not by itself describe a
browser-specific debugging API such as Chrome’s debugger permission,
which is out of scope for this section; where such an API exists, an
implementation should state its own access boundary rather than relying on
this one, since the two need not coincide. In Chrome specifically, that
debugging API bypasses the requirement that a URL fall within the
extension’s granted host permissions, while remaining subject to the
scheme and specific-host restrictions in this section. External remote
debugging of a browser instance is outside this specification’s scope
regardless.
Implementations additionally restrict specific hosts and schemes beyond what match pattern parsing alone would allow, independently of whether a granted match pattern would otherwise match:
-
Pages served from the extension’s own store or gallery, if the implementation has one.
-
The implementation’s own privileged/internal pages (browser UI pages), except where an implementation-specific flag or a component or policy-installed extension overrides this.
-
Another extension’s own pages, except where an implementation-specific flag or a component or policy-installed extension overrides this.
-
Domains an implementation-specific allowlist marks as always off-limits, for example to protect account-management or extension-store login flows.
Should this specification normatively list restricted hosts and schemes, require the restricted set to be discoverable at runtime, or leave it implementation-defined as today?
No implementation currently exposes the restricted set to an extension at runtime.
8.2. Granting host permissions
A match pattern listed in host_permissions is a required host permission.
An optional host permission is a host permission the extension may hold without it
being granted at install: a pattern listed in optional_host_permissions,
or one listed in host_permissions that the user agent has withheld until
requested. An optional host permission is not part of the extension’s granted host permissions until granted.
Chrome and Firefox both grant required host permissions automatically at install time, then let the user subsequently withhold a granted host permission from a specific site through browser UI, at which point the extension’s effective access to that site reverts to a runtime-request flow even though the permission is still listed as required in the manifest. WebKit’s engine performs no automatic grant at all, for either required or optional host permissions; the embedding application is responsible for presenting required permissions to the user (for example at install or update) and recording the result, and nothing in the engine forces this to happen or guarantees a required host permission is granted by the time the extension runs.
This specification does not require required host permissions to be
granted automatically at install, nor does it require user consent before
every use. The standardized optional_host_permissions manifest key lets
an extension author declare which host permissions are not required for
core functionality and must be requested at runtime via
request(); whether permissions listed in host_permissions
are granted automatically at install, deferred to a runtime consent step,
or made revocable or withholdable by the user is left to the
implementation (w3c/webextensions#119, closed 2022-10-18).
To request a set of host permissions, an extension calls
request() with those match patterns in origins.
This may only be called from a privileged extension context and only
while handling a user activation. The implementation must either add
each requested pattern to the extension’s granted host permissions,
or deny the entire request; a partial grant of a single request()
call is not observable by the extension.
{ "optional_host_permissions" : [ "https://*.example.com/*" ] }
let granted= await browser. permissions. request({ origins: [ "https://*.example.com/*" ] });
8.3. Cross-origin fetch
A request made from a privileged extension context whose target URL is
matched by the extension’s granted host permissions is exempt from Fetch § 3.3 CORS protocol: the request
proceeds, and the response is made available to the requesting script,
without regard to Access-Control-Allow-Origin or any other CORS
response header.
A request made from a content script context is not exempt from
CORS on the basis of the extension’s host permissions. Such a request is
subject to exactly the cross-origin rules that would apply to a request
made by the page the content script is running within: same-origin
requests succeed as normal, and cross-origin requests succeed only if the
target server’s response grants access under Fetch § 3.3 CORS protocol.
A granted host permission does not change this outcome, even
<all_urls>.
-
Chrome removed cross-origin content script fetches as a platform change (Chrome 73 CORB, Chrome 85 CORS enforcement, allowlist fully removed in Chrome 87), independent of manifest version; MV2 and MV3 extensions in current Chrome both get page-origin-only fetch from content scripts.
-
Firefox gates this explicitly on manifest version: an MV2 content script’s
fetch/XMLHttpRequest/WebSocketrun with a principal that includes the extension’s own principal (bypassing CORS via host permissions, per Firefox’s own source comments), while an MV3 content script gets no such override and inherits the page’s ownfetch/XHR. -
Safari’s engine wires its CORS bypass configuration only into extension-page web views, and that wiring does not branch on manifest version at all, unlike a neighboring property in the same code path that does. Content scripts run inside the tab’s own web view, in an isolated script world that never receives the bypass configuration. Nothing in the engine, for either manifest version, ever gives a content script a cross-origin fetch bypass.
Safari’s content-script fetch behavior has therefore never differed by manifest version: content scripts have always been subject to the same cross-origin rules as the page, matching Chrome’s and Firefox’s current (post-MV3-convergence) behavior rather than Firefox’s legacy MV2 exemption.
"host_permissions": ["https://api.example.com/*"] can
call fetch("https://api.example.com/data") from its background service
worker regardless of api.example.com’s CORS headers. The same call made
from a content script injected into https://news.example/ succeeds only
if https://api.example.com/data’s response includes an
Access-Control-Allow-Origin header permitting https://news.example.
9. The permissions API
The permissions API lets an extension check, request, and relinquish its own optional permissions and optional host permissions at runtime, instead of requiring every permission at install time.
An optional permission is a permission the extension may hold
without it being granted at install: one listed in optional_permissions,
or one listed in permissions that the user agent has withheld until the
extension requests it.
A named permission required by the extension’s manifest, or optional and granted to the extension at runtime, is part of the extension’s granted permissions.
9.1. Types
9.1.1. The Permissions dictionary
A Permissions dictionary’s permissions member is a list of named API permission strings,
excluding host permissions. Its origins member is a list of match patterns,
validated using the same match pattern grammar used for the host_permissions manifest key.
Which strings are valid entries in permissions is implementation-defined: each browser
recognizes its own set of permission names, and a name outside a given browser’s set is not a
valid entry for that browser. An entry in permissions or origins that the user agent does
not recognize is an error. No permission is granted or revoked, and the error is surfaced to
the caller.
-
activeTab -
alarms -
clipboardWrite -
contextMenus -
cookies -
declarativeNetRequest -
declarativeNetRequestFeedback -
declarativeNetRequestWithHostAccess -
nativeMessaging -
notifications -
scripting -
storage -
tabs -
unlimitedStorage -
webNavigation -
webRequest
A name outside this list is recognized by at most two of the three, so it is not portable.
-
Chrome: rejects the returned promise.
-
Firefox: throws synchronously.
-
Safari: throws synchronously for
contains(), but rejects the promise forrequest()andremove().
A synchronous throw escapes a .catch() attached to the returned promise, so
browser.permissions.contains(bad).then(ok).catch(err) behaves differently across browsers.
dictionary {Permissions sequence <DOMString >;permissions sequence <DOMString >; };origins
9.2. Methods
[Exposed =WebExtensions ]interface {permissions Promise <Permissions >getAll ();Promise <boolean >contains (optional Permissions );permissions Promise <boolean >request (optional Permissions );permissions Promise <undefined >remove (optional Permissions );permissions readonly attribute PermissionsEvent onAdded ;readonly attribute PermissionsEvent onRemoved ; };
9.2.1. getAll()
getAll() returns the extension’s complete current set of
permissions and match patterns: its granted permissions and granted host permissions.
-
Let result be a new
Permissions. -
Set result["permissions"] to the extension’s granted permissions.
-
Set result["origins"] to the extension’s granted host permissions.
-
Resolve with result.
May getAll() return an all-hosts pattern that no single granted
permission contains, when the granted patterns together cover all hosts?
Safari does, returning *://*/*. Chrome and Firefox return only what was
granted.
9.2.2. contains()
contains(permissions) checks whether the extension’s
granted permissions and granted host permissions include every permission and origin
named in permissions.
-
For each named permission in permissions["permissions"]:
-
If it is not in the extension’s granted permissions, resolve with
false.
-
-
For each match pattern in permissions["origins"]:
-
If it is not subsumed by the extension’s granted host permissions, resolve with
false.
-
-
Resolve with
true.
9.2.3. request()
request(permissions) asks the user to grant the extension
some or all of permissions that are not already part of its granted permissions or
granted host permissions.
-
If this call is not happening within a user activation, reject with an error.
-
If any named permission in permissions["permissions"] is not an optional permission of the extension, reject with an error.
-
If any match pattern in permissions["origins"] is not subsumed by an optional host permission of the extension, reject with an error.
-
If every named permission in permissions["permissions"] is in the extension’s granted permissions, and every match pattern in permissions["origins"] is subsumed by the extension’s granted host permissions, resolve with
true. -
Prompt the user to approve the permissions and origins in permissions that are not already part of the extension’s granted permissions or granted host permissions. Skip this prompt, and grant those permissions and origins automatically, if they carry no additional user-facing warnings beyond what the extension is already granted.
-
If the user does not approve the prompt, resolve with
false. -
Grant the extension the requested permissions and origins not already in its granted permissions or granted host permissions. This fires
onAddedfor the newly granted set. -
Resolve with
true.
What does request() reject with, for a missing user activation, an
unlisted permission, or an unlisted origin? Chrome rejects with an Error:
one fixed message for a missing user activation, and a second, shared
fixed message for an unlisted permission or an unlisted origin. Firefox
rejects with an ExtensionError. Safari reports a callback/promise error
with no named type.
What happens when no window is available to show the prompt in,
separate from the user-activation requirement? Chrome rejects, distinctly
from the no-activation rejection. Firefox has no window requirement and
prompts through whatever surface is available. Safari has no window
concept: the embedding application prompts, and the call resolves false
if it never responds.
Chrome and Safari exclude the entire permissions namespace from content scripts. Firefox
exposes request() alone to content scripts, keeping the rest of the namespace restricted to
background and extension-page contexts.
A user agent may decline a request without prompting the user, for example where administrative policy has already denied the permission.
Does a request declined by policy reject, or resolve false?
Chrome and Firefox reject. Safari resolves false without prompting.
9.2.4. remove()
remove(permissions) relinquishes some or all of the
extension’s optional granted permissions and granted host permissions.
-
If any named permission or match pattern in permissions is not an optional permission or optional host permission of the extension, reject with an error.
-
If any named permission or match pattern in permissions is not declared anywhere in the extension’s manifest, reject with an error.
-
Revoke each named permission in permissions["permissions"] that is in the extension’s granted permissions, and each match pattern in permissions["origins"] that is subsumed by the extension’s granted host permissions. Entries in permissions not granted are ignored rather than treated as an error. This fires
onRemovedfor the revoked set, if any. -
Resolve with
undefined.
remove() resolve with undefined here, when every shipping browser resolves true?
-
All three browsers currently resolve
trueon success; none has been found to resolvefalse. -
w3c/webextensions#1069 (open) argues a boolean that never varies carries no information, and proposes dropping it. The sibling case is
alarms.clearAll(), w3c/webextensions#1055.
request() and contains() are not affected: both resolve a boolean that carries real
information (whether the request was granted, whether the extension has the permission), so
neither changes.
9.3. Events
Should the event-listener pattern (addListener(), removeListener(), hasListener())
used by every event in this specification be defined once, as a shared events.Event type,
rather than duplicated as PermissionsEvent in each namespace that fires one?
PermissionsEvent below is a minimal local stand-in pending that decision.
callback =PermissionsListener undefined (optional Permissions );changed interface {PermissionsEvent undefined (addListener PermissionsListener );listener undefined (removeListener PermissionsListener );listener boolean (hasListener PermissionsListener ); };listener
9.3.1. onAdded
The onAdded event fires with a Permissions
dictionary listing the permissions and origins newly granted to the extension, whenever the
extension’s granted permissions or granted host permissions increase. This includes grants made through
request, and grants made through user agent UI outside of the permissions API,
for example the user agent’s own controls for granting a withheld host permission.
onAdded/onRemoved fire for a permission or host-permission change driven entirely by
administrative or managed policy?
-
Chrome does not fire either event for a policy-driven change.
-
Firefox and Safari fire the event for a policy-driven change, the same as for any other change.
9.3.2. onRemoved
The onRemoved event fires with a Permissions
dictionary listing the permissions and origins no longer granted to the extension, whenever the
extension’s granted permissions or granted host permissions decrease, whether through remove or through user
agent UI.
10. Match patterns
A match pattern is a string used to match URLs.
A match pattern is either the special value <all_urls>, or a string of the
form:
<scheme>://<host><path>
A match pattern a subsumes a match pattern b if every URL matched by b is also matched by a.
10.1. Grammar
10.1.1. scheme
scheme is one of a fixed list of literal scheme names, or *.
A pattern’s scheme is the substring of the match pattern before the
first ://.
The literal scheme values http, https, file, and ftp, plus a browser’s own extension scheme, are valid scheme values in Chrome, Firefox, and Safari alike. http, https,
file, and ftp are four of the URL Standard’s six special schemes; the other two are ws
and wss. data is not a special scheme, which matters for
§ 10.2 <all_urls>: Firefox is the only one of the three browsers that includes data in
<all_urls>.
http, https, file, ftp, and a browser’s own extension scheme,
does a match pattern’s scheme component accept? Firefox additionally accepts ws, wss, and
data. Chrome additionally accepts ws/wss for host_permissions (but not for
content_scripts.matches) and, only in a non-default developer configuration, its own
chrome:// scheme. Safari accepts none of ws, wss, data, or an equivalent internal-pages
scheme in a match pattern at all.
If scheme is *, the pattern matches a fixed, implementation-defined
set of schemes in place of a literal scheme. That set always includes http and https in
Chrome, Firefox, and Safari alike.
Should the * scheme wildcard expand to include ws/wss, matching Firefox, or only
http/https, matching Chrome and Safari?
10.1.2. host
host is one of:
-
*, matching any host. -
*.followed by a domain suffix (containing no*or/), matching that domain and any of its subdomains. -
a literal domain or IP-address literal (containing no
*or/), matching only that exact host.
Those are the only shapes host takes when it contains a * at all; see
§ 10.3 Parsing a match pattern for what a * in any other position does to
parsing.
When scheme is file, host is omitted (the pattern has the form
file://<path> or file:///<path>, with <path> beginning at the third
slash regardless of what, if anything, appears between the second and
third slash).
Host comparison is case-insensitive.
Chrome and Safari match hosts case-insensitively. Firefox does not: it stores the pattern’s
host verbatim and compares it byte-for-byte, so *://EXAMPLE.com/* matches nothing there.
Specifying case-insensitive host matching is a behavior change for Firefox, not a documentation
fix.
Only Chrome does this today. Firefox and Safari store the host verbatim, so a pattern host written with literal Unicode matches nothing in those two browsers: the navigated URL’s host is always Punycode-encoded, and the author has to write the Punycode form themselves.
A port is never written in host; see § 10.3 Parsing a match pattern for what writing one does to parsing.
Does a match pattern need a port component at all? Chrome’s
grammar has one: a literal <host>:<port> matches only that port, and
omitting the port (the common case) matches any port. Safari’s and
Firefox’s grammars have none.
https://*.example.com/* matches https://example.com/,
https://www.example.com/, and https://a.b.example.com/, but not
https://notexample.com/.
10.1.3. path
path begins with /; see § 10.3 Parsing a match pattern for what a
pattern with no path does to parsing.
http://example.com/ has a path of / and is a valid match pattern.
http://example.com, with no trailing slash and therefore no path at
all, fails to parse.
path may contain any number of * characters, each matching zero or
more characters. No other character in path has special meaning; in
particular, unlike a glob, ? is an ordinary literal character in a
match pattern’s path, not a single-character wildcard.
Path matching is case-sensitive in Chrome, Firefox, and Safari (Chrome exposes a case-insensitive matching mode in its API, but no call site in the browser’s own permission or content-script matching code uses it).
Chrome and Firefox match it against the URL’s path and query string together (that is,
against pathname + search). Safari matches it against the path alone; the query
string is excluded.
Chrome decodes percent-encoded octets in both the URL and the pattern’s path before comparing
(falling back to a raw comparison for byte sequences that aren’t valid UTF-8). Firefox and
Safari perform no decoding at all: a pattern containing a literal, unescaped reserved character
will not match a URL where the user agent encoded that same character, in either browser. See
also w3c/webextensions#945, a related but
distinct percent-encoding discussion scoped to declarativeNetRequest’s urlFilter syntax
rather than match patterns.
https://example.com/foo/* matches https://example.com/foo/,
https://example.com/foo/bar, and https://example.com/foo/bar?baz,
but not https://example.com/foobar or https://example.com/foo
(without a trailing slash).
10.2. <all_urls>
The special match pattern <all_urls> matches all URLs whose scheme is
in a permitted set of schemes, regardless of host or path. http and https are in that set
in every engine, in every context that defines one.
Beyond http and https, each browser permits a different subset of the URL Standard’s six
special schemes plus data for <all_urls>:
| scheme | Chrome | Firefox | Safari |
|---|---|---|---|
http
| yes | yes | yes |
https
| yes | yes | yes |
file
| yes | yes | opt-in |
ftp
| yes | yes | no |
ws, wss
| host_permissions only
| yes | no |
data
| no | yes | no |
Safari’s <all_urls> also includes its own extension scheme, shared by all of its
extensions. Chrome’s and Firefox’s do not.
<all_urls> cover a single portable scheme set across browsers, or leave file: and
other non-http(s) schemes as a browser-specific opt-in (Safari’s model) rather than an
unconditional inclusion (Firefox’s model)?
content_scripts.matches list of ["<all_urls>"]:
| URL | Chrome | Firefox | Safari |
|---|---|---|---|
https://example.com/
| runs | runs | runs |
ftp://example.com/
| runs | runs | does not run |
data:text/html,...
| does not run | runs | does not run |
file:///home/user/x.html
| runs only if file access has separately been granted | runs only if file access has separately been granted | runs only if file access has separately been granted |
10.3. Parsing a match pattern
To parse a match pattern given input:
-
If input is
<all_urls>, return<all_urls>. -
If input does not contain
://, return failure. -
Let scheme be the substring of input before the first
://. -
If scheme is not
*, and is not one of the permitted literal scheme values (see § 10.1.1 scheme), return failure. -
Let rest be the substring of input following that
://. -
If scheme is
file:-
If rest contains no U+002F (/), return failure.
-
Let host be the empty string. Any text of rest before its first U+002F (/) is discarded, not read as a host.
-
Let path be the substring of rest starting at its first U+002F (/), inclusive.
-
-
Otherwise:
-
If rest does not contain U+002F (/), return failure.
-
Let host be the substring of rest before its first U+002F (/).
-
If host contains U+003A (:), return failure.
-
If host is not
*, host does not consist of*.followed by one or more characters containing no U+002A (*), and host contains U+002A (*), return failure. -
Let path be the substring of rest starting at its first U+002F (/), inclusive.
-
-
Return a match pattern whose scheme is scheme, whose host is host, and whose path is path.
A file match pattern’s host is never read as a host at all: whatever
text appears between file:// and the next /, if any, is discarded
rather than validated. § 10.4 Matching algorithm separately
skips path comparison for a file pattern.
<all_urls> and https://*.example.com/* both parse successfully.
example.com/* (no ://) and https://example.com (no path) both
fail to parse. http://example.com:8080/* also fails to parse; see
§ 10.1.2 host for whether match patterns take a port
component.
10.4. Matching algorithm
pattern here is either <all_urls> or the result of a successful
parse of a match pattern string; see
§ 10.3 Parsing a match pattern. surface identifies which manifest key or API call is making the
check (for example content_scripts.matches or host_permissions); see
§ 10.1.1 scheme and § 10.2 <all_urls> for how the permitted and *-expansion scheme sets
vary by surface.
To determine whether a match pattern pattern matches a URL url for a manifest surface surface:
-
Let url record be the result of parsing url.
-
If pattern is
<all_urls>:-
If the scheme of url record is in the permitted scheme set for
<all_urls>given surface (see § 10.2 <all_urls>), return true. -
Otherwise, return false.
-
-
If pattern’s scheme is
*:-
If the scheme of url record is not one of the schemes
*expands to for surface, return false.
-
-
Otherwise:
-
If the host of url record does not match pattern’s host, return false.
-
If pattern’s scheme is not
file, and pattern’s path does not match the path of url record (see § 10.1.3 path for what "path" includes), return false. -
Return true.
This algorithm is run against the URL produced by
Determine the URL for matching a document when matching a document
for content_scripts, not necessarily against the document’s literal
URL; it does not itself define how the URL of a document with an
opaque origin is resolved.
Firefox has a stricter mode for host permissions in which a pattern with a wildcard host
(*.example.com, or a bare *) never matches. Chrome and Safari have no equivalent.
*://*.example.com/*:
| URL | Chrome | Firefox | Safari |
|---|---|---|---|
https://www.example.com/
| matches | matches | matches |
http://example.com/
| matches | matches | matches |
wss://example.com/
| does not match | matches | does not match |
Firefox’s * scheme wildcard additionally covers ws/wss.
10.5. Unparseable match patterns
A match pattern string that does not parse does not match any URL.
For a permission-bearing key (permissions, host_permissions,
optional_permissions, optional_host_permissions), an unparseable
entry is dropped and the extension loads with the rest of the list.
Chrome and Firefox record a warning; Safari does not.
content_scripts.matches fail the whole extension’s load, or
only that one entry?
Chrome and Firefox fail the whole load. Safari drops the one unparseable entry (or, if every entry in one content script definition fails, that definition) and still loads the extension.
11. Globs
A glob is a string matched against a URL. It may
contain the wildcard characters *, which matches zero or
more characters, and ?. Every character other than a wildcard
matches only itself, so a glob with no wildcards matches only a URL
identical to it.
Glob matching is case-sensitive.
Does a single ? match exactly one character, or zero or one? The
published specification
says exactly one, and Firefox matches that. In Chrome a run of k
consecutive ? matches 0 to k characters, so a lone ? can match zero.
\ escape the next * or ? in a glob, as in Chrome, or have no special meaning, as
in Firefox?
Chrome treats \ as an escape character for the next * or ?. Firefox has no escape syntax:
a \ in a glob is matched as a literal backslash, and there is no way to write a literal *
or ?.
Chrome includes the fragment. Firefox strips it, so a glob ending in
#section matches in Chrome and never in Firefox.
https://example.com/path?query#frag:
-
*example.com*matches in both Chrome and Firefox. -
*#fragmatches in Chrome, but not in Firefox, because Firefox matches against the URL with its fragment removed.
11.1. Key include_globs
A list of globs. A document matches if the URL matches both the matches field and the include_globs field. If include_globs is empty or not present, it does not restrict matching.
include_globs list restrict nothing, or match nothing?
Chrome treats it as no restriction, and so does Firefox’s userScripts API. Firefox’s
content_scripts manifest key does not: there an empty list matches nothing.
11.2. Key exclude_globs
A list of globs used to specify URLs where the content script does not run, even if the URL matches entries in matches and (if specified) § 11.1 Key include_globs. If exclude_globs is empty or not present, it does not exclude anything.
12. Concepts
12.1. Uniqueness of extension IDs
An extension ID is an opaque string that identifies a WebExtension. Every loaded WebExtension has an extension ID.
A user agent must not have two different currently-loaded WebExtensions with the same extension ID in the same profile. Loading a WebExtension whose extension ID matches an already-loaded WebExtension is an update of that WebExtension, not the addition of a second, distinct one.
An extension ID’s derivation from a WebExtension’s package, manifest, or other input is implementation-defined, as are its accepted character set, length, and case-sensitivity: Chrome, Firefox, and Safari each derive it from different inputs by different mechanisms, and this specification does not require them to converge on one.
bmnlcjabdbnaibedpokdopcffdilbkco (Chrome’s derived form), {daf44bf7-a45e-4450-979c-91cf07434c3a} or my-extension@example.org (Firefox’s authored forms), or any other unique string an embedding application assigns.
An extension ID must be stable for the lifetime of a given installation of a WebExtension: reloading, restarting the user agent, or restarting the device must not change it. Whether the same extension ID is produced again for a separate installation of the same WebExtension source, on the same or a different machine, or when loading the same source both unpacked and packed, depends on how each user agent derives its IDs; this specification leaves that choice to the implementation.
Whether an extension origin’s extension-specific host is the extension ID depends on the user agent.
-
Chrome’s extension origin host is the extension ID directly.
-
Firefox’s extension origin host is a separate, randomly generated identifier, unrelated to the extension ID, generated once per (profile, extension) pair and persisted for the life of that profile, deliberately so that a web page cannot learn whether a given extension is installed by testing its ID against an observable resource origin.
-
Safari’s extension origin host defaults to the extension ID, but the embedding application can set the two independently, so they are not guaranteed to match.
Requiring the extension ID as the extension origin host would foreclose Firefox’s anti-fingerprinting design; w3c/webextensions#868 documents a concrete leak of this kind through a declarativeNetRequest redirect. Tracked at w3c/webextensions#238 and #896.
The extension ID is available to a privileged extension context and content script context of the WebExtension it identifies. An extension may also learn the extension ID of another, cooperating extension through APIs that name it explicitly, such as externally_connectable.
Where a user agent’s extension origin host is the extension ID, a web page that can load a resource from that origin observes the extension ID as that resource’s host.
12.2. Promises and callbacks
An asynchronous method defined by this specification accepts an optional callback function as
its last argument, in addition to the method’s own parameters. Calling the method with that
callback present uses the callback form: the user agent invokes the callback with
the method’s result once it is available, and the method call itself returns undefined.
Calling the method without a trailing callback uses the promise form: the method
returns a promise that resolves with that same result, and no
callback is invoked. A given call uses one form or the other; supplying a callback does not also
produce a usable promise, and omitting one does not also invoke a callback. This holds
independently of whether the extension’s manifest declares Manifest V2 or Manifest V3: neither
form is restricted to one manifest version.
permissions.contains({permissions: ['tabs']}) returns a promise that resolves with a boolean.
permissions.contains({permissions: ['tabs']}, hasTabs => { ... }) instead invokes the callback
with that boolean and returns undefined.
12.2.1. Result value
The callback is invoked with the value the promise would otherwise have resolved with, in the
same argument shape: a method whose promise resolves with a single value invokes its callback
with that value as its sole argument, and a method whose promise resolves with undefined
invokes its callback with no arguments. A method whose result needs more than one callback
argument is instead defined without a promise form at all (see
§ 12.2.3 Methods that support only one form). Firefox’s devtools.inspectedWindow.eval() disagrees
with this pattern: its callback is invoked with two positional arguments, a result and an
exception-info object, while its promise resolves with a single array value bundling the same two
values.
devtools.inspectedWindow.eval() does?
12.2.2. Errors
An asynchronous method that fails communicates the failure differently depending on the form
used. In the promise form, the returned promise rejects with the error. In the
callback form, the callback is invoked the same as it would be on success, with no error
argument added; the error is instead exposed through
runtime.lastError for
the duration of that one callback invocation, and cleared once the callback returns. The callback
avoids a logged warning by reading runtime.lastError while it is set. Otherwise, the warning is
logged once the callback returns.
someNamespacereports a failure the same way. someMethod( argument, result=> { if ( browser. runtime. lastError) { // handle the error } });
someNamespace.someMethod(argument) reports one by rejecting the
promise it returns, just through runtime.lastError instead of a catch.
runtime.lastError, or reference its definition from wherever
the runtime namespace is specified?
12.2.3. Methods that support only one form
Not every asynchronous method supports both forms. Chrome and Firefox each diverge from that expectation, in opposite directions:
| Chrome | Firefox | Safari | |
|---|---|---|---|
| A method supporting only the callback form | Exists, where the method already has a synchronous return value of its own that a promise would collide with | None. | None. |
| A method supporting only the promise form | None. | Exists, across many namespaces: no callback parameter is defined for the method at all | None. |
contextMenus.create() supports only the callback form: it already returns the newly
created item’s ID synchronously, and cannot also return a promise. Several Firefox methods, such
as action.openPopup() and contentScripts.register(), support only the promise form: no
callback parameter exists for them at all.
Chrome’s callback-only methods and Firefox’s promise-only methods are not accidents of one method each: Chrome’s predate promise support and already return a value synchronously, and Firefox’s span many namespaces. Safari has neither exception.
12.3. User gestures and activeTab
A user activation is transient activation of the document
handling the interaction, extended to browser-chrome surfaces that have no
document at all: clicking the extension’s toolbar action, invoking one of
its keyboard commands, choosing one of its context menu
items, or (implementation-defined) selecting it from the address bar. A
user activation triggered from one of those surfaces is scoped to the
extension it was directed at, the same way transient activation is
scoped to a browsing context.
If an extension declares the activeTab permission, then each
time a user activation for that extension occurs while a tab is active,
the extension is granted a temporary host permission for that
tab: for the remainder of the tab’s current navigation, the extension has
access to a URL matching the tab’s current URL as if that URL were
covered by a granted host permission, and the extension is granted
the tabs permission for that tab (exposing its url, title,
and favIconUrl).
A temporary host permission does not persist across navigation of the
tab’s top-level document to a different origin; a fresh user activation
is required after such a navigation. It does not grant access to any
other tab, and it does not add anything to the extension’s granted host permissions as observed by getAll().
activeTab creates revoked? Chrome, Firefox, and Safari disagree:
-
Chrome revokes it when the tab’s main frame completes a committed, cross-origin navigation (same-origin navigations, and any subframe navigation, leave it intact).
-
Safari revokes it when the tab’s main frame commits a URL no longer matched by the granted pattern.
-
Firefox does not revoke it on navigation at all: source comments state this is a deliberate difference from Chrome, and the grant instead tracks the surviving lifetime of the tab’s inner window (including back/forward-cache revival), being cleared only when the extension’s action is invoked again for a different tab or the tab/window is destroyed.
Tracked at w3c/webextensions#657.
A user clicks the extension’s toolbar icon while viewing{ "permissions" : [ "activeTab" ] }
https://example.com/. The extension is granted access to
https://example.com/ for that tab until the tab navigates to a
different origin; it still has no access to any other tab, and no access
to https://example.com/ in a new tab the user opens afterward.
12.4. Extension permissions and web perissions
13. Content security policy
14. Architecture
14.1. Background content
14.2. Content scripts
Content scripts represent a set of JS and CSS files that should be injected into matching pages loaded by the user agent. They are injected using the steps in § 19.2 Inject a content script. Content scripts run in an isolated world by default.
14.2.1. Key matches
A list of match patterns that are used to decide which pages the user agent injects the content script into. This key is required.
14.2.2. Key exclude_matches
A list of match patterns that can be used to specify URLs where the content script should not run, even if the URL matches entries in § 14.2.1 Key matches and (if specified) § 14.2.9 Key include_globs.
14.2.3. Key js
A list of file paths, relative to the extension’s package, that should be injected as scripts.
14.2.4. Key css
A list of file paths, relative to the extension’s package, that should be injected as stylesheets.
14.2.5. Key all_frames
If all_frames is true, the content script must be injected into any subframes that match the other matching criteria for the content script. If false, content scripts will only be injected into top-level documents. Defaults to false.
14.2.6. Key match_about_blank
If this is true, use the URL of the parent frame when matching a child frame whose document URL is about:blank or about:srcdoc. See also § 19.1 Determine the URL for matching a document. Defaults to false.
14.2.7. Key match_origin_as_fallback
If this is true, use fallbacks as described in § 19.1 Determine the URL for matching a document.
No path is available when the URL to match against falls back to an origin. Therefore, when set, the user agent may treat a § 14.2.1 Key matches with a path other than /* as an error.
Defaults to false.
14.2.8. Key run_at
Specifies when the content script should be injected. Valid values are defined by the RunAt enum.
14.2.9. Key include_globs
A list of globs that a document should match. A document matches if the URL matches both the § 14.2.1 Key matches field and the § 14.2.9 Key include_globs field.
14.2.10. Key exclude_globs
A list of globs that can be used to specify URLs where the content script should not run, even if the URL matches entries in § 14.2.1 Key matches and (if specified) § 14.2.9 Key include_globs.
14.2.11. Key world
The world any JavaScript scripts should be injected into. Defaults to ISOLATED. Valid values are defined by the ExecutionWorld enum.
14.2.12. RunAt enum
enum {RunAt ,"document_start" ,"document_end" };"document_idle"
The RunAt enum represents when a content script should be injected.
14.2.13. ExecutionWorld enum
enum {ExecutionWorld ,"ISOLATED" };"MAIN"
The ExecutionWorld enum represents a JavaScript world.
"ISOLATED" corresponds to an isolated world.
"MAIN" corresponds to the main world.
14.3. Extension pages
15. Classes of security risk
16. Web accessible resources
17. Interaction with the web
17.1. Current behavior of cookie partitioning
18. Version number handling
18.1. Key version
The value must be a non-empty string.
version, or scope the key to
the loosest grammar all three already accept without a warning?
Grammar accepted today:
-
Chrome: 1 to 4 dot-separated integers. No leading zero in the first component; a leading zero in a later component is accepted and normalized, with a warning. Anything else fails to load.
-
Firefox: any string. A documented preferred grammar exists and a mismatch warns, but the value is used as-is. Firefox’s comparator also treats a non-numeric part of the value as meaningful rather than invalid (see § 18.4 Comparing version strings).
-
Safari: no grammar at all. Only emptiness is checked, and that does not block loading at the engine level.
Tracked at w3c/webextensions#283.
18.2. Key version_name
An optional string providing a version to show to users in place of
version, for example to include a channel qualifier such
as "1.2 beta". It has no effect on version comparison and is
not required to follow the version string grammar. The key is
OPTIONAL, and an implementation MAY ignore it.
version_name as a manifest key. This makes
version_name a two-of-three feature: Chrome and Safari support it as
display text that falls back to version when absent or empty. Firefox
has stated the omission is deliberate, not an oversight: bugzilla
1380219,
RESOLVED WONTFIX, records the decision, reasoning that the version
string is part of an extension’s identity and a customizable display
string risks confusing users.
18.3. The version string
An extension’s version string is the value of its
version manifest key.
To parse a version string given input:
-
Let parts be the result of strictly splitting input on U+002E (.).
-
If parts contains the empty string, return failure.
-
Let components be an empty list.
-
For each part of parts:
-
Return components.
-
Is the number of components capped (Chrome allows at most 4)?
-
Is each component’s value capped (Chrome allows 0 to 4294967295)?
Firefox and Safari accept a version string that fails this algorithm, storing the value unchanged.
18.4. Comparing version strings
To compare two version strings given versionA and versionB:
-
Let a be the result of parsing versionA.
-
Let b be the result of parsing versionB.
-
If a or b is failure, return failure.
-
Let n be the larger of a’s and b’s size.
-
For each i in the range 0 to n, exclusive:
-
Return "equal".
"4294967295.0" and "1.0" both parse
successfully under Chrome’s grammar and under Firefox’s (Firefox only warns
about the 10-digit first component; it does not reject it). Chrome and
Firefox order them oppositely:
-
Chrome parses the first component as the literal integer 4294967295 (it fits exactly in a 32-bit unsigned value) and so treats
"4294967295.0"as newer than"1.0". -
Firefox’s comparator parses the same component as a signed 32-bit integer; 4294967295 overflows that type, and Firefox silently substitutes 0 on overflow rather than rejecting the value or saturating it. Firefox therefore treats
"4294967295.0"as equal to"0.0", and so older than"1.0".
Chrome and Firefox disagree about which of "4294967295.0" and "1.0" is
newer, for two version strings both accept without error.
Chrome’s comparator performs the numeric tuple compare given above. Firefox and Safari:
-
Firefox: compares each part as a number followed by a string, so it can order pairs Chrome cannot compare at all, such as
1.0a1against1.0b1. A number outside the representable range silently becomes 0. -
Safari: no comparison exists in the engine. The version is stored and returned as an opaque string, and any comparison for update purposes happens in the host application.
18.5. Invalid version strings
A version string that does not parse is
invalid. Chrome, Firefox, and Safari each treat an invalid or empty
version differently from an absent key.
version is invalid, and if so, is the whole
extension rejected, or only the key?
The consequence today, including for the specific case of an empty
version, which the normative requirement above
(§ 18.1 Key version) already forbids:
-
Chrome: the extension fails to load, with an install error. An empty string is included in this: it fails to parse the same as any other unparseable value, so it is rejected, not treated as though the key were absent.
-
Firefox: the extension loads. Any string is used as-is, including an empty one, with a console warning when it does not match the documented preferred grammar. Firefox does not conform to the non-empty requirement above: an empty
versionis accepted and its value retained, not rejected and not treated as though the key were absent. -
Safari: the engine records an error for an empty or otherwise unparseable value but does not itself refuse to load the extension; this is by the engine’s own documented design, which defers the decision to the embedding application rather than leaving it unimplemented. Whether Safari itself then blocks installation on that error is a decision made in Safari’s own application code, which is not part of any open source tree.
18.6. Observability to extensions
Version comparison is not exposed to extension JavaScript as an API in any of the three browsers examined; it is purely an install/update-time concern of the browser.
runtime.getManifest() returns the version string exactly as authored, unmodified, in Chrome, Firefox, and Safari: it reads
directly from the parsed manifest object rather than from any canonicalized
form.
runtime.getVersion() is a required method
(w3c/webextensions#878, closed 2026-04-08).
Whether it returns the literal manifest string or a canonicalized form is
implementation-defined. Chrome canonicalizes its input before returning it,
so calling it can return a string different from runtime.getManifest().version
for the same extension (for example "1.002.3" in the manifest becomes
"1.2.3" from getVersion()); Safari performs no canonicalization, so the
two calls always agree. Firefox does not implement runtime.getVersion()
yet.
19. Algorithms
19.1. Determine the URL for matching a document
To determine the URL to use for matching a document, given the document, match_origin_as_fallback and match_about_blank:
-
Let url be the document’s URL.
-
If the scheme of url is
http,httpsorfile:-
Return url.
-
-
If the scheme of url is
blob,dataorfilesystem, or if url isabout:blankorabout:srcdoc:-
If
match_origin_as_fallbackis set totrue:-
If the document’s origin is a tuple origin:
-
Let document-origin be the serialization of the document’s origin.
-
If the scheme of document-origin is
http,httpsorfile:-
Return document-origin.
-
-
Else, return null.
-
-
Note: If not a tuple origin, the document’s origin is an opaque origin.
-
Let precursor-origin be the serialization of the document’s precursor origin, if any.
"precursor origin" concept needs to be specified. It is not in the HTML spec at the moment. At least Chrome and Firefox recognize the concept, see e.g. https://bugzilla.mozilla.org/show_bug.cgi?id=1715167.
-
If the scheme of precursor-origin is
http,httpsorfile:-
Return precursor-origin.
-
-
Else, return null.
-
-
-
Else, if
match_about_blankis set totrue:-
If url is
about:blankorabout:srcdoc:-
Let opener be the active document of document’s opener browsing context.
-
If all of the following conditions are true:
-
opener is not null
-
opener’s origin is still the same as the document’s opener origin at creation
-
The algorithm has not been repeated for opener yet.
Then repeat the algorithm for opener.
-
-
-
-
-
Return null.
19.2. Inject a content script
If the same extension specifies the same script twice, what should happen? (bug)
To determine if a content script should be injected in a document:
-
Let url be the result of running § 19.1 Determine the URL for matching a document.
-
If the extension does not have access to url, return.
-
If url is not matched by a match pattern in
matches, return. -
If
include_globsis present and url is not matched by any glob pattern, return. -
If url matches an entry in
exclude_matchesorexclude_globs, return. -
If this is a child frame, and
all_framesis nottrue, return. -
Otherwise, inject the content script. This should be done based on the
run_atsetting.