Web Extensions

Draft Community Group Report,

More details about this document
This version:
https://w3c.github.io/webextensions/specification/
Issue Tracking:
GitHub
Inline In Spec
Editors:
(Microsoft Corporation)
(Mozilla)
(Google)
(Apple)

Abstract

[Placeholder] Abstract.

Status of this document

This specification was published by the WebExtensions Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

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:

The following keys must be considered valid in Manifest V3:

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:

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.

For example, 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:

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.

Examples of extension schemes include chrome-extension, moz-extension, and safari-web-extension.

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:

  1. url is not restricted per § 8.1 Restricted URLs.

  2. 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:

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:

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, Firefox, and Safari arrived at this content-script rule differently:

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.

An extension with "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.

These sixteen permission names are recognized by Chrome, Firefox and Safari alike:

A name outside this list is recognized by at most two of the three, so it is not portable.

Should an unrecognized name be signalled by throwing synchronously or by rejecting the returned promise?

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.

  1. Let result be a new Permissions.

  2. Set result["permissions"] to the extension’s granted permissions.

  3. Set result["origins"] to the extension’s granted host permissions.

  4. 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.

  1. For each named permission in permissions["permissions"]:

    1. If it is not in the extension’s granted permissions, resolve with false.

  2. For each match pattern in permissions["origins"]:

    1. If it is not subsumed by the extension’s granted host permissions, resolve with false.

  3. 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.

  1. If this call is not happening within a user activation, reject with an error.

  2. If any named permission in permissions["permissions"] is not an optional permission of the extension, reject with an error.

  3. If any match pattern in permissions["origins"] is not subsumed by an optional host permission of the extension, reject with an error.

  4. 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.

  5. 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.

  6. If the user does not approve the prompt, resolve with false.

  7. Grant the extension the requested permissions and origins not already in its granted permissions or granted host permissions. This fires onAdded for the newly granted set.

  8. 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.

  1. 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.

  2. If any named permission or match pattern in permissions is not declared anywhere in the extension’s manifest, reject with an error.

  3. 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 onRemoved for the revoked set, if any.

  4. Resolve with undefined.

Why does remove() resolve with undefined here, when every shipping browser resolves true?

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.

Should onAdded/onRemoved fire for a permission or host-permission change driven entirely by administrative or managed policy?

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>.

Which schemes, beyond 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:

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.

Should match pattern host comparison be 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.

Should a match pattern’s host be canonicalized from Unicode to Punycode at parse time?

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).

Should a match pattern’s path component match against the URL’s path alone, or against path and query together?

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.

Should match pattern comparison decode percent-encoded octets before comparing, as Chrome does, or compare byte-for-byte, as Firefox and Safari do?

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.

Should <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)?
Given a 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:

  1. If input is <all_urls>, return <all_urls>.

  2. If input does not contain ://, return failure.

  3. Let scheme be the substring of input before the first ://.

  4. If scheme is not *, and is not one of the permitted literal scheme values (see § 10.1.1 scheme), return failure.

  5. Let rest be the substring of input following that ://.

  6. If scheme is file:

    1. If rest contains no U+002F (/), return failure.

    2. Let host be the empty string. Any text of rest before its first U+002F (/) is discarded, not read as a host.

    3. Let path be the substring of rest starting at its first U+002F (/), inclusive.

  7. Otherwise:

    1. If rest does not contain U+002F (/), return failure.

    2. Let host be the substring of rest before its first U+002F (/).

    3. If host contains U+003A (:), return failure.

    4. 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.

    5. Let path be the substring of rest starting at its first U+002F (/), inclusive.

  8. 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:

  1. Let url record be the result of parsing url.

  2. If pattern is <all_urls>:

    1. If the scheme of url record is in the permitted scheme set for <all_urls> given surface (see § 10.2 <all_urls>), return true.

    2. Otherwise, return false.

  3. If pattern’s scheme is *:

    1. If the scheme of url record is not one of the schemes * expands to for surface, return false.

  4. Otherwise:

    1. If the scheme of url record is not equal to pattern’s scheme, return false.

  5. If the host of url record does not match pattern’s host, return false.

  6. 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.

  7. 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.

Should a pattern with a wildcard host be treated differently when granting host permissions than when matching content scripts?

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.

Given the pattern *://*.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.

Should a user agent record a warning when it drops an unparseable entry from a permission-bearing key?

Chrome and Firefox record a warning; Safari does not.

Should an unparseable entry in 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.

Should \ 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 ?.

Should a glob be matched against the full URL, or the URL without its fragment?

Chrome includes the fragment. Firefox strips it, so a glob ending in #section matches in Chrome and never in Firefox.

Given the URL https://example.com/path?query#frag:

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.

Should an empty 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.

Depending on the user agent, an extension ID can look like 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.

Must the extension origin host be the extension ID?

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.

Does this specification require a method’s callback and promise forms to agree on result shape, or may a method define them differently the way Firefox’s 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.

someNamespace.someMethod(argument, result => {
  if (browser.runtime.lastError) {
    // handle the error
  }
});
reports a failure the same way someNamespace.someMethod(argument) reports one by rejecting the promise it returns, just through runtime.lastError instead of a catch.
Should this specification define 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.
Chrome’s 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.
Does this specification require every asynchronous method it defines to support both forms, or does it need to leave room for a method that supports only one?

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().

When is the temporary grant activeTab creates revoked? Chrome, Firefox, and Safari disagree:

Tracked at w3c/webextensions#657.

{
  "permissions": ["activeTab"]
}
A user clicks the extension’s toolbar icon while viewing 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

18. Version number handling

18.1. Key version

The value must be a non-empty string.

Should this specification require Chrome’s strict grammar for version, or scope the key to the loosest grammar all three already accept without a warning?

Grammar accepted today:

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.

Firefox does not implement 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:

  1. Let parts be the result of strictly splitting input on U+002E (.).

  2. If parts contains the empty string, return failure.

  3. Let components be an empty list.

  4. For each part of parts:

    1. If part does not consist entirely of ASCII digits, return failure.

    2. If part’s length is greater than 1 and part’s first code point is U+0030 (0), return failure.

    3. Append the result of interpreting part as a base-ten integer to components.

  5. Return components.

"1.2.3" parses to «1, 2, 3». "01.2" and "1..2" both fail to parse.

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:

  1. Let a be the result of parsing versionA.

  2. Let b be the result of parsing versionB.

  3. If a or b is failure, return failure.

  4. Let n be the larger of a’s and b’s size.

  5. For each i in the range 0 to n, exclusive:

    1. Let x be a[i] if i is less than a’s size, otherwise 0.

    2. Let y be b[i] if i is less than b’s size, otherwise 0.

    3. If x is greater than y, return "versionA is newer".

    4. If x is less than y, return "versionB is newer".

  6. Return "equal".

This algorithm returns failure when either input does not parse. What a user agent does with that failure is a consequence of § 18.5 Invalid version strings, not of this algorithm.
Comparing "1.2" and "1.2.0" returns "equal": the missing third component of "1.2" is treated as 0.
"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 and Firefox disagree about which of "4294967295.0" and "1.0" is newer, for two version strings both accept without error.

Should this specification require one version-comparison algorithm, permit either of the two in use today, or leave comparison implementation-defined and unobservable to extensions?

Chrome’s comparator performs the numeric tuple compare given above. Firefox and Safari:

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.

Must an implementation reject an extension whose 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:

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:

  1. Let url be the document’s URL.

  2. If the scheme of url is http, https or file:

    1. Return url.

  3. If the scheme of url is blob, data or filesystem, or if url is about:blank or about:srcdoc:

    1. If match_origin_as_fallback is set to true:

      1. If the document’s origin is a tuple origin:

        1. Let document-origin be the serialization of the document’s origin.

        2. If the scheme of document-origin is http, https or file:

          1. Return document-origin.

        3. Else, return null.

      2. Note: If not a tuple origin, the document’s origin is an opaque origin.

        1. 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.

        2. If the scheme of precursor-origin is http, https or file:

          1. Return precursor-origin.

        3. Else, return null.

    2. Else, if match_about_blank is set to true:

      1. If url is about:blank or about:srcdoc:

        1. Let opener be the active document of document’s opener browsing context.

        2. 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.

  4. 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:

  1. Let url be the result of running § 19.1 Determine the URL for matching a document.

  2. If the extension does not have access to url, return.

  3. If url is not matched by a match pattern in matches, return.

  4. If include_globs is present and url is not matched by any glob pattern, return.

  5. If url matches an entry in exclude_matches or exclude_globs, return.

  6. If this is a child frame, and all_frames is not true, return.

  7. Otherwise, inject the content script. This should be done based on the run_at setting.

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[ECMASCRIPT]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/multipage/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[JSON]
T. Bray, Ed.. The JavaScript Object Notation (JSON) Data Interchange Format. December 2017. Internet Standard. URL: https://www.rfc-editor.org/info/rfc8259/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Non-Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[WEBEXTENSIONS-BROWSER-GLOBAL]
Patrick Kettner. window.browser. CG-DRAFT. URL: https://w3c.github.io/webextensions/specification/window.browser.html

IDL Index

dictionary Permissions {
  sequence<DOMString> permissions;
  sequence<DOMString> origins;
};

[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;
};

callback PermissionsListener = undefined (optional Permissions changed);

interface PermissionsEvent {
  undefined addListener(PermissionsListener listener);
  undefined removeListener(PermissionsListener listener);
  boolean hasListener(PermissionsListener listener);
};

enum RunAt {
    "document_start",
    "document_end",
    "document_idle"
};

enum ExecutionWorld {
    "ISOLATED",
    "MAIN"
};

Issues Index

Specify background scripts. For relevant discussion, see https://github.com/w3c/webextensions/issues/282 ↵
Specify localization handling. [Issue #62] ↵
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? ↵
Should an unrecognized name be signalled by throwing synchronously or by rejecting the returned promise?

A synchronous throw escapes a .catch() attached to the returned promise, so browser.permissions.contains(bad).then(ok).catch(err) behaves differently across browsers.

↵
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. ↵
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. ↵
Does a request declined by policy reject, or resolve false? Chrome and Firefox reject. Safari resolves false without prompting. ↵
Why does remove() resolve with undefined here, when every shipping browser resolves true?

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.

↵
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? ↵
Should onAdded/onRemoved fire for a permission or host-permission change driven entirely by administrative or managed policy? ↵
Which schemes, beyond 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. ↵
Should the * scheme wildcard expand to include ws/wss, matching Firefox, or only http/https, matching Chrome and Safari? ↵
Should match pattern host comparison be 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.

↵
Should a match pattern’s host be canonicalized from Unicode to Punycode at parse time?

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.

↵
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. ↵
Should a match pattern’s path component match against the URL’s path alone, or against path and query together?

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.

↵
Should match pattern comparison decode percent-encoded octets before comparing, as Chrome does, or compare byte-for-byte, as Firefox and Safari do?

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.

↵
Should <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)? ↵
Should a pattern with a wildcard host be treated differently when granting host permissions than when matching content scripts?

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.

↵
Should a user agent record a warning when it drops an unparseable entry from a permission-bearing key?

Chrome and Firefox record a warning; Safari does not.

↵
Should an unparseable entry in 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.

↵
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. ↵
Should \ 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 ?.

↵
Should a glob be matched against the full URL, or the URL without its fragment?

Chrome includes the fragment. Firefox strips it, so a glob ending in #section matches in Chrome and never in Firefox.

↵
Should an empty 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.

↵
Must the extension origin host be the extension ID?

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.

↵
Does this specification require a method’s callback and promise forms to agree on result shape, or may a method define them differently the way Firefox’s devtools.inspectedWindow.eval() does? ↵
Should this specification define runtime.lastError, or reference its definition from wherever the runtime namespace is specified? ↵
Does this specification require every asynchronous method it defines to support both forms, or does it need to leave room for a method that supports only one?

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.

↵
When is the temporary grant activeTab creates revoked? Chrome, Firefox, and Safari disagree:

Tracked at w3c/webextensions#657.

↵
Should this specification require Chrome’s strict grammar for version, or scope the key to the loosest grammar all three already accept without a warning?

Grammar accepted today:

Tracked at w3c/webextensions#283.

↵

Firefox and Safari accept a version string that fails this algorithm, storing the value unchanged.

↵
Should this specification require one version-comparison algorithm, permit either of the two in use today, or leave comparison implementation-defined and unobservable to extensions?

Chrome’s comparator performs the numeric tuple compare given above. Firefox and Safari:

↵
Must an implementation reject an extension whose 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:

↵
"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 same extension specifies the same script twice, what should happen? (bug) ↵