If you've ever tried to automate Firefox extension installation — maybe for a testing pipeline, a standardized dev environment, or just because you're the kind of person who wants to script everything — you've probably hit a wall. Firefox doesn't make this easy, and the reasons why are actually interesting from a browser architecture perspective.
I recently went down this rabbit hole after seeing a wild experiment on Hacker News where someone attempted to install every Firefox extension. It got me thinking about the actual mechanics of extension installation and why bulk-managing extensions is such a pain.
The Problem: Firefox Extensions Aren't Just Files You Drop In
Here's the naive assumption most of us start with: extensions are just .xpi files (which are really ZIP archives), so you should be able to dump them into a folder and call it a day.
Nope.
Firefox has a multi-layered extension system that involves signature verification, manifest parsing, compatibility checks, and a database that tracks installation state. If you just copy XPI files into a profile directory, Firefox will cheerfully ignore them — or worse, flag them as corrupted.
Understanding Firefox's Extension Architecture
Firefox manages extensions through a few key components:
-
extensions.json— The database file in your profile directory that tracks all installed extensions, their state, and metadata -
addons.json— Cache of add-on metadata from AMO (addons.mozilla.org) -
The
extensions/directory — Where unpacked extension files actually live inside the profile - Signature verification — Since Firefox 43, all extensions must be signed by Mozilla (with limited exceptions)
The extension installation flow goes roughly like this: Firefox downloads the XPI, verifies the signature, checks the manifest.json (or legacy install.rdf), unpacks it, registers it in extensions.json, and then loads it. Skip any of these steps and things break.
Step 1: Downloading Extensions from AMO
The Mozilla Add-ons API is actually pretty well-documented. You can query and download extensions programmatically:
import requests
import os
def download_extension(slug, download_dir="./extensions"):
"""Download a Firefox extension by its AMO slug."""
# AMO v5 API endpoint for extension details
api_url = f"https://addons.mozilla.org/api/v5/addons/addon/{slug}/"
resp = requests.get(api_url)
resp.raise_for_status()
data = resp.json()
# Get the download URL for the latest version
current = data["current_version"]
xpi_url = current["file"]["url"]
# Download the actual XPI file
xpi_resp = requests.get(xpi_url)
os.makedirs(download_dir, exist_ok=True)
filename = f"{slug}-{current['version']}.xpi"
filepath = os.path.join(download_dir, filename)
with open(filepath, "wb") as f:
f.write(xpi_resp.content)
return filepath
If you want to get a list of extensions to work with, the search endpoint is your friend:
def search_extensions(query, page_size=25):
"""Search AMO for extensions matching a query."""
url = "https://addons.mozilla.org/api/v5/addons/search/"
params = {
"q": query,
"type": "extension",
"page_size": page_size,
"sort": "users" # sort by popularity
}
resp = requests.get(url, params=params)
resp.raise_for_status()
return resp.json()["results"]
This part works fine. The headaches start when you try to actually install what you've downloaded.
Step 2: The Right Way to Install Extensions Programmatically
There are a few legitimate approaches, and which one you should use depends on your use case.
Option A: Using Firefox Policies (Best for Managed Environments)
Firefox supports enterprise policies that can force-install extensions. Create a policies.json file:
{
"policies": {
"ExtensionSettings": {
"uBlock0@raymondhill.net": {
"installation_mode": "force_installed",
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi"
},
"addon@darkreader.org": {
"installation_mode": "force_installed",
"install_url": "https://addons.mozilla.org/firefox/downloads/latest/darkreader/latest.xpi"
}
}
}
}
On macOS, this goes in Firefox.app/Contents/Resources/distribution/. On Linux, it's /usr/lib/firefox/distribution/ or /etc/firefox/policies/. On Windows, it's in the Firefox installation directory under distribution/.
This is the cleanest approach for dev environments because Firefox handles all the verification and registration itself.
Option B: Using Selenium/WebDriver (Best for Testing)
If you're setting up extensions for automated testing, the WebDriver approach is straightforward:
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
options = Options()
# install_addon returns the addon ID for later removal
options.add_argument("-profile")
driver = webdriver.Firefox(options=options)
# Install from local XPI file
driver.install_addon("/path/to/extension.xpi", temporary=True)
# temporary=True means it won't persist after the session
# Use temporary=False if you need it to stick around
Option C: Direct Profile Manipulation (Fragile, But Sometimes Necessary)
If you need to pre-bake extensions into a Firefox profile — say, for a Docker container or a portable dev setup — you can place signed XPI files directly in the profile's extensions/ directory. The catch: the filename must be the extension's ID.
# First, find the extension ID from the XPI's manifest
unzip -p extension.xpi manifest.json | python3 -c "
import sys, json
manifest = json.load(sys.stdin)
# The ID lives in browser_specific_settings.gecko.id
print(manifest.get('browser_specific_settings', {}).get('gecko', {}).get('id', 'NO_ID_FOUND'))
"
# Then copy the XPI with that ID as the filename
cp extension.xpi ~/.mozilla/firefox/YOUR_PROFILE/extensions/{extension-id}.xpi
Firefox will detect the new file on next startup and go through its verification process. If the extension is properly signed, it should install. But this approach breaks if Firefox's internal state gets confused, and there's no good error reporting when it fails.
Why Bulk Installation Breaks Down
Here's where it gets interesting if you're trying to install a lot of extensions at once. Several things go wrong:
Memory and startup time. Each extension adds overhead. Firefox loads all extensions during startup, and each one runs in its own context. Installing hundreds will make Firefox take minutes to start — if it starts at all.
Extension conflicts. Extensions that modify the same web APIs or content scripts that target the same pages will fight each other. Two ad blockers will interfere. Multiple password managers will race to fill the same form fields.
The extensions.json bottleneck. Firefox's extension database is a single JSON file. With hundreds of entries, parsing and updating it becomes slow, and corruption becomes more likely if Firefox doesn't shut down cleanly.
AMO rate limiting. If you're downloading extensions programmatically, Mozilla's servers will rate-limit you. The API doesn't publish specific limits, but in my experience you'll start hitting 429 responses after a few hundred requests in quick succession. Add a delay:
import time
def download_with_backoff(slug, max_retries=3):
for attempt in range(max_retries):
try:
return download_extension(slug)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
wait = 2 ** attempt * 10 # exponential backoff
print(f"Rate limited. Waiting {wait}s...")
time.sleep(wait)
else:
raise
raise Exception(f"Failed to download {slug} after {max_retries} retries")
Prevention: Building Reproducible Browser Environments
If the actual goal is having a consistent set of extensions across machines (the realistic version of this problem), here's what I'd recommend:
-
Use Firefox policies for any managed or team environment. Check the
policies.jsoninto your dotfiles repo. -
For CI/testing, use Selenium's
install_addon()with temporary installs. Don't try to pre-bake profiles. - For Docker-based testing, build a profile with your extensions once, verify it works, then copy the entire profile directory into your image.
- Always pin extension versions. The AMO API lets you download specific versions — use that instead of always grabbing latest.
The Firefox Enterprise documentation covers the policies approach thoroughly, and the AMO API docs are the reference for programmatic access.
The Takeaway
Firefox's extension system is designed for humans installing a handful of add-ons through the browser UI. Once you step outside that happy path, you're fighting against assumptions baked into the architecture. That's not necessarily bad — those assumptions exist for security reasons — but it means automation requires understanding the internals rather than just moving files around.
The policies approach is genuinely good, though. If you're not using it for your team's dev environments, it's worth the 10 minutes to set up.
Top comments (0)