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

Permission Model Overview — Three Categories, One Trust Contract

~11 min · permissions, manifest, mv3, trust

Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Every permission you declare is a sentence in the install prompt the user reads. Lesson 1 is the model — three categories, two grant moments, one Chrome that calculates the warning text from your manifest verbatim."

The Three Categories

  • API permissions — declared in permissions array. Each unlocks a chrome.* namespace: "storage" enables chrome.storage.*, "scripting" enables chrome.scripting.*, "contextMenus" enables menus, etc.
  • Host permissions — declared in host_permissions array. Each is a URL match pattern (https://*.github.com/*, <all_urls>). Grants the extension permission to inject scripts into, read DOM from, and intercept requests on those origins.
  • Optional permissions — declared in optional_permissions AND/OR optional_host_permissions array. Same shape as the two above, but the user is NOT prompted at install. Your code requests them at runtime via chrome.permissions.request.

MV3 separated host permissions from API permissions specifically so Chrome can show a cleaner install warning. The user sees "This extension can: read your data on github.com," not "This extension can: read your data on github.com AND use storage AND use tabs" — those distinct lines, distinct visual weights.

The Two Grant Moments

  • Install-time: everything in permissions and host_permissions is granted when the user installs the extension. The install dialog summarizes everything you've declared; if the user clicks Add, you have it forever (until uninstall).
  • Runtime: anything in optional_permissions or optional_host_permissions is granted only when you call chrome.permissions.request from a user-gesture handler. A small dialog appears; the user clicks Allow or Deny. If denied, the API stays unavailable; if allowed, it works for the rest of the session and persists across browser restarts.

How Chrome Computes the Install Warning

Each permission maps to one of a few warning categories. Combining permissions can collapse the warnings (e.g., 'tabs' subsumes some weaker permissions) or add scary ones (anything with <all_urls> ends up under 'Read and change all your data on all websites'). The exact mapping is in the Chrome Developers docs, but the rule of thumb is:

  • storage, activeTab, contextMenus, sidePanel, commands, omnibox — quiet, no install warning of their own.
  • tabs — moderate, "Read your browsing history."
  • scripting, host permissions narrower than <all_urls> — "Read and change your data on specific sites."
  • <all_urls> in host_permissions OR content_scripts.matches — the loud "Read and change all your data on all websites."
  • downloads, notifications, identity, history, geolocation, cookies — each adds its own dedicated warning line.

The Principle of Least Privilege

The Chrome Web Store review (and informed users) judge extensions by their install warning. Every line you can remove is one fewer reason to refuse. ClipDeck's actual needs:

  • storage — essential, always declared, no scary warning.
  • tabs — for reading tab.url in the SW. Mildly scary warning, but necessary for the visit counter and per-site clip filter.
  • scripting + activeTab — for programmatic injection on toolbar click. activeTab means we can skip host_permissions: [<all_urls>] for the on-demand path.
  • sidePanel, contextMenus — quiet.
  • Content scripts declared with matches: [<all_urls>] — this is what's giving ClipDeck its 'all websites' warning. Track 6 Lesson 3 discusses narrowing this.
  • downloads for export — should go in optional_permissions, requested only when the user clicks Export.
Three categories: API, host, optional. Two grant moments: install or on-demand. The install warning is your trust budget — spend it carefully.
The MV2 permissions-array compatibility trap. Chrome still accepts the old MV2 shape where API and host permissions live in one array. It loads, but the warning summary is harder to read and the Chrome Web Store reviewer treats it as a smell. Always split into the MV3 shape — permissions for API, host_permissions for URLs, optional_* for the lazy versions.

Code

manifest.json — the MV3 shape with all four arrays explicit·json
{
  "manifest_version": 3,
  "name": "ClipDeck",
  "version": "0.9.0",
  "permissions": [
    "storage",
    "tabs",
    "scripting",
    "activeTab",
    "sidePanel",
    "contextMenus"
  ],
  "host_permissions": [],
  "optional_permissions": [
    "downloads"
  ],
  "optional_host_permissions": [
    "https://*/*",
    "http://*/*"
  ],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "exclude_matches": ["https://accounts.google.com/*", "https://*.bank.com/*"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ]
}
background.js — diagnostic dump of declared vs granted permissions·javascript
// background.js — inspect declared and granted permissions
async function dumpPermissions() {
  const declared = chrome.runtime.getManifest().permissions ?? [];
  const declaredHosts = chrome.runtime.getManifest().host_permissions ?? [];
  const optional = chrome.runtime.getManifest().optional_permissions ?? [];
  const optionalHosts = chrome.runtime.getManifest().optional_host_permissions ?? [];

  const granted = await chrome.permissions.getAll();
  console.group("[ClipDeck SW] permissions");
  console.log("declared API:", declared);
  console.log("declared hosts:", declaredHosts);
  console.log("optional API:", optional);
  console.log("optional hosts:", optionalHosts);
  console.log("granted now (API):", granted.permissions);
  console.log("granted now (hosts):", granted.origins);
  console.groupEnd();
}

chrome.runtime.onInstalled.addListener(dumpPermissions);

External links

Exercise

Update clipdeck/manifest.json to match the first code block (version 0.9.0, MV3-shape split into permissions / host_permissions / optional_permissions / optional_host_permissions). Reload the extension at chrome://extensions. Click ClipDeck → Details. Look at the 'Permissions' section — you should see the API permissions listed, and the Site access section should show the content-script match patterns separately. Then add the second code block (dumpPermissions) and reload. Open the SW DevTools console — you'll see four lines: the declared API permissions, the declared hosts (empty array now that we moved them to optional_host_permissions), the optional sets, and what's actually granted at this moment. The optional_host_permissions are declared but NOT yet granted; we'll request them in Lesson 5.
Hint
If the console shows optional_host_permissions undefined, your Chrome may be older than 105 (when the field landed); upgrade or fall back to declaring the hosts directly. If the install warning gets worse after splitting the arrays (sometimes it does because Chrome recomputes), it's usually because moving hosts from content_scripts to host_permissions made them more visible — check whether they really need to be persistently granted, or whether activeTab + on-demand injection covers them. The diagnostic dump is a one-time tool; remove it before shipping to the Chrome Web Store.

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.