Firefox has deprecated "side-loading" of extensions

From the announcement:

Today marks the release of Firefox 74 and as we announced last fall, developers will no longer be able to install extensions without the user taking an action. This installation method was typically done through application installers, and is commonly referred to as “sideloading.”

If you are the developer of an extension that installs itself via sideloading, please make sure that your users can install the extension from your own website or from (AMO).

Of course, it is still possible to side-load. The browser itself is installing the extension, and the browser isn't even closed source. It can't hide what it's doing, just obfuscate and write annoying blog posts about how taking away features is making users safer.

I spent some time yesterday figuring out how to side-load extensions and writing code to do this for my own purposes. I got a script working that gives an automated extension install into a new profile based on copying out of some other profile where the extension had been installed by firefox interactively.

What I'm doing is very coarse -- mostly just copying entire files, but editing the paths within them to refer to the new extension directory instead of the old one.

This works for me since I want to copy my entire list of extensions, but perhaps you want to surgically install just one extension. I didn't go that far with my code, but I can tell you how to do the surgery based on what I found. You just need to edit some JSON. (BTW jq is great.)

To get an extension running in a firefox profile, you need to edit the following files in the profile dir:


Put your *.xpi here.

Each extension needs a long entry in the addons array

Used to give extensions some special privileges, refers to extension by id (the id field in extensions.json)

Used to give extensions privilege to run in private tab, refers to extensions by id

Contains one addons object within each of the four labels "app-builtin", "app-profile", "app-system-defaults", "app-system-share".

I use to decompress and compress this file.

Needed to set extensions.webextensions.uuids or to make an extension run as the homepage or new tab page without user interaction. I also use it to bypass confirmation/warning popups, mozilla upgrade notice, etc.

Here is an example of the JSON for each file for one extension. This is generated by script and the values are edited (by jq) so that only one extension is shown.


  "app-profile": {
    "addons": {
      "dependencies": [],
      "enabled": true,
      "lastModifiedTime": 1592563110000,
      "loader": null,
      "path": "",
      "rootURI": "jar:file:///home/d/.mozilla/firefox/fm0hmz3f.second_gen4/extensions/!/",
      "runInSafeMode": false,
      "signedState": 2,
      "telemetryKey": "",
      "version": "2.0.3"
+ mozlz4 addonStartup.json.lz4
+ jq -c '.["app-profile"]["addons"][""] | keys'


  "": {
    "permissions": [
    "origins": []
+ jq -c '.[""] | keys' extension-preferences.json


  "addons": [
      "id": "",
      "syncGUID": "{c95f9e0d-6450-4383-87e4-2244586de7f5}",
      "version": "2.0.3",
      "type": "extension",
      "loader": null,
      "updateURL": null,
      "optionsURL": null,
      "optionsType": null,
      "optionsBrowserStyle": true,
      "aboutURL": null,
      "defaultLocale": {
        "name": "Tabliss",
        "description": "A beautiful New Tab page with many customisable backgrounds and widgets that does not require any permissions.",
        "creator": null,
        "developers": null,
        "translators": null,
        "contributors": null
      "visible": true,
      "active": true,
      "userDisabled": false,
      "appDisabled": false,
      "embedderDisabled": false,
      "installDate": 1592563110000,
      "updateDate": 1592563110000,
      "applyBackgroundUpdates": 1,
      "path": "/home/d/.mozilla/firefox/fm0hmz3f.second_gen4/extensions/",
      "skinnable": false,
      "sourceURI": "",
      "releaseNotesURI": null,
      "softDisabled": false,
      "foreignInstall": false,
      "strictCompatibility": true,
      "locales": [],
      "targetApplications": [
          "id": "",
          "minVersion": "54.0",
          "maxVersion": "*"
      "targetPlatforms": [],
      "signedState": 2,
      "seen": true,
      "dependencies": [],
      "incognito": "spanning",
      "userPermissions": {
        "permissions": [
        "origins": []
      "optionalPermissions": {
        "permissions": [],
        "origins": []
      "icons": {
        "32": "icons/32.png",
        "48": "icons/48.png",
        "96": "icons/96.png",
        "128": "icons/128.png"
      "iconURL": null,
      "blocklistState": 0,
      "blocklistURL": null,
      "startupData": null,
      "hidden": false,
      "installTelemetryInfo": {
        "source": "amo",
        "sourceURL": "",
        "method": "amWebAPI"
      "recommendationState": {
        "validNotAfter": 1744746924000,
        "validNotBefore": 1586958924000,
        "states": [
      "rootURI": "jar:file:///home/d/.mozilla/firefox/fm0hmz3f.second_gen4/extensions/!/",
      "location": "app-profile"
+ jq -c '.addons[] | select(.id == "") | keys' extensions.json


  "url_overrides": {
    "newTabURL": {
      "precedenceList": {
        "id": "",
        "installDate": 1592563110000,
        "value": "moz-extension://5457f46b-efee-42b2-bea9-19bcb10f7618/index.html",
        "enabled": true
+ jq -c keys extension-settings.json


user_pref("extensions.webextensions.uuids", "\{\"\":\"5457f46b-efee-42b2-bea9-19bcb10f7618\"\}");

In total, I copy these prefs (....mstone is assigned value "ignore" rather than copied):


It appears only the last value -- extensions.webextensions.uuids -- is crucial. I know it's needed if you want the browser.startup.homepage to refer to such a uuid (i.e., to use an extension as the homepage). If you don't include it, mozilla will regenerate the uuids and any copied reference to the old uuid (hiding anywhere) will fail.

This only matters if you are trying to copy configuration that refers to that UUID, otherwise Firefox just generates a new UUID for them. But since special permissions used by (and other extensions) require a matching UUID in extension-settings.json, you might need the configuration even if you don't want actual configuration.