C.W.K.
Stream
Lesson 03 of 05 · published

Host Permissions — URL Match Patterns and the Install Warning

~11 min · host_permissions, url-pattern, all_urls, install-warning

Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Every host pattern in your manifest is a square mile of internet you're asking permission to touch. Lesson 3 is the syntax, the install prompt cost, and how to design ClipDeck for the maximum reach with the minimum scary warning."

URL Match Pattern Syntax

A match pattern is three parts: scheme, host, path. Joined with :// and /:

<scheme>://<host>/<path>

Examples:

  • https://github.com/* — github.com root and all paths, HTTPS only.
  • https://*.github.com/* — github.com AND any subdomain (gist.github.com, api.github.com, ...).
  • https://github.com/anthropics/* — only repositories under the anthropics org.
  • *://*/* — any scheme except those Chrome restricts (chrome://, etc.), any host, any path. Equivalent to <all_urls> minus a few edge cases.
  • <all_urls> — the explicit wildcard. Matches HTTP, HTTPS, FTP, file:.

Rules:

  • Scheme: http, https, file, ftp, or *. chrome:// and chrome-extension:// are never matchable from a regular extension.
  • Host: literal, OR * alone (any host), OR *. prefix (subdomain wildcard). Wildcards in the middle of the host (foo*.example.com) are not allowed.
  • Path: literal characters and * wildcard. Path must be present; use /* for "any path."

The Install Warning Ladder

Chrome's prompt scales with how broad your hosts are:

  • One specific host (https://github.com/*) → "Read and change your data on github.com."
  • A subdomain wildcard (https://*.github.com/*) → "Read and change your data on sites in the github.com domain."
  • Several specific hosts (https://github.com/*, https://gitlab.com/*) → "Read and change your data on github.com and gitlab.com." Chrome lists them up to a small limit, then collapses.
  • <all_urls> or *://*/* → the loud "Read and change all your data on all websites."

The drop in user trust between "three hosts" and "all websites" is steep. Cluster your hosts narrowly when you can, and reserve <all_urls> for the case where you really do need to work everywhere.

The Chrome 'Runtime Host Permissions' Twist

Since around Chrome 70, users can change site access for any installed extension via the puzzle-piece menu or chrome://extensions: "On click," "On <specific site>," or "On all sites." Even if your manifest declares host_permissions: ["<all_urls>"], the user might restrict you to "On click" only — making your extension behave like an activeTab one.

Implication: your code can't assume manifest-declared host permission is the runtime state. Use chrome.permissions.contains when it matters, or design every host-touching feature to be resilient to denial (you get back a 'Cannot access contents of the page' error from scripting calls; catch it and explain).

The Content-Script Match Pattern

Content scripts have their own matches array under content_scripts. It uses the same syntax but is a separate grant from host_permissions:

  • host_permissions grants you the right to programmatically inject (chrome.scripting), fetch, and observe requests.
  • content_scripts.matches declares static, automatic injection on every page load.

Chrome unions the two for warning purposes: host_permissions: ["<all_urls>"] OR content_scripts.matches: ["<all_urls>"] trigger the same scary warning. If you want narrower hosts, narrow both fields together.

ClipDeck's Choice

ClipDeck v0.9 has content_scripts.matches: ["<all_urls>"] because the floating button and selection capture should work on any page the user visits. Two ways to soften the warning:

  • Drop the always-on content script entirely. Use activeTab + programmatic chrome.scripting.executeScript on toolbar click or hotkey. Cost: no floating button on every page, only after user invokes. Benefit: no install warning, much friendlier.
  • Keep the content script but explicitly exclude sensitive hosts. Keeps the broad reach, signals to the user that some categories are off-limits. The warning is unchanged, but the trust signal is stronger.

ClipDeck v1 takes the second path because the floating button is part of the discovery story. v2 might add a per-user toggle to switch to activeTab-only mode for the privacy-minded crowd.

Match patterns: scheme + host + path. Narrower hosts = friendlier install warning. <all_urls> is the loudest line in any prompt — earn it. The user can downgrade your hosts at runtime; design for that.
Test what the user actually sees. chrome://extensions → Developer mode → Pack extension → Pack only generates a .crx; install that into a regular Chrome profile (or use chrome://extensions → Drag and drop). The dialog that pops up is the exact install prompt your users see. Cheap, instant feedback on whether your permission set passes the eye test.

Code

Narrowly-scoped host_permissions for a GitHub-specific helper·json
{
  "host_permissions": [
    "https://github.com/*",
    "https://*.github.com/*",
    "https://gist.github.com/*"
  ]
}
Broad matches with explicit privacy-sensitive exclusions·json
{
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "exclude_matches": [
        "https://accounts.google.com/*",
        "https://*.googleusercontent.com/*",
        "https://*.bank.com/*",
        "https://*.chase.com/*",
        "https://*.passwordmanager.com/*"
      ],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ]
}
background.js — defensive wrapper for executeScript denials·javascript
// background.js — handle the user downgrading host access at runtime
async function safeInject(tabId, fileOrFunc) {
  try {
    return await chrome.scripting.executeScript({
      target: { tabId },
      ...(typeof fileOrFunc === "function" ? { func: fileOrFunc } : { files: [fileOrFunc] }),
    });
  } catch (err) {
    if (String(err.message).includes("Cannot access")) {
      // Either the URL is restricted (chrome:// etc) OR the user revoked
      // site access for this extension on this site.
      console.warn("[ClipDeck SW] cannot inject:", err.message);
      return null;
    }
    throw err;
  }
}

External links

Exercise

Edit clipdeck/manifest.json to add the explicit exclude_matches list from the second code block to your content_scripts entry. Reload. Visit a bank or password-manager URL (or any one in the exclude list) — confirm the floating button does NOT appear and the SW DevTools logs no [ClipDeck content] lines. Then in chrome://extensions → ClipDeck → 'This can read and change site data,' switch from 'On all sites' to 'On click.' Now visit any real page — ClipDeck appears not to work; toolbar-icon click should still capture (via activeTab). Add the third code block's safeInject helper to background.js and wire it where chrome.scripting.executeScript currently lives — the wrapper turns runtime-revoked-access errors into a silent return instead of an exception.
Hint
If the floating button still appears on excluded sites, your exclude_matches patterns might be wrong — Chrome matches them literally, and https://*.bank.com/* does NOT match https://bank.com/ (no subdomain). Add https://bank.com/* explicitly if you need the apex. If switching to 'On click' breaks the side panel too, that's actually expected on older Chrome — newer Chromes let panel content render even on restricted tabs since the panel is extension-owned, but chrome.scripting.executeScript against the page still rejects. The safeInject wrapper handles the rejection silently; surface a user-facing message if the action being denied is user-initiated.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.