permissionschrome-extensionsmanifest-v3conversion-rate

Chrome Extension Permission Warnings That Kill Install Rate

The 'read and change all your data on all websites' warning is the single biggest CVR drop in a Chrome extension's install funnel. Here's how to migrate from broad host_permissions to activeTab + optional, with code and measurable before/after.

The dialog that opens when a user clicks Add to Chromehas one bold line, and for most extensions that line says "Read and change all your data on all websites". It's the scariest sentence the Web Store will ever show on your behalf, and it accounts for more lost installs than any other single factor in the funnel. This guide is what the permission warning actually does to your install rate, the migration path from broad host_permissions to activeTab plus optional_permissions (with code that won't break your extension), and how to measure the before/after honestly.

Why the permission warning kills install rate

Three structural reasons:

  • It's the only sentence in bold on the install dialog. The dialog is otherwise plain. Whatever sentence Chrome renders bold is the one the user reads — and for a broad host-permissions extension, it's the worst-case phrasing.
  • It triggers a trust check the user wasn't ready for. They came from your listing thinking about a feature. The dialog is suddenly about data and websites. The cognitive frame switches; many users back out rather than process the switch.
  • It survives no matter how friendly your listing was. You can write the warmest description, post the prettiest screenshots, set up the perfect attribution (see the install attribution guide) — the user still hits the dialog right at the bottom of the funnel, and the dialog is louder than your listing.

Practical impact: extensions that migrate from broad host_permissions: ["<all_urls>"] to a narrower combination typically see install-rate lifts of 15–35% on warm traffic and 5–15% on cold organic CWS traffic. It is the highest-leverage CVR change available to most extensions — usually more than screenshot or description tweaks. The full CVR funnel context is in the CWS conversion rate guide.

The five permission strings in order of scariness

The phrasing Chrome shows is derived from the permissions in your manifest.json. From scariest (highest CVR damage) to mildest:

  1. "Read and change all your data on all websites" — triggered by <all_urls> or https://*/* in host_permissions (or content_script matches). The worst case; single biggest CVR drag.
  2. "Read and change your data on [N] websites" — triggered by a list of specific site patterns. Far milder if N is small and the sites are recognizable to your audience.
  3. "Read and change your data on a specific website" — single domain; users barely notice this one.
  4. "Read your browsing history" — triggered by history, tabs (in some forms), topSites, webNavigation with broad match patterns. Less common but a real installation-stopper for privacy-conscious audiences.
  5. No warning at all — the goal. Achieved with no host_permissions at all, just activeTab + optional_host_permissions requested at runtime when needed.

Note that "Read your browsing history" can be triggered without history in your manifest — broad tabs.onUpdated usage with tabs permission can surface it. Check the actual dialog Chrome produces for your manifest combination, not just what you expect from the keys.

The migration target: activeTab + optional

Most extensions that think they need <all_urls> actually need one of two narrower models:

  • activeTab — the user clicks your toolbar icon (or invokes a keyboard shortcut, or your context-menu entry), and Chrome grants you access to the current tab for the duration of that interaction. No install-time permission warning at all. Perfect for: translators that work on the current page, clippers, screenshot tools, formatters, dev-tooling extensions, color pickers.
  • optional_host_permissions — declare permissions in the manifest but ask for them at runtime via chrome.permissions.request(). Install-time warning shows only the always-required permissions; the rest are triggered by an in-app prompt that the user has context for ("Enable on facebook.com? [Allow]"). The dialog feels like asking, not warning.

Some extensions need true host_permissions at install time — ad blockers, always-on translators, password managers. We cover that case in §7. For everyone else, the activeTab + optional combination is the migration target.

Code: from broad host_permissions to activeTab

Common starting point — broad host permissions everywhere:

// manifest.json (before)
{
  "manifest_version": 3,
  "name": "My Extension",
  "permissions": ["storage", "scripting"],
  "host_permissions": ["<all_urls>"],
  "background": { "service_worker": "background.js" },
  "action": { "default_title": "Run on this page" }
}

Migrated to activeTab:

// manifest.json (after)
{
  "manifest_version": 3,
  "name": "My Extension",
  "permissions": ["storage", "scripting", "activeTab"],
  "background": { "service_worker": "background.js" },
  "action": { "default_title": "Run on this page" }
}

The key change: host_permissions removed, activeTab added. Now your extension has access to the current tab only after the user invokes it — clicks the toolbar icon, uses a context menu item, or triggers a keyboard command.

// background.js
// activeTab grants tab access at the moment of user invocation.
// You can read the URL and inject scripts only inside this handler.
chrome.action.onClicked.addListener(async (tab) => {
  if (!tab.id) return;
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['content.js'],
  });
});

Three things you lose by going to activeTab:

  • Auto-injection on page load. Content scripts declared with matches still work, but your background can't programmatically inject without user invocation.
  • Reading every tab's URL from the SW. tabs.query returns tabs but with stripped url/title unless you have host permissions for the matching origin.
  • Background webRequest interception. Needed for ad-blocking and similar — these extensions need the more advanced declarative approach (see §7).

Code: requesting optional_permissions at runtime

For extensions that need persistent access on user-chosen sites (clippers that watch certain pages, integrations with specific SaaS tools, custom scripts on specific domains), declare optional and ask at runtime:

// manifest.json
{
  "manifest_version": 3,
  "name": "My Extension",
  "permissions": ["storage", "scripting", "activeTab"],
  "optional_host_permissions": ["https://*/*"],
  "background": { "service_worker": "background.js" }
}

Install-time dialog now only shows the warning corresponding to the always-required permissions (here: nothing scary). The broad host pattern is declared but not granted; the user grants it at the moment they ask for the feature:

// In your popup or options page, after the user clicks "Enable on this site"
async function enableOnCurrentSite() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  if (!tab?.url) return;
  const origin = new URL(tab.url).origin + '/*';

  const granted = await chrome.permissions.request({
    origins: [origin],
  });

  if (granted) {
    // Persist the choice for the dashboard / settings UI.
    await chrome.storage.local.set({ enabled_origins: [...await getEnabled(), origin] });
    showToast('Enabled on ' + origin);
  } else {
    showToast('Permission denied');
  }
}

The user sees: "Add 'Read and change your data on facebook.com'?" with two buttons. Even users who bounce from the install dialog click Allow here at roughly 60–80% rates — because the request now has context.

Listen for permission revocation too, so your UI matches reality:

chrome.permissions.onRemoved.addListener(({ origins }) => {
  if (!origins) return;
  // Remove from enabled set, refresh UI, stop content scripts on those origins.
});

Match patterns that don't trigger the broad warning

Sometimes you need a few specific sites. The pattern matters:

  • Bad: https://*/* — counts as "all websites", worst warning.
  • Less bad: https://*.youtube.com/* — counts as "youtube.com and subdomains", single-site warning.
  • Best: https://www.youtube.com/* + https://m.youtube.com/* — explicit list, recognizable, friendly.

A common mistake: declaring https://*/* "just in case" while you only actually run on 3 known sites. The dialog gets nuked, the permission usage stays the same. Audit your real match patterns and tighten them. The CVR delta is free.

Use a manifest validator to double-check what string Chrome will actually render:

# Local check: install the extension into a test profile and look at
# chrome://extensions → Details → Permissions. The string there is what
# users see on install. If it's "Read and change all your data on all
# websites" you still have a broad host_permissions entry somewhere.

When you genuinely need broad permissions

Some categories cannot avoid the broad warning:

  • Ad blockers + privacy tools using declarativeNetRequest. The rule-set declaration requires host_permissions in practice.
  • Always-on translators that inject inline on every page load.
  • Password managers that need to detect form fields on any site.
  • Accessibility tools rewriting DOM on every page.

Three things to do anyway:

  1. Address the warning in the listing description. The second paragraph of your CWS listing is read by anyone who's nervous after seeing the dialog. A plain "we use this to do X and never send Y" halves the post-dialog bounce. Lean into specifics. The wider listing CVR levers are in the CWS conversion rate guide.
  2. Set the privacy practices in the CWS dashboard honestly. The Web Store now requires this; users see a "data usage" panel before installing. Filled-in and specific outperforms blank or generic. The principles are in privacy-first extension analytics.
  3. Ship the rest of the extension to the optional model. Many ad blockers, for example, can move logging and analytics features to optional, so the install-time warning shows only what's strictly required.

Measuring the install-rate impact

After migrating, measure the lift honestly. Three signals:

  • Landing-page click-through-install rate — the number you fully control from your own page. With the install-intent handoff from the install attribution guide, a side-by-side before/after on the same source comes out clean.
  • CWS weekly installs as a coarse second signal — but treat it as a 2–3 day-lagged number, per the accuracy caveats in why CWS analytics are inaccurate. Compare the same weekday buckets before/after, not raw weeks.
  • Activation rate (install → first use) — a permission migration shouldn't hurt this, but verify. If activation drops, the migration broke something the user notices in the first 60 seconds. The instrumentation is covered in the DAU/MAU guide.

Expected magnitude after a clean broad → activeTab migration: install rate up 15–35% on warm traffic within 2 weeks of the adoption curve reaching ~80% of users on the new version. Compounded against landing-page funnel + activation rate, this is usually the single most expensive change you can not make.

FAQ

Does activeTab let me read URLs from the background?

Only inside the handler triggered by the user's invocation. You get the tab object on the action click event; outside of it, tab URLs are masked. If you need persistent URL visibility for a specific site, use optional_host_permissions and request that origin.

Will activeTab work for keyboard shortcuts?

Yes. Keyboard commands declared via commands in the manifest count as user invocation and grant activeTab for the current tab. Same for context menu clicks.

What if a user denies my optional permission?

Handle it. Your feature shouldn't crash — show a clear explanation of what they would unlock and let them try again later. Re-asking immediately after a denial is annoying and sometimes against Web Store policy. Wait for the next natural prompt.

Does the broad warning return if I add a single broad permission later?

Yes — any update that broadens the install-time permission set disables the extension until the user re-approves. Your weekly active users will dip while users re-approve. Be deliberate about adding permissions in a release.

How do I see what dialog Chrome will actually show?

Load the unpacked extension into a clean profile and read the permission string Chrome displays. chrome://extensions → your extension → Details also shows the cumulative permission string. That's the canonical source — not your guess from the manifest.

Can I track which permissions users actually grant?

Yes, indirectly. Fire an event on chrome.permissions.onAdded / onRemoved with the origin pattern (not the URL). Joining grant rate × activation rate × retention tells you which sites your users actually want your extension on — that feeds back into your listing description and feature work. See the metrics tracking guide for the firing pattern.

Does this affect the "why users uninstall" numbers?

Tangentially. Broad install-time warnings tend to drive never-activated uninstalls (users install, regret, leave within a day). After the permission migration, the never-activated bucket usually shrinks — see the diagnostic breakdown in why users uninstall your Chrome extension.

Measure the permission lift, don't guess it
Crxlytics tracks install attribution per source, activation rate per release, and the channel-by-channel CVR delta before and after a permission migration. Anonymous-by-default, no remote code, no policy risk.
Get started free →