Open to SWE / AI Engineering roles — Let's talk
Skip to content

Chrome Extension Permissions: How to Ask for Less and Ship Faster

Software engineeringMay 1, 2026

Request the minimum permissions upfront—every extra scope costs user trust.

Chrome Extension Permissions: How to Ask for Less and Ship Faster

Permissions are the #1 reason Chrome extensions get rejected from the Web Store. Request too many, and Google flags your extension for manual review. Request the wrong ones, and users see scary install warnings that tank your conversion rate.

MV3 changed the permission model significantly. This post covers what permissions exist, how to request the minimum set, and how to avoid the rejection cycle.

The Permission Categories

MV3 has three categories:

1. Declared Permissions (permissions)

Always-on permissions granted at install time. User sees them in the install dialog.

{
  "permissions": [
    "activeTab",
    "storage",
    "alarms",
    "notifications"
  ]
}

Common declared permissions and what they do:

  • activeTab — Access the current tab, but only when the user clicks your extension icon. No install warning.
  • storage — Use chrome.storage API. No install warning.
  • alarms — Schedule timers. No install warning.
  • tabs — Read tab URLs and titles for ALL tabs. Shows a warning: "Read your browsing history."
  • notifications — Show desktop notifications. Shows a warning.
  • scripting — Inject scripts into pages. Required for chrome.scripting.executeScript.

2. Host Permissions (host_permissions)

Control which websites your extension can access.

{
  "host_permissions": [
    "https://api.example.com/*",
    "https://*.google.com/*"
  ]
}

The scary one is <all_urls> or https://*/* — this triggers the warning "Read and change all your data on all websites." Users hate this. Google scrutinizes it heavily.

3. Optional Permissions (optional_permissions)

Not granted at install. Requested at runtime via chrome.permissions.request(). User gets a focused prompt only when the feature needs it.

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

This is the most underused feature in extension development. Instead of asking for everything upfront, request permissions when the user actually needs the feature.

The activeTab Strategy

activeTab is the single most important permission to understand. It grants temporary access to the current tab — but only when the user explicitly interacts with your extension (clicks the icon, uses a keyboard shortcut, or selects a context menu item).

What activeTab gives you:

  • Read the tab's URL and title
  • Inject scripts into the tab via chrome.scripting.executeScript
  • Access the tab's content

What it doesn't:

  • No access to other tabs
  • No access without user interaction
  • Expires when the user navigates away or switches tabs

This means zero install warnings for tab access. Compare:

// BAD — shows "Read your browsing history" warning
{ "permissions": ["tabs"] }

// GOOD — no warning, sufficient for most use cases
{ "permissions": ["activeTab"] }

Use tabs only if you need to enumerate or monitor ALL open tabs (like a tab manager extension). For reading the current tab's info on user click, activeTab is always enough.

This is the approach used in Build Your First Chrome Extension with TypeScriptactiveTab + a popup gives you tab access without any scary warnings.

Requesting Optional Permissions at Runtime

// src/features/bookmarks.ts

async function enableBookmarkSync(): Promise<boolean> {
  // This shows a focused permission prompt
  const granted = await chrome.permissions.request({
    permissions: ["bookmarks"],
  });

  if (!granted) {
    console.log("User denied bookmark access");
    return false;
  }

  // Now you can use chrome.bookmarks API
  const bookmarks = await chrome.bookmarks.getTree();
  return true;
}

Benefits of optional permissions:

  • Smaller install warning → higher install rate
  • User understands WHY you need the permission (they just clicked a related feature)
  • Google reviews are faster when your base permissions are minimal

Content Security Policy (CSP)

MV3 enforces a strict default CSP:

script-src 'self'; object-src 'self';

This means:

  • No inline scripts. <script>alert('hi')</script> in your popup HTML won't execute.
  • No eval(). Libraries that use eval or new Function() break.
  • No remote scripts. <script src="https://cdn.example.com/lib.js"> is blocked.

You can relax CSP slightly in the manifest, but you cannot remove script-src 'self' entirely. MV3 does not allow remote code execution in extensions.

{
  "content_security_policy": {
    "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
  }
}

'wasm-unsafe-eval' is the only relaxation that matters — required if you use WebAssembly.

For fetching data from external APIs, use fetch() in your service worker with appropriate host_permissions. That's data, not code — CSP allows it.

Web Store Rejection Reasons (and Fixes)

"Broad host permissions without justification"

Problem: Using <all_urls> or https://*/* when you don't need it.

Fix: List specific domains in host_permissions. If you need broad access (like an ad blocker), explain why in your listing description and privacy policy.

"Excessive permissions"

Problem: Requesting tabs, history, bookmarks, etc. when your extension doesn't visibly use those features.

Fix: Move non-essential permissions to optional_permissions. Only request what the core functionality needs.

"Missing privacy policy"

Problem: Any extension that accesses user data (including via activeTab) needs a privacy policy URL in the Web Store listing.

Fix: Write a simple privacy policy. Host it on your site. Add the URL to your listing. More on this in How to Publish a Chrome Extension to the Web Store.

"Remote code execution"

Problem: Loading scripts from external URLs, using eval(), or injecting remotely-fetched code into pages.

Fix: Bundle all code locally. Use fetch() for data only, never for code. If a library uses eval(), find an alternative or patch it.

"Obfuscated code"

Problem: Minified code that Google's reviewers can't read. Some bundlers produce output that triggers this.

Fix: Include source maps or submit unminified code. Vite's default output is usually fine. Avoid aggressive uglification.

Permission Audit Checklist

Before submitting to the Web Store, audit your manifest.json:

  1. Can activeTab replace tabs? If you only need the current tab on user click, yes.
  2. Can host permissions be narrowed? Replace <all_urls> with specific domains.
  3. Are any permissions unused? Remove them. Google checks.
  4. Can anything move to optional_permissions? If a feature is secondary, request its permission at runtime.
  5. Does CSP need relaxation? Only add 'wasm-unsafe-eval' if you actually use WASM.
  6. Do you have a privacy policy URL? Required for any data-accessing extension.

Real Example: ResistGate's Permission Setup

ResistGate needs to intercept navigation to blocked sites and inject a challenge page. Here's how the permissions break down:

{
  "permissions": ["storage", "alarms", "activeTab", "scripting"],
  "host_permissions": ["<all_urls>"],
  "optional_permissions": ["notifications"]
}
  • storage — Persist blocked sites list and session data (typed storage wrappers)
  • alarms — Timer for earned access windows
  • activeTab + scripting — Inject challenge UI into blocked pages
  • <all_urls> — Required because ResistGate needs to intercept ANY site the user adds to their block list. Can't predict which domains.
  • notifications — Optional. Only requested when user enables "remind me to refocus" feature.

The <all_urls> permission is justified here because the core feature requires it. The listing description explains this clearly, and the privacy policy confirms no data leaves the device.

Communicating Permissions to Users

Permissions affect message passing too — content scripts can only run on pages that match your host permissions. If a user reports "the extension doesn't work on this site," check whether your permissions cover that domain.

Two strategies for transparency:

  1. Progressive disclosure — Start with minimal permissions, unlock features as user needs them via optional_permissions.
  2. Permission explainer page — Show an options page on first install that explains what each permission does and why you need it. Users who understand why are less likely to uninstall.

What's Next

  1. Add AI to your extensionBuilding an AI-Powered Chrome Extension with Claude shows how to wire an API into your service worker while keeping permissions minimal.
  2. Ship itHow to Publish a Chrome Extension to the Web Store is the full publishing checklist, including the privacy policy and permission justification review.
  3. See it in practiceHow I Built ResistGate covers every architecture decision, including permission trade-offs.

This is part of the Chrome Extensions from Zero to Product series, where I walk through everything I learned building ResistGate and Amethyst — from first popup to published product.

Building in public

Follow the journey

as I build AI tools, products, and a serious founder life.

No spam. Unsubscribe anytime.

Chrome Extension Permissions: How to Ask for Less and Ship Faster | Orlando Ascanio