Skip to main content
The tabs API needs no bundler support. Extension.js compiles chrome.tabs calls like any other code. What it does change is the manifest that you run against during development, and that difference has a trap in it.

Development injects the tabs permission

The dev loop reloads content scripts by injecting them into tabs that are already open. That needs permissions your extension may not declare, so Extension.js adds them to the manifest that it writes into dist/. For a manifest v3 project, development adds these to permissions:
It also unions every content script match pattern into host_permissions. For a manifest v2 project, development adds tabs and storage, plus the match patterns, because manifest v2 has no host_permissions key. None of this reaches production. extension build emits exactly the permissions that you declared. You can see the difference yourself. Declare only storage in manifest.json, then compare the two builds:
dist/chromium/manifest.json
dist/chromium/manifest.json

The trap, and how to avoid it

Because development injects tabs, a call to chrome.tabs.query succeeds in extension dev even when manifest.json never asked for the permission. The same call fails after extension build. Extension.js warns about this for some APIs. Use chrome.management without declaring it and the build says so:
The warning covers storage, scripting, and management. It does not cover tabs. A project that calls chrome.tabs without declaring the permission compiles clean, runs clean in development, and breaks in the packaged extension. Declare what you use:
manifest.json
Then confirm against a production build before you ship:
Load dist/chromium as an unpacked extension and exercise the feature.

Do you need the tabs permission at all?

Many extensions do not. The tabs permission exists to read privileged fields, not to call the API. activeTab is the smaller request. It grants access to the tab that the user acted on, for as long as that visit lasts. Stores review it more kindly than tabs plus <all_urls>. Read Permissions and host permissions for the full set.

Cross-browser naming

Firefox and Safari ship a promise-based browser.tabs. Chromium ships a callback-based chrome.tabs. Extension.js provides the browser namespace on Chromium through webextension-polyfill, so one spelling works everywhere:
Read Cross-browser compatibility for how that is wired.

Targeting one tab from the terminal

Several CLI verbs act on a single tab. List the open tabs first:
Each row carries an id, a URL, a title, an active flag, and a window id. Pass the id:
--tab takes a numeric tab id and nothing else. To match on address instead, use --url, which accepts a match pattern or a plain substring:
With neither flag, the active tab in the last focused window is used. Other verbs that take a tab:
The two mean different things. On reload, --tab chooses what to act on. On logs, it filters events that were already recorded. extension storage has no --tab option. Choose a surface with --context instead.

Failure messages

The last one is common on chrome:// pages and on the Web Store, which no extension may touch.

Best practices

  • Declare tabs when you read tab URLs. Development will not remind you.
  • Prefer activeTab when a user gesture starts the work.
  • Verify permissions against a production build, not against the dev build.
  • Never assume a tab id survives a browser restart. Query for it again.

Next steps