Writing Match Patterns and Globs
Write correct MV3 match patterns: scheme, host and path rules, subdomain wildcards, what all_urls includes, exclude_matches, include_globs and exclude_globs, and testing patterns before you ship.
Table of Contents
- The grammar of a match pattern
- Step-by-step: patterns that match what you mean
- Common mistakes
- Cross-browser variation
- Verification
- FAQ
- Do match patterns match query strings?
- Can a pattern match by port?
- Are patterns case-sensitive?
- How do patterns differ between content scripts and host permissions?
- Can I use a pattern to match every subdomain except one?
- What about internationalised domain names?
- Why does my pattern work for content scripts but fail in externally_connectable?
- Related
The content script runs on www.example.com but not on example.com. A pattern meant for one site’s docs section also matches its marketing pages. The manifest fails to load with “Invalid value for ‘content_scripts[0].matches[0]’”. Match patterns look like URLs with wildcards, but they follow a stricter grammar than a URL or a glob, and every part — scheme, host, path — has its own rules. Getting them wrong either breaks the feature or widens the extension’s reach and its install warning. This guide covers the grammar, the common mistakes, and the glob options that refine patterns further. It belongs to content scripts and DOM injection.
The grammar of a match pattern
A match pattern has the form <scheme>://<host><path>. The scheme is http, https, file, ftp (legacy), or *, which means http or https only — not file. The host is either * (any host), an exact hostname, or *. followed by a hostname, which matches that hostname and any subdomain of it; a * cannot appear anywhere else in the host. The path must start with / and may contain * anywhere, matching any characters; query strings are part of the path for matching purposes. The special pattern <all_urls> matches every URL with a permitted scheme, including file: (subject to the file access toggle). Ports are ignored in host matching in Chrome. Patterns appear in content_scripts.matches, host_permissions, web_accessible_resources.matches, externally_connectable.matches and several APIs — with small differences in what each accepts.
Step-by-step: patterns that match what you mean
1. Cover the apex and subdomains together
1{
2 "matches": [
3 "https://*.example.com/*" // example.com, www.example.com, docs.example.com, a.b.example.com
4 ]
5}
Execution context: the manifest. *.example.com matches the bare domain as well as every subdomain, so it is the usual choice. To match only www, write https://www.example.com/*. A pattern like https://example.*/* or https://*example.com/* is invalid — the wildcard may only be a whole leading label.
2. Always include a path
1// Invalid: missing path → manifest fails to load
2"matches": ["https://example.com"]
3
4// Valid
5"matches": ["https://example.com/*"]
Execution context: the manifest. The path is mandatory. /* matches every path on the host. / alone matches only the root page — rarely what you want for content scripts, and sometimes surprising: https://example.com/ does not match https://example.com/about.
3. Narrow with exclude_matches
1{
2 "content_scripts": [{
3 "matches": ["https://*.example.com/*"],
4 "exclude_matches": ["https://accounts.example.com/*", "https://*.example.com/checkout/*"],
5 "js": ["enhance.js"]
6 }]
7}
Execution context: the manifest. exclude_matches uses the same grammar and removes URLs from the set. It is the right tool for keeping content scripts away from sign-in pages, payment flows and admin areas — both for safety and so a bug in your script cannot break a checkout. Exclusions do not reduce the host permission or the install warning; they only affect injection.
4. Refine with globs where patterns cannot express it
1{
2 "content_scripts": [{
3 "matches": ["https://*.example.com/*"],
4 "include_globs": ["*example.com/*/issues/*", "*example.com/*/pull/*"],
5 "exclude_globs": ["*?print=1*"],
6 "js": ["issues.js"]
7 }]
8}
Execution context: the manifest. Globs apply after match patterns and use simpler syntax: * matches any characters and ? matches exactly one, anywhere in the full URL including scheme and host. A URL must match a pattern, then at least one include_globs entry (if any are given), and no exclude_globs entry. Globs are handy for path shapes like “any repository’s issues page” that match-pattern paths cannot express precisely. They are evaluated per navigation, so keep them few.
5. Choose schemes deliberately
1"matches": ["https://*.example.com/*"] // prefer https only
2// "*://*.example.com/*" also matches http — only if the site still serves http pages you need
3// "<all_urls>" also matches file:// — almost never needed for a site-specific feature
Execution context: the manifest. * in the scheme position adds http, which widens exposure to unencrypted pages that could be tampered with in transit. <all_urls> adds file: and, in permissions, triggers the broadest install warning. Use the narrowest scheme that works.
6. Test patterns before shipping
1// Quick checker for development — mirrors Chrome's grammar closely enough to catch mistakes
2export function matchesPattern(pattern, url) {
3 if (pattern === "<all_urls>") return /^(https?|file|ftp):/.test(url);
4 const m = /^(\*|https?|file|ftp):\/\/(\*|\*\.[^/*]+|[^/*]+)?(\/.*)$/.exec(pattern);
5 if (!m) throw new Error(`invalid pattern: ${pattern}`);
6 const [, scheme, host = "", path] = m;
7 const u = new URL(url);
8 const okScheme = scheme === "*" ? /^https?:$/.test(u.protocol) : u.protocol === `${scheme}:`;
9 const okHost = host === "*" || host === u.hostname || (host.startsWith("*.") && (u.hostname === host.slice(2) || u.hostname.endsWith(host.slice(1))));
10 const re = new RegExp("^" + path.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$");
11 return okScheme && okHost && re.test(u.pathname + u.search);
12}
Execution context: a unit test in Node. A table of URLs that must and must not match each pattern, run in CI, catches regressions when someone edits the manifest. For the browser’s exact behaviour, chrome.permissions.contains({ origins: [url] }) and loading the extension against fixture pages are the ground truth.
7. Keep patterns consistent across manifest keys
Content script matches, host_permissions, web_accessible_resources.matches and dynamic registrations often need the same sites. Define them once in a build-time constant and generate each key from it, so adding a site does not leave one key behind — a mismatch that shows up as a content script that injects but cannot load its own fonts, or a host permission requested for a site the script never runs on.
Common mistakes
- Missing path.
https://example.comis invalid; add/*. - Wildcards in the middle of hosts.
https://*-cdn.example.com/*is invalid. - Two slashes for files.
file://*matches nothing; usefile:///*. - Assuming
*://includes file. It does not; only<all_urls>andfilepatterns do. - Patterns broader than the feature. They widen the install warning and review scrutiny.
Cross-browser variation
- Chrome / Edge: grammar as described; ports ignored in host matching; globs supported.
- Firefox: same grammar; Firefox matches ports when specified in some contexts and supports
match_about_blank; globs supported. - Safari: same core grammar; Safari’s per-site permission prompts use the hosts from your patterns, so narrow patterns produce clearer prompts.
Verification
- Load the extension unpacked; an invalid pattern makes
chrome://extensionsreport the exact key and index. - Visit the apex domain,
www, a subdomain and an excluded path, and confirm injection only where intended (console.logfrom the script, or the Sources panel’s content scripts list). - Run the pattern table in CI.
- Check the install warning (pack the extension and drag it in) lists only the hosts you expect.
FAQ
Do match patterns match query strings?
The path part includes the query string for content script matching, so /*?tab=* can be used. Fragments are never matched.
Can a pattern match by port?
Not reliably in Chrome; ports are ignored. Filter in the script if the port matters.
Are patterns case-sensitive?
Hosts are compared case-insensitively; paths are case-sensitive.
How do patterns differ between content scripts and host permissions?
The grammar is the same, but the effect differs. host_permissions decides what the extension may access and drives the install warning; content_scripts.matches decides where scripts are injected and implicitly needs matching host access. A content script whose matches are broader than the granted hosts simply does not inject on the ungranted ones.
Can I use a pattern to match every subdomain except one?
Match the wildcard and exclude the one: matches: ["https://*.example.com/*"] with exclude_matches: ["https://admin.example.com/*"].
What about internationalised domain names?
Write them in their punycode form (xn--…) in patterns; the browser compares the ASCII form of the host.
Why does my pattern work for content scripts but fail in externally_connectable?
externally_connectable.matches is stricter: it rejects patterns that are too broad, such as <all_urls> or wildcards over public suffixes, because it grants web pages the ability to message your extension. Name specific second-level domains there.
Related
- Running content scripts on file URLs and special pages — the special-scheme cases.
- The review cost of all_urls and broad host patterns — what broad patterns cost.
- Declarative vs programmatic content script registration — where patterns are used.
- Content scripts and DOM injection — the parent topic.