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.

Published October 2, 2026 Updated October 2, 2026 7 min read
Table of Contents

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.

Anatomy of a match patternThe scheme part, the host part with an optional leading wildcard subdomain, and the path part where wildcards may appear anywhere.Schemehttps · http · * · file* = http + httpsHost* · example.com · *.example.com*.x includes xPath/* · /docs/* · /*?lang=*must start with /Special<all_urls>includes file:
Wildcards are allowed in exactly three places: the whole scheme, the leading host label, and anywhere in the path.

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.

Patterns and what they matchExample match patterns and whether each matches example.com, www.example.com, http:// pages, a docs path, and a file URL.Patternexample.com/www.example.com/ahttp://example.com/example.com/docs/xhttps://example.com/*YesNoNoYeshttps://*.example.com/*YesYesNoYes*://*.example.com/*YesYesYesYeshttps://*.example.com/docs/*NoNoNoYes
Check each pattern against the URLs you mean and the ones you don't.

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.

How a URL is tested for injectionThe URL must match one of matches, must not match exclude_matches, must match one include_globs entry if present, and must not match exclude_globs; only then is the script injected.matchesany match?exclude_matchesnone match?include_globsany match (if set)?all passexclude_globsnone match?Host permissiongranted?Injectrun_at stage
Patterns choose the sites; globs refine within them.

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.com is invalid; add /*.
  • Wildcards in the middle of hosts. https://*-cdn.example.com/* is invalid.
  • Two slashes for files. file://* matches nothing; use file:///*.
  • Assuming *:// includes file. It does not; only <all_urls> and file patterns 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

  1. Load the extension unpacked; an invalid pattern makes chrome://extensions report the exact key and index.
  2. Visit the apex domain, www, a subdomain and an excluded path, and confirm injection only where intended (console.log from the script, or the Sources panel’s content scripts list).
  3. Run the pattern table in CI.
  4. 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.

Other MV3 Architecture & Extension Lifecycle Resources