activeTab vs Host Permissions
Decide between activeTab and declared host permissions in an MV3 extension: what each grants, which gestures trigger activeTab, what stops working without hosts, and hybrid designs that keep install warnings low.
Table of Contents
You are writing the manifest and need to decide how the extension gets onto web pages. Declaring "<all_urls>" makes every feature work everywhere, and also shows “Read and change all your data on all websites” in the install dialog, sends the extension to in-depth review, and invites users to restrict it to “on click” anyway. Declaring only activeTab produces no warning at all, but nothing runs until the user clicks. Most extensions need a deliberate mix, and choosing it early saves a painful permission change after launch. This guide belongs to host permissions and site access.
What each mechanism actually grants
activeTab is a promise from the browser: “when the user explicitly invokes this extension on a tab, it may act on that tab’s current origin”. The invocation can be a click on the toolbar action, a click on one of the extension’s context menu items, a declared keyboard command, or acceptance of an omnibox suggestion. The grant covers the tab’s main frame origin and lets the extension inject scripts with chrome.scripting, read tab.url, tab.title and tab.favIconUrl, and call chrome.tabs.captureVisibleTab. It ends when the tab navigates to another origin or closes. Declared host permissions, by contrast, are standing grants: content scripts inject automatically, background code can act on matching tabs at any time, and cross-origin requests and cookie access work for those hosts without any user action.
Step-by-step: choose and implement the model
1. Classify every feature by trigger
List each feature that touches page content and ask one question: does it run because the user did something, or by itself? “Summarise this page”, “save this image”, “translate the selection” and “fill this form” are user-triggered. “Highlight prices on every shopping site”, “warn me before I submit a password on a phishing page” and “show the reading time on every article” run by themselves. The first group fits activeTab. The second needs host permissions — ideally narrow ones, ideally optional.
1// features.js — make the classification explicit and reviewable
2export const FEATURES = {
3 summarise: { trigger: "user", hosts: null }, // activeTab
4 saveImage: { trigger: "user", hosts: null }, // activeTab via context menu
5 priceHighlight: { trigger: "auto", hosts: ["https://*.shop.example/*"] },
6 phishingGuard: { trigger: "auto", hosts: ["<all_urls>"], optional: true },
7};
Execution context: a shared module, used by build tooling to generate the manifest’s host lists and by the options page to show which features need which grants. Keeping the mapping in data makes a code review of a new host request a one-line diff.
2. Implement user-triggered features with activeTab
1{
2 "permissions": ["activeTab", "scripting", "contextMenus"],
3 "action": { "default_title": "Summarise this page" }
4}
1// sw.js
2chrome.runtime.onInstalled.addListener(() => {
3 chrome.contextMenus.create({ id: "save-image", title: "Save image to Board", contexts: ["image"] });
4});
5
6chrome.action.onClicked.addListener((tab) => summarise(tab.id));
7chrome.contextMenus.onClicked.addListener((info, tab) => {
8 if (info.menuItemId === "save-image") saveImage(tab.id, info.srcUrl);
9});
10
11async function summarise(tabId) {
12 const [{ result }] = await chrome.scripting.executeScript({
13 target: { tabId },
14 func: () => document.body.innerText.slice(0, 50_000),
15 });
16 // … send to the summariser
17}
Execution context: the service worker. Both handlers run immediately after a gesture, so activeTab has already granted the tab. The grant is per tab: clicking in a different tab grants that one separately. The func runs in the page’s isolated world; its return value must be structured-cloneable. Firefox and Safari behave identically for these triggers.
3. Implement automatic features with the narrowest hosts
1{
2 "host_permissions": ["https://*.shop.example/*"],
3 "content_scripts": [{
4 "matches": ["https://*.shop.example/*"],
5 "js": ["price-highlight.js"],
6 "run_at": "document_idle"
7 }]
8}
Execution context: the manifest. A content script’s matches implicitly needs host access for the same patterns; listing them in host_permissions too makes the dependency explicit and is what Chrome uses for the install warning. A narrow pattern produces a warning naming the site — “Read and change your data on shop.example” — which users accept far more readily than the all-sites warning.
4. Make broad automatic features optional
For a feature that genuinely needs every site, put the broad pattern in optional_host_permissions and ask when the user enables it.
1// options.js
2toggle.addEventListener("change", async () => {
3 if (toggle.checked) {
4 toggle.checked = await chrome.permissions.request({ origins: ["<all_urls>"] });
5 } else {
6 await chrome.permissions.remove({ origins: ["<all_urls>"] });
7 }
8});
Execution context: the options page, inside the change handler so the user gesture is still active. The browser shows the all-sites warning at this moment, in context, to a user who has just asked for the feature — the best possible moment for that warning. Content scripts for the feature should be registered dynamically after the grant, as described in detecting when host permissions are granted or revoked.
5. Avoid the “tabs” permission trap
Many extensions request "tabs" only to read tab.url. That permission produces its own warning — “Read your browsing history” — and is unnecessary when the extension already has host access to the tab, or when the URL is needed only after a gesture.
1// With activeTab, the clicked tab's url is available in the handler
2chrome.action.onClicked.addListener((tab) => {
3 console.log(tab.url); // defined: activeTab granted this tab
4});
5
6// Without a gesture or host permission, url is undefined in query results
7const tabs = await chrome.tabs.query({});
8console.log(tabs[0].url); // undefined unless "tabs" or host access
Execution context: the service worker. chrome.tabs.query filtering by url also requires access to the tabs in question. Reach for "tabs" only when the feature genuinely needs every tab’s URL — a tab manager, for example — and justify it in the listing.
Cross-browser variation
- Chrome / Edge:
activeTabtriggers include action click, context menu, keyboard command and omnibox. Users can additionally restrict declared hosts to “on click”, in which caseactiveTab-style grants are all the extension gets. - Firefox: same triggers and lifetime for
activeTab. Declared hosts can be revoked per host from the add-on’s permissions panel. - Safari:
activeTabworks, and Safari’s per-site permission prompt often appears on first use even for declared hosts — users may choose “Allow for one day”, so an automatic feature can lose access overnight.
Verification
- Install the build with only
activeTabfor user-triggered features. Confirm the install dialog shows no site-data warning. - Click the action on a page and confirm the feature works; navigate to another site and confirm a background
executeScriptinto the same tab now fails. - In Chrome’s extensions menu, set site access to “On click” and confirm automatic features stop on their hosts while click-triggered ones still work.
- Enable the optional broad feature and confirm the warning appears in context.
1await chrome.permissions.getAll();
2// { permissions: ["activeTab","scripting","contextMenus","storage"], origins: ["https://*.shop.example/*"] }
Execution context: the service worker console. activeTab grants never appear in getAll — they are temporary and per tab — so do not use getAll to test whether a click-triggered feature will work.
FAQ
Does activeTab work in the popup without a click on the action?
Opening the popup is the click on the action, so the popup can act on the active tab. A popup opened programmatically with chrome.action.openPopup() does not count as a user gesture for this purpose.
Can activeTab inject into iframes?
It grants the tab’s top-level origin. Frames from other origins need host permission for those origins; same-origin frames are covered.
Will switching from all_urls to activeTab in an update disable the extension?
No. Removing permissions never triggers re-consent. Adding them does, which is why it pays to start narrow.
Related
- Handling user-restricted site access — when users choose “on click” for you.
- Injecting only after a user gesture with activeTab — the injection side in detail.
- The review cost of all_urls and broad host patterns — what broad hosts cost you.
- Host permissions and site access — the parent topic.