Host Permissions in Firefox MV3 Are Optional

Why Firefox treats MV3 host permissions as user-revocable, how versions before and after 127 differ, and how to detect missing hosts, prompt on first run and keep content scripts working in Firefox.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

The Chrome build works out of the box. The Firefox build of the same MV3 extension installs cleanly, shows no errors, and does nothing: content scripts never inject, fetch to your API fails with a CORS error, and tabs.query returns tabs without URLs. Opening the add-on’s page in about:addons reveals the cause — under Permissions, every site toggle is off. Firefox’s MV3 model treats host permissions as something the user controls, and depending on the version and how the add-on was installed, they may start out ungranted. This guide covers how to handle that. It belongs to host permissions and site access.

Firefox’s model for MV3 hosts

When Firefox introduced MV3 support in version 109, it decided that host_permissions would behave like optional permissions: listed in the manifest, shown to the user, but not granted until the user switched them on from the extensions panel or the add-on’s permissions tab. That was a deliberate privacy stance and a large compatibility surprise. From Firefox 127, hosts in host_permissions are shown in the install prompt and granted at install, much like Chrome — but they remain individually revocable at any time from about:addons, extensions installed before 127 may still be ungranted, and temporary add-ons loaded from about:debugging follow their own rules. The safe assumption for Firefox is the same as for Chrome’s “on click” mode: declared hosts may or may not be granted, and the code must check.

Firefox host permission behaviour over timeFirefox 109 shipped MV3 with host permissions ungranted by default; Firefox 127 began requesting them at install; throughout, users can revoke individual hosts from the add-on's permissions tab.Firefox 109todayMV3 launchhosts off by defaultFirefox 127+granted at installAlwaysuser can revoke per hostinstall prompt starts listing hostsusers can revoke hosts at any time
Newer Firefox grants at install — but revocation has always been one click away.

Step-by-step: a Firefox build that copes

1. Declare a gecko id and the hosts as usual

1{
2  "manifest_version": 3,
3  "browser_specific_settings": { "gecko": { "id": "readable@acme.example", "strict_min_version": "115.0" } },
4  "host_permissions": ["https://*.example.com/*"],
5  "optional_host_permissions": ["https://*/*"],
6  "background": { "scripts": ["background.js"], "type": "module" }
7}

Execution context: the Firefox manifest. Firefox MV3 runs the background as an event page by default, declared with scripts rather than service_worker; a build tool can emit both keys from one source, as covered in generating a manifest per browser target. The host declarations themselves are identical to Chrome’s.

2. Check on install and on startup

 1// background.js
 2const REQUIRED = { origins: ["https://*.example.com/*"] };
 3
 4async function checkHosts(reason) {
 5  const ok = await browser.permissions.contains(REQUIRED);
 6  await browser.storage.local.set({ hostsGranted: ok });
 7  if (!ok && reason === "install") {
 8    await browser.tabs.create({ url: browser.runtime.getURL("onboarding.html#hosts") });
 9  }
10  await browser.action.setBadgeText({ text: ok ? "" : "!" });
11}
12
13browser.runtime.onInstalled.addListener(({ reason }) => checkHosts(reason));
14browser.runtime.onStartup.addListener(() => checkHosts("startup"));
15browser.permissions.onAdded.addListener(() => checkHosts("added"));
16browser.permissions.onRemoved.addListener(() => checkHosts("removed"));

Execution context: the Firefox background script, with listeners registered at the top level. Opening an onboarding page on install when hosts are missing turns a silent failure into a one-click fix. The badge keeps the problem visible for users who dismiss onboarding. This same code runs unchanged in Chrome with chrome.*, where it simply finds the hosts granted in most cases.

First run in Firefox with hosts ungrantedOn install the background checks permissions, finds hosts missing, and opens onboarding; the user clicks Grant, which calls permissions.request; onAdded fires and the background clears the badge.BackgroundOnboarding pageFirefoxpermissions.contains → falsetabs.create(onboarding)click → permissions.requestprompt acceptedonAddedbadge cleared
The request must come from the onboarding page's click — the background cannot prompt on its own.

3. Request from the onboarding page on a click

1// onboarding.js
2document.querySelector("#grant").addEventListener("click", async () => {
3  const granted = await browser.permissions.request({ origins: ["https://*.example.com/*"] });
4  document.body.dataset.state = granted ? "granted" : "declined";
5});

Execution context: an extension page in a normal tab. Firefox, like Chrome, requires permissions.request to run from a user gesture; calling it from the background script throws “permissions.request may only be called from a user input handler”. Requesting a host that is already declared in host_permissions is allowed in Firefox — it simply re-grants it.

4. Make content scripts resilient to late grants

In Firefox, manifest content scripts for an ungranted host do not inject. When the user grants the host later, already-open tabs do not receive the script retroactively.

1browser.permissions.onAdded.addListener(async ({ origins = [] }) => {
2  if (origins.length === 0) return;
3  const tabs = await browser.tabs.query({ url: origins });
4  for (const tab of tabs) {
5    browser.scripting.executeScript({ target: { tabId: tab.id }, files: ["content.js"] })
6      .catch(() => {});                        // tab may be on a privileged page
7  }
8});

Execution context: the Firefox background script. Querying tabs by the newly granted patterns and injecting into them makes the grant take effect immediately rather than on the next page load. content.js must be idempotent, because a page reload will inject it again from the manifest declaration. The same pattern helps Chrome users who widen site access from “on click”.

5. Remember Firefox’s content script privilege difference

Firefox grants content scripts with host permission extension-level cross-origin access, unlike Chrome. Code that relies on that will fail in Chrome; code that proxies through the background works everywhere.

1// Portable: ask the background to fetch, in every engine
2const data = await browser.runtime.sendMessage({ type: "lookup", term });

Execution context: a content script. The reverse surprise also occurs — a Firefox-first extension whose content-script fetches break the moment it is ported to Chrome. See making cross-origin fetch requests from an extension.

6. Write the onboarding copy for Firefox’s UI

Firefox users grant hosts in two places: the puzzle-piece extensions panel, where an add-on that wants access shows a request, and the Permissions tab of the add-on’s page in about:addons. Your copy should name those places and show a screenshot of the toggle. Generic “allow access” instructions written for Chrome’s site-access menu confuse Firefox users, who do not have that menu.

Host grant behaviour, Chrome versus FirefoxComparison of install-time granting, user revocation, background prompting and content-script cross-origin privilege between Chrome and Firefox MV3.BehaviourChromeFirefoxGranted at installYes127+, yes; earlier, noUser can revokeSite access menuPer-host toggleBackground can promptNoNoContent script CORS bypassNoYes
Both let users take hosts away; Firefox has historically also started without them.

Common mistakes

  • Testing only as a temporary add-on. Add-ons loaded from about:debugging behave differently from signed installs. Test the signed .xpi from AMO’s unlisted channel on a clean profile before every release.
  • Prompting from the background. permissions.request from a background script throws in Firefox exactly as it does in Chrome. The prompt must come from a click in an extension page — onboarding, popup or options.
  • Assuming a granted host stays granted. Firefox users can revoke a single host from about:addons at any time. Keep the onRemoved listener and the badge even after onboarding succeeds.
  • Relying on Firefox’s content-script fetch privileges. It works in Firefox and nowhere else; the Chrome build of the same code fails with CORS errors. Proxy through the background in every engine.
  • Leaving the onboarding page open forever. Once the grant arrives, have the onboarding page close itself or switch to a “you’re all set” state; a stale “please grant access” tab confuses users who already did.

Cross-browser variation

  • Chrome / Edge: hosts granted at install; the user may restrict them later. The same checks find them granted in the common case and catch the restricted case.
  • Firefox: hosts granted at install from 127, ungranted by default before that, revocable per host always. Temporary add-ons in about:debugging have hosts granted while loaded.
  • Safari: per-site prompts on first use regardless of declaration; the same “check, then request from a click” pattern applies.

Verification

  1. In Firefox, install the add-on, open about:addons → your add-on → Permissions, and turn the host toggle off.
  2. Restart Firefox. The badge should show “!” and the check should store hostsGranted: false.
  3. Turn the toggle on with a matching tab open. The content script should inject into that tab immediately, without a reload.
  4. Install the same build on a Firefox ESR older than 127 and confirm the onboarding page opens on install.

FAQ

Can I force Firefox to grant hosts at install?

Not on older versions. On 127 and later the install prompt includes them, which is effectively the same; but users can still revoke them, so the checks remain necessary.

Does this affect <all_urls> content scripts?

Yes — they are subject to the same grant. A content script matching all URLs injects nowhere until the corresponding host access is granted.

Is optional_host_permissions still useful in Firefox?

Yes. It keeps broad hosts out of the install prompt in Firefox 127+, exactly as in Chrome, and lets you request them in context.

Other MV3 Architecture & Extension Lifecycle Resources