Self-hosting Chromium extensions

  • Gérald Barré

Google Chrome and Microsoft Edge provide a store to publish extensions. Extensions published on the store are validated by testers, so you know the quality is ok and the extension does what the description said. However, there is no staging environment before publishing a new version. Also, you cannot publish personal extensions not testable by other people. What you can do is to create a private store. This is not that hard, but it requires some work. In this post, I describe how to self-host extensions for chromium-based browsers such as Google Chrome or Microsoft Edge at no cost.

For security reasons, you cannot install extensions from outside the store without manual configuration. Also, the website serving the extension must use the correct MIME type for the extension file. Indeed, extensions can be harmful, so the browser does not want to install them without your explicit consent.

#Get a domain name

First, you'll need a domain name. It can be a free domain name or a domain name that you own. For this sample, I'll use a free Azure Static Web App. This app comes with a generated domain such as It will also provide hosting for static pages, so we'll be able to host our Chromium extension.

Follow this tutorial to create the application and get the domain name: Quickstart: Building your first static site with Azure Static Web Apps

#Deploy the extension

##Repository structure

The repository looks like this:

# GitHub Action file to deploy the website to Azure Static Web App
# This file is generated when you create the App on Azure

# Extension source code


The files that are deployed to the website are in the dist folder:


##Repository content

Create a manifest file src/manifest.json, and replace the update_url with your actual domain:

src/manifest.json (JSON)
  "manifest_version": 3,
  "name": "sample-extension",
  "version": "1.0.0",
  "description": "My sample extension",
  "icons": {
    "128": "icon.png"
  "update_url": "",
  "background": {
    "service_worker": "background.js",
    "type": "module"

Generate the PEM key to sign the extension. This file allows to preserve the same extension id when you publish a new version of the extension. This is important to allow the browser to automatically update the extension.

openssl genrsa -out sample-extension.private.pem 2048
openssl pkcs8 -topk8 -nocrypt -in sample-extension.private.pem -out sample-extension.pem

Pack the extension using chrome.exe:

# Paths to "src" and the pem file must be absolute
$src = Resolve-Path src
$pem = Resolve-Path sample-extension.pem
& "${env:ProgramFiles}\Google\Chrome\Application\chrome.exe" --pack-extension=$src --pack-extension-key=$pem

Move the generated crx file to the dist folder:

Move-Item -Path $src\..\src.crx -Destination dist\sample-extension.crx

Extensions can only be installed by clicking on a link. So, you need to create a web page with a link to download the extension. Create a new file named dist/index.html. You can do something fancy, or just a simple page with a link to the extension:

dist/index.html (HTML)
<a href="sample-extension.crx">sample-extension.crx</a>

To allow the browser to automatically update the extension, we need to create a manifest file. Create a file dist/manifest.xml. This file contains a list of app. Each app represents an extension. You need to provide the extension id, the latest version, and a link to the extension file. To get the extension id, you can open about://extensions, enable the developer mode, and drop the .crx file on the page to install the extension. Then, it should show the extension id in the list.

dist/manisfest.json (JSON)
<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='' protocol='2.0'>
  <app appid='cclehcjioikemgdocpmhpohbakhmhbbc'>

    <!-- The version attribute represent the latest version available for the extension. -->
    <!-- The browser use this value to auto-update the extension. -->
    <updatecheck codebase=''
                  version='1.0.0' />

Chrome has strict requirements to allow downloading an extension. One of them is that the extension file must be served with the correct MIME type. You can use the staticwebapp.config.json file to instruct Azure Static Web App which MIME type it must use for the .crx file.

dist/staticwebapp.config.json (JSON)
  "mimeTypes": {
    ".crx": "application/x-chrome-extension"

You can now deploy the website to Azure Static Web App. If you have created the Azure SWA and associated the GitHub repository, you should already have a workflow file and the secrets should be configured. If so, you should simply need to update the app_location property in the file:

.github/workflows/azure-static-web-apps.yml (YAML)
name: Azure Static Web Apps CI/CD
      - main
    types: [opened, synchronize, reopened, closed]
      - main

    if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
    runs-on: ubuntu-latest
    name: Build and Deploy Job
      - uses: actions/checkout@v2
          submodules: true
      - name: Build And Deploy
        id: builddeploy
        uses: Azure/static-web-apps-deploy@v1
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: "upload"
          app_location: "/dist"
          api_location: ""
          output_location: ""
          skip_app_build: true

    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    runs-on: ubuntu-latest
    name: Close Pull Request Job
      - name: Close Pull Request
        id: closepullrequest
        uses: Azure/static-web-apps-deploy@v1
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          action: "close"

#Configure the local machine and install the extension

First, you need to configure the local machine to allow extensions from your domain:

  • Microsoft Edge

    New-Item -Path HKLM:\SOFTWARE\Policies\Microsoft\Edge -Name ExtensionInstallSources -Force
    New-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallSources -Name "2" -Value "*" -Force
    New-Item -Path HKLM:\SOFTWARE\Policies\Microsoft\Edge -Name ExtensionInstallAllowlist -Force
    New-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionInstallAllowlist -Name "1" -Value "cclehcjioikemgdocpmhpohbakhmhbbc" -Force
  • Google Chrome

    New-Item -Path HKLM:\SOFTWARE\Policies\Google\Chrome -Name ExtensionInstallSources -Force
    New-ItemProperty -Path HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallSources -Name "2" -Value "*" -Force
    New-Item -Path HKLM:\SOFTWARE\Policies\Google\Chrome -Name ExtensionInstallAllowlist -Force
    New-ItemProperty -Path HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallAllowlist -Name "1" -Value "cclehcjioikemgdocpmhpohbakhmhbbc" -Force

If you are on Linux or Mac, you need to create a json file to configure the browser as indicated in the documentation

You can validate the configuration by opening the browser and navigating to about://policy. If the configuration is not correct, click on the "Reload Policies" button.

You can now navigate to the website and install the extension!

You can also install the extension using a registry key ExtensionInstallForcelist. This policy specifies a list of apps and extensions that install silently, without user interaction. Users can't uninstall or turn off this setting. Permissions are granted implicitly.

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub