Chrome Extension Permissions Deep Dive

28 min read

Chrome Extension Permissions Deep Dive

The definitive reference for every Chrome extension permission: what each grants, what users see, and how to request only what you need.


Table of Contents

  1. How Permissions Work
  2. Permission Categories
  3. Complete Permission Reference
  4. Host Permissions
  5. activeTab: The Most Important Permission
  6. Permission Warnings
  7. Optional Permissions
  8. Minimum Viable Permissions Strategy
  9. Permission Escalation in Updates
  10. Withdrawn Permissions
  11. TypeScript Helpers for Permission Checking
  12. Using @theluckystrike/webext-permissions
  13. Permission Audit Checklist

How Permissions Work

Every Chrome extension declares what it needs in manifest.json. The browser enforces these declarations at runtime – an extension cannot access an API or a host unless the manifest (or a runtime grant) authorises it.

There are three places permissions appear in the manifest:

{
  "permissions": [
    // Required API permissions -- granted at install
    "storage",
    "alarms"
  ],
  "host_permissions": [
    // Required host access -- granted at install (MV3)
    "https://api.example.com/*"
  ],
  "optional_permissions": [
    // API permissions the user can grant later
    "bookmarks"
  ],
  "optional_host_permissions": [
    // Host access the user can grant later
    "https://*.github.com/*"
  ]
}

In Manifest V2, host patterns lived inside permissions. Manifest V3 moved them to the dedicated host_permissions and optional_host_permissions keys.

Install-Time vs Runtime Grants

Declared in When granted User experience
permissions Install Warning dialog before install
host_permissions Install Warning dialog before install
optional_permissions Runtime Prompt when chrome.permissions.request() is called
optional_host_permissions Runtime Prompt when chrome.permissions.request() is called

Install-time permissions cannot be denied individually. The user either accepts everything or cancels the installation. This is why minimising required permissions matters.


Permission Categories

Permissions fall into three broad categories.

1. API Permissions {#1-api-permissions}

These unlock specific Chrome APIs. Without the permission, calling the API throws an error.

2. Host Permissions {#2-host-permissions}

These allow the extension to interact with web pages on matching origins: inject content scripts, read page content, intercept network requests, and modify headers.

3. Implicit Permissions {#3-implicit-permissions}

Some APIs require no explicit permission declaration:


Complete Permission Reference

Every permission you can declare in manifest.json, grouped by function.

Storage and Data

Permission API Access Warning?
storage chrome.storage.local, chrome.storage.sync, chrome.storage.managed No
unlimitedStorage Lifts the 10 MB quota on storage.local and IndexedDB No
cookies chrome.cookies – read/write cookies for permitted hosts Yes

Tabs and Windows

Permission API Access Warning?
tabs chrome.tabs – access url, title, favIconUrl on Tab objects Yes
activeTab Temporary host access to the active tab on user gesture No
tabGroups chrome.tabGroups – query and modify tab groups No
Permission API Access Warning?
history chrome.history – read/write browser history Yes
bookmarks chrome.bookmarks – CRUD on the bookmark tree Yes
topSites chrome.topSites – read the new tab page’s top sites list Yes
sessions chrome.sessions – query and restore recently closed tabs/windows Yes
readingList chrome.readingList – CRUD on the reading list Yes

Content and Scripting

Permission API Access Warning?
scripting chrome.scripting – inject scripts, CSS, and register content scripts No (host permissions control actual injection)
activeTab Temporary scripting access on user gesture No
declarativeContent chrome.declarativeContent – show action icon based on page content No
contentSettings chrome.contentSettings – modify per-site settings (JS, cookies, images) Yes

Network

Permission API Access Warning?
webRequest chrome.webRequest – observe network requests No (host permissions determine scope)
declarativeNetRequest chrome.declarativeNetRequest – block/redirect/modify requests via rules No
declarativeNetRequestWithHostAccess Like above but can also redirect to extension resources No
declarativeNetRequestFeedback Access matched rules info for debugging No
proxy chrome.proxy – manage proxy settings Yes
dns chrome.dns – resolve hostnames No

UI and Browser Integration

Permission API Access Warning?
alarms chrome.alarms – schedule periodic or one-shot tasks No
notifications chrome.notifications – rich desktop notifications Yes
contextMenus chrome.contextMenus – add items to right-click menus No
sidePanel chrome.sidePanel – register and control the side panel No
offscreen chrome.offscreen – create offscreen documents for DOM/audio/etc. No
search chrome.search – trigger searches via the default search engine No
omnibox chrome.omnibox – add keyword suggestions to the address bar No
fontSettings chrome.fontSettings – read/write font preferences No

Identity and Accounts

Permission API Access Warning?
identity chrome.identity – OAuth2 flows, getAuthToken, launchWebAuthFlow No
identity.email Access user’s email address via chrome.identity.getProfileUserInfo Yes

Downloads

Permission API Access Warning?
downloads chrome.downloads – create, pause, search, monitor downloads Yes
downloads.open Open downloaded files (requires downloads too) Yes
downloads.ui Modify the downloads UI (shelf visibility) No

Developer and Debugging

Permission API Access Warning?
debugger chrome.debugger – attach Chrome DevTools Protocol to tabs Yes
management chrome.management – list, enable, disable other extensions Yes
power chrome.power – prevent display/system sleep No
system.cpu chrome.system.cpu – query CPU info No
system.memory chrome.system.memory – query memory info No
system.storage chrome.system.storage – query attached storage devices No
system.display chrome.system.display – query display info No

Privacy and Security

Permission API Access Warning?
privacy chrome.privacy – control privacy-related browser settings Yes
browsingData chrome.browsingData – clear browsing data (cache, cookies, history) No

Communication

Permission API Access Warning?
tts chrome.tts – text-to-speech synthesis No
ttsEngine chrome.ttsEngine – register as a TTS engine No
gcm chrome.gcm – Google Cloud Messaging for push notifications No
nativeMessaging chrome.runtime.connectNative, sendNativeMessage – communicate with native apps Yes

Clipboard

Permission API Access Warning?
clipboardRead Read from clipboard via document.execCommand('paste') Yes
clipboardWrite Write to clipboard via document.execCommand('copy') No

Platform and Enterprise

Permission API Access Warning?
geolocation Use the Geolocation API from the service worker Yes
favicon Access chrome://favicon/ URLs for site favicons No
idle chrome.idle – detect when the user is idle No
webNavigation chrome.webNavigation – observe navigation events in tabs Yes
tabCapture chrome.tabCapture – capture tab audio/video Yes
desktopCapture chrome.desktopCapture – capture screen, window, or tab Yes
accessibilityFeatures.modify Modify accessibility settings Yes
accessibilityFeatures.read Read accessibility settings Yes

Host Permissions

Host permissions control which websites the extension can interact with. They use match patterns:

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

Common Patterns

{
  "host_permissions": [
    "https://www.example.com/*",          // Single specific site
    "https://*.example.com/*",            // All subdomains of a domain
    "https://*/*",                        // Any HTTPS site
    "<all_urls>",                         // Everything including file:// and ftp://
    "https://api.example.com/v2/*"        // Specific path prefix
  ]
}

Specific Hosts vs <all_urls>

Approach Pros Cons
Specific hosts Minimal warning, higher trust Must know all hosts upfront
*://*.example.com/* Covers subdomains Still limited to one domain
https://*/* Works on any HTTPS site Triggers “Read and change all your data on all websites”
<all_urls> Maximum flexibility Scariest possible warning; invites scrutiny in review

Rule of thumb: use the narrowest pattern that covers your use case. If your extension only talks to your own API, declare only that origin. If it needs to work on arbitrary sites (like an ad blocker), you have no choice but to request broad access – but consider using optional_host_permissions and requesting at runtime instead.

Host Permissions and Content Scripts

Content scripts declared in the manifest are only injected on pages that match both the content script’s matches pattern and a granted host permission:

{
  "content_scripts": [
    {
      "matches": ["https://mail.google.com/*"],
      "js": ["content.js"]
    }
  ],
  "host_permissions": [
    "https://mail.google.com/*"
  ]
}

If you use chrome.scripting.executeScript() programmatically, you need host permission for the target tab’s URL (or activeTab).


activeTab: The Most Important Permission

activeTab is the single most important permission in the Chrome extension ecosystem. It grants temporary access to the currently active tab, but only when the user explicitly invokes the extension.

What Triggers activeTab

What activeTab Grants

When triggered, the extension receives temporary permission to:

  1. Call chrome.scripting.executeScript() on the active tab
  2. Call chrome.scripting.insertCSS() / removeCSS() on the active tab
  3. Access the tab’s URL, title, and favicon
  4. Interact with the page as if it had host permission for that origin

The grant lasts until the tab is navigated or closed.

Why activeTab Matters

Without activeTab, an extension that needs to run on arbitrary pages must request <all_urls> or https://*/* – triggering the most alarming permission warning. With activeTab, it requests nothing scary at install time and gets access only when the user takes explicit action.

// BAD: Requests access to every website at install
{
  "permissions": ["scripting"],
  "host_permissions": ["<all_urls>"]
}

// GOOD: No warning, user-initiated access only
{
  "permissions": ["activeTab", "scripting"]
}

activeTab Limitations

When activeTab Is Not Enough

You need real host permissions when:


Permission Warnings

Permission warnings are what users see in the install dialog. They are the number one reason users abandon installations.

Warning Messages by Permission

Permission / Pattern Warning Message
bookmarks “Read and change your bookmarks”
clipboardRead “Read data you copy and paste”
contentSettings “Change your settings that control websites’ access to features such as cookies, JavaScript, plugins, geolocation, microphone, camera, etc.”
debugger “Access the page debugger backend”
desktopCapture “Capture content of your screen”
downloads “Manage your downloads”
geolocation “Detect your physical location”
history “Read and change your browsing history on all signed-in devices”
management “Manage your apps, extensions, and themes”
nativeMessaging “Communicate with cooperating native applications”
notifications “Display notifications”
privacy “Change your privacy-related settings”
proxy “Read and change all your data on all websites”
sessions “Use data you save in bookmarks and browsing history”
tabs “Read your browsing activity”
tabCapture “Capture content of your screen”
topSites “Read a list of your most frequently visited websites”
webNavigation “Read your browsing activity”
<all_urls> “Read and change all your data on all websites”
https://*.example.com/* “Read and change your data on all example.com sites”
Single host “Read and change your data on www.example.com”

How Warnings Affect Install Rates

Research consistently shows:

Combining Warnings

Chrome de-duplicates warnings. If two permissions produce the same string, users see it only once. For example, both tabs and webNavigation produce “Read your browsing activity” – requesting both only shows the warning once.


Optional Permissions

Optional permissions let you defer scary warnings until the user actually needs the feature.

Declaring Optional Permissions

{
  "permissions": ["storage"],
  "optional_permissions": ["bookmarks", "history"],
  "optional_host_permissions": ["https://*.github.com/*"]
}

Requesting at Runtime

async function requestBookmarkAccess(): Promise<boolean> {
  return chrome.permissions.request({
    permissions: ['bookmarks'],
  });
}

async function requestGitHubAccess(): Promise<boolean> {
  return chrome.permissions.request({
    origins: ['https://*.github.com/*'],
  });
}

Critical rule: chrome.permissions.request() must be called from a user gesture handler (click, keypress). Calling it from a timer or on page load will fail silently or throw.

Checking Granted Permissions

async function hasBookmarkPermission(): Promise<boolean> {
  return chrome.permissions.contains({
    permissions: ['bookmarks'],
  });
}

Removing Permissions

Users can revoke optional permissions from chrome://extensions. You can also remove them programmatically:

async function releaseBookmarkPermission(): Promise<boolean> {
  return chrome.permissions.remove({
    permissions: ['bookmarks'],
  });
}

Listening for Permission Changes

chrome.permissions.onAdded.addListener((permissions) => {
  console.log('Granted:', permissions);
});

chrome.permissions.onRemoved.addListener((permissions) => {
  console.log('Revoked:', permissions);
  // Disable features that depend on these permissions
});

Best Practices for Optional Permissions

  1. Explain before requesting: Show UI explaining why the permission is needed before calling request(). Users who understand the reason grant at much higher rates.
  2. Degrade gracefully: If the user denies, disable the feature but do not break the extension.
  3. Re-check on startup: Permissions can be revoked while the extension is not running. Always verify with contains() before using a gated API.
  4. Provide a way to revoke: Let users disable features and revoke permissions from your settings page.

Minimum Viable Permissions Strategy

Follow this decision tree for every permission your extension needs:

Do I need this API?
  +-- No  --> Do not request it
  +-- Yes
       +-- Can I use activeTab instead of host permissions?
       |    +-- Yes --> Use activeTab
       |    +-- No  --> Use narrowest host pattern possible
       |
       +-- Is this needed for the core feature?
       |    +-- Yes --> Required permission (permissions array)
       |    +-- No  --> Optional permission (optional_permissions array)
       |
       +-- Does it trigger a warning?
            +-- Yes --> Document why in your listing
            +-- No  --> Declare it without worry

Practical Example

An extension that highlights text on any page and optionally saves highlights to bookmarks:

{
  "manifest_version": 3,
  "name": "Highlighter",
  "permissions": [
    "activeTab",     // No warning -- inject on click
    "scripting",     // No warning -- needed for executeScript
    "storage"        // No warning -- save highlights locally
  ],
  "optional_permissions": [
    "bookmarks"      // Warning deferred until user enables bookmark sync
  ]
}

Zero install-time warnings. The bookmark warning only appears when the user opts into the bookmark sync feature.


Permission Escalation in Updates

When you publish an update that adds new required permissions, Chrome disables the extension for existing users and shows a re-consent dialog. The user must accept the new permissions before the extension re-enables.

How to Handle Escalation

  1. Avoid it if possible. Use optional permissions for new features.
  2. Communicate proactively. Warn users in the previous version’s changelog that the next update will request additional access.
  3. Time it with a major feature. Users are more willing to re-consent when they see clear new value.
// Version 1.0 -- no warnings
{
  "permissions": ["activeTab", "storage"]
}

// Version 2.0 -- BAD: forces re-consent for all users
{
  "permissions": ["activeTab", "storage", "bookmarks"]
}

// Version 2.0 -- GOOD: no re-consent, request at runtime
{
  "permissions": ["activeTab", "storage"],
  "optional_permissions": ["bookmarks"]
}

Withdrawn Permissions

You can remove permissions in an update. Chrome handles this gracefully:

Why Remove Permissions

Moving Required to Optional

This is a two-step process:

  1. Version N: Add the permission to optional_permissions while keeping it in permissions. (No user impact – redundant declaration.)
  2. Version N+1: Remove it from permissions. Chrome revokes the grant, but since it is in optional_permissions, the extension can re-request it at runtime.

If you skip step 1 and just remove the permission, users lose access and you have no way to get it back without adding it to optional_permissions in a future update.


TypeScript Helpers for Permission Checking

Type-safe utilities for managing permissions in your extension.

Permission Gate Decorator

type ChromePermission = chrome.runtime.ManifestPermissions;

interface PermissionGateOptions {
  permissions?: ChromePermission[];
  origins?: string[];
  onDenied?: () => void;
}

function requiresPermission(options: PermissionGateOptions) {
  return function (
    _target: unknown,
    _propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;

    descriptor.value = async function (...args: unknown[]) {
      const granted = await chrome.permissions.contains({
        permissions: options.permissions,
        origins: options.origins,
      });

      if (!granted) {
        const accepted = await chrome.permissions.request({
          permissions: options.permissions,
          origins: options.origins,
        });

        if (!accepted) {
          options.onDenied?.();
          return undefined;
        }
      }

      return original.apply(this, args);
    };

    return descriptor;
  };
}

Permission Status Checker

interface PermissionStatus {
  granted: boolean;
  permission: string;
  warning: string | null;
}

const WARNING_MAP: Record<string, string> = {
  bookmarks: 'Read and change your bookmarks',
  history: 'Read and change your browsing history',
  tabs: 'Read your browsing activity',
  downloads: 'Manage your downloads',
  notifications: 'Display notifications',
  geolocation: 'Detect your physical location',
  clipboardRead: 'Read data you copy and paste',
  topSites: 'Read a list of your most frequently visited websites',
  management: 'Manage your apps, extensions, and themes',
};

async function checkPermissions(
  permissions: string[]
): Promise<PermissionStatus[]> {
  const results: PermissionStatus[] = [];

  for (const perm of permissions) {
    const granted = await chrome.permissions.contains({
      permissions: [perm as chrome.runtime.ManifestPermissions],
    });

    results.push({
      granted,
      permission: perm,
      warning: WARNING_MAP[perm] ?? null,
    });
  }

  return results;
}

Safe API Caller

async function safeCallWithPermission<T>(
  permission: ChromePermission,
  fn: () => Promise<T>,
  fallback: T
): Promise<T> {
  const hasPermission = await chrome.permissions.contains({
    permissions: [permission],
  });

  if (!hasPermission) {
    console.warn(`Missing permission: ${permission}. Using fallback.`);
    return fallback;
  }

  try {
    return await fn();
  } catch (error) {
    console.error(`API call failed for ${permission}:`, error);
    return fallback;
  }
}

// Usage:
const history = await safeCallWithPermission(
  'history',
  () => chrome.history.search({ text: '', maxResults: 10 }),
  []
);

Using @theluckystrike/webext-permissions

The @theluckystrike/webext-permissions library provides human-readable descriptions and utilities for working with browser extension permissions.

Installation

npm install @theluckystrike/webext-permissions

Getting Human-Readable Descriptions

import {
  getPermissionDescription,
  describeHostPattern,
  summariseManifestPermissions,
} from '@theluckystrike/webext-permissions';

// Single permission
const desc = getPermissionDescription('tabs');
// => "Allows the extension to read your browsing activity including
//     the URL, title, and favicon of every open tab."

// Host pattern
const hostDesc = describeHostPattern('https://*.github.com/*');
// => "Read and change your data on all github.com sites"

// Full manifest summary
const summary = summariseManifestPermissions({
  permissions: ['storage', 'tabs', 'activeTab'],
  host_permissions: ['https://api.example.com/*'],
  optional_permissions: ['bookmarks'],
});

Building a Permissions Page

import {
  getPermissionDescription,
  getPermissionWarning,
} from '@theluckystrike/webext-permissions';

async function renderPermissionsUI(container: HTMLElement) {
  const manifest = chrome.runtime.getManifest();
  const optional = manifest.optional_permissions ?? [];

  for (const perm of optional) {
    const granted = await chrome.permissions.contains({
      permissions: [perm],
    });

    const row = document.createElement('div');
    row.className = 'permission-row';
    row.innerHTML = `
      <label>
        <input type="checkbox" data-perm="${perm}" ${granted ? 'checked' : ''}>
        <strong>${perm}</strong>
      </label>
      <p class="description">${getPermissionDescription(perm)}</p>
      <p class="warning">
        ${getPermissionWarning(perm) ?? 'No warning shown to users'}
      </p>
    `;

    const checkbox = row.querySelector('input')!;
    checkbox.addEventListener('change', async () => {
      if (checkbox.checked) {
        const ok = await chrome.permissions.request({
          permissions: [perm],
        });
        if (!ok) checkbox.checked = false;
      } else {
        await chrome.permissions.remove({ permissions: [perm] });
      }
    });

    container.appendChild(row);
  }
}

Validating Manifest Permissions

Use the library in your build pipeline to catch mistakes:

import { validateManifest } from '@theluckystrike/webext-permissions';

const result = validateManifest(manifest);

if (result.warnings.length > 0) {
  console.warn('Permission issues found:');
  for (const w of result.warnings) {
    console.warn(` - ${w}`);
  }
}
// Example output:
// - "tabs" permission is redundant when "activeTab" is declared
//   and no background tab enumeration is needed
// - "<all_urls>" in host_permissions: consider using
//   optional_host_permissions to reduce install friction

Permission Audit Checklist

Run through this checklist before every Chrome Web Store submission.

Required Permissions

Host Permissions

Optional Permissions

Warnings and User Experience

Updates and Maintenance

Security


Summary

Permissions are the trust contract between your extension and its users. Every permission you request is a promise: “I need this, and I will use it responsibly.” Minimise what you ask for, explain what you need, and use optional permissions to defer warnings until users understand the value.

The extensions with the highest install rates are the ones that request the least. Build trust by asking for less, and earn more access through runtime prompts that users understand and accept.

Frequently Asked Questions

What are optional permissions in Chrome extensions?

Optional permissions are declared in “optional_permissions” and requested at runtime when needed, giving users more control over what your extension can access.

How do I request permissions at runtime?

Use chrome.permissions.request() with the permissions you need. The user will be prompted to grant access. —

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.