Skip to content
C.W.K.
Stream
Lesson 04 of 05 · published

activeTab and Other Narrow Permissions — Avoiding the Scary Warnings

~11 min · activeTab, permissions, narrow-permissions, ux

Level 0Extension Curious
0 XP0/56 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"Half of Chrome's most-installed extensions never get the all-websites warning, because they use activeTab and a handful of narrow permissions that solve the same problems. Lesson 4 is the shortcut list — what to reach for instead of <all_urls>."

activeTab — The Workhorse

"activeTab" is a special API permission that grants temporary host access to the currently-active tab, starting when the user invokes the extension via:

  • Toolbar icon click (chrome.action.onClicked or popup open).
  • Keyboard command (chrome.commands.onCommand).
  • Context menu (chrome.contextMenus.onClicked).

The grant covers: chrome.scripting.executeScript on that tab, chrome.tabs.captureVisibleTab on that tab, and reading tab.url / tab.title. It lasts until the user navigates the tab to a new URL or closes the tab.

Crucially: no install warning. activeTab by itself adds nothing to the prompt. Combined with the user-gesture trigger pattern, you get "works on demand, only when asked" semantics without the all-sites alarm.

activeTab vs Programmatic Injection

Confusion point: activeTab grants host access; scripting grants the API. You usually want both:

"permissions": ["activeTab", "scripting"]

With those two declared, your SW can call chrome.scripting.executeScript on the user-active tab inside a gesture handler. No host_permissions needed. No content-script declaration needed unless you also want always-on injection.

Other Narrow Permissions Worth Knowing

  • tabGroups — read and modify Chrome's tab grouping. No install warning of its own.
  • notifications — show desktop notifications via chrome.notifications.create. Mild install warning ("Show notifications"); benign for most users.
  • alarms — schedule one-shot or recurring SW wake-ups. No install warning. Replaces setInterval/setTimeout (which die with the SW).
  • identity — OAuth flows for Google sign-in. Install warning summarizes the scopes you'll later request, not host access.
  • webNavigation — fine-grained navigation events. Comes with a "Read your browsing history" warning; tabs covers most of the same ground less scarily.
  • contextMenus — already in your manifest. No warning.
  • storage and sidePanel are quiet API permissions. commands, omnibox, and action are manifest keys, not permission strings.

The Permissions to Treat With Care

These each add their own loud warning. Use only when you truly need them, and consider gating behind optional_permissions:

  • history — "Read and modify your browsing history." Rarely needed; tabs plus a per-tab cache usually covers the use case.
  • cookies — "Read and modify cookies on all sites." Almost never needed for a clip-saving extension.
  • geolocation — "Detect your physical location." Surprises users; ask runtime.
  • nativeMessaging — "Communicate with cooperating native applications." Lets you talk to a desktop binary; opens an attack surface.
  • debugger — "Use the developer tools on any tab." Power-user only; install warning is loud and justified.
  • webRequest — "Block or modify network requests." MV3 restricted this heavily compared to MV2; even narrow use produces a warning.

The Substitution Game

Before declaring a host permission or a scary API permission, ask: "is there a narrower way?" Often yes:

  • Need a periodic background job? alarms, not setInterval + persistent worker.
  • Need to remember per-site preferences? storage, not cookies.
  • Need to read the current page? activeTab + scripting, not host_permissions.
  • Need to know which sites the user just visited? tabs with onUpdated, not history or webNavigation.
  • Need to act when the user types something? commands for a hotkey, or omnibox for an address-bar keyword. No debugger.
activeTab + scripting + storage is the no-warning trinity that covers a huge fraction of real extensions. Reach for narrow permissions first; declare host_permissions only when no narrower path exists.
How activeTab interacts with content_scripts.matches. activeTab does NOT auto-inject a declared content script — it just unlocks chrome.scripting and tab.url access. If you want a content script to load when activeTab is invoked, you must call chrome.scripting.executeScript from the gesture handler. The static content_scripts manifest entry is always-on for matching URLs and is governed by content_scripts.matches, separately from activeTab.

Code

background.js — activeTab unlocks scripting on the user-clicked tab·javascript
// background.js — activeTab + scripting in a gesture handler
chrome.action.onClicked.addListener(async (tab) => {
  if (!tab.id) return;
  // activeTab + scripting permits this even without host_permissions
  const [result] = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => ({ url: location.href, title: document.title }),
  });
  console.log("[ClipDeck SW] page info:", result.result);
});
background.js — an alarm for a real clock boundary·javascript
// background.js — midnight is a clock boundary the worker must be woken for
function scheduleNextMidnight() {
  const next = new Date();
  next.setHours(24, 0, 0, 0);
  chrome.alarms.create("clipdeck-midnight", { when: next.getTime() });
}

chrome.runtime.onInstalled.addListener(scheduleNextMidnight);
chrome.runtime.onStartup.addListener(scheduleNextMidnight);
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name !== "clipdeck-midnight") return;
  void refreshBadge();
  scheduleNextMidnight();
});

// pausedTabs is storage.session state and is cleaned on tabs.onRemoved,
// not by an hourly alarm.
manifest.json — the narrow-permission ClipDeck shape·json
{
  "permissions": [
    "storage",
    "activeTab",
    "scripting",
    "sidePanel",
    "contextMenus",
    "alarms",
    "tabs"
  ],
  "host_permissions": [],
  "optional_permissions": ["downloads", "notifications"],
  "optional_host_permissions": ["https://*/*", "http://*/*"]
}

External links

Exercise

Add alarms to permissions and use the midnight alarm to recompute the daily badge without a storage event. Temporarily schedule it one minute ahead for the test, then restore the next-midnight calculation. Confirm pausedTabs stays in storage.session and is cleaned by tabs.onRemoved instead of an hourly sweep.
Hint
If the install warning is unchanged after dropping host_permissions, content_scripts.matches: ['<all_urls>'] is still doing the work — narrow it (Lesson 3) or switch to activeTab + programmatic injection (this lesson). If chrome.alarms.onAlarm never fires, the alarm was probably not created because chrome.runtime.onInstalled didn't run yet — try removing the extension and re-loading the unpacked directory, which triggers a fresh onInstalled. The chrome.alarms minimum period in MV3 is 30 seconds; values below that are silently rounded up.

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.