Skip to content

Handling Extension Updates

When releasing an update to your extension, there's a couple of things you need to keep in mind:

Content Script Cleanup

Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API (ie, the browser or chrome globals).

WXT provides a utility for handling this process: ContentScriptContext. An instance of this class is provided to you automatically inside the main function of each content script.

When your extension updates or reloads, the context will become invalidated, and will trigger any ctx.onInvalidated listeners you add:

export default defineContentScript({
  main(ctx) {
    ctx.onInvalidated(() => {
      // Do something

The ctx also provides other convenient APIs for stopping your content script without manually calling onInvalidated to add a listener:

  1. Setting timers:
    ctx.setTimeout(() => { ... }, ...);
    ctx.setInterval(() => { ... }, ...);
    ctx.requestAnimationFrame(() => { ... });
  2. Adding DOM events:
    ctx.addEventListener(window, "mousemove", (event) => { ... });
  3. Implements AbortController for canceling standard APIs:
    fetch('...', {
      signal: ctx.signal,

Other WXT APIs require a ctx object so they can clean themselves up. For example, createIntegratedUi, createShadowRootUi, and createIframeUi automatically unmount and stop a UI when the script is invalidated.


When working with content scripts, you should always use the ctx object to stop any async or future work.

This prevents old content scripts from interfering with new content scripts, and prevents error messages from being logged to the console in production.

Testing Permission Changes

When permissions/host_permissions change during an update, depending on what exactly changed, Chrome will disable your extension until the user accepts the new permissions.

It is possible to test this before you release an update, but it's not a simple process:

  1. Get 2 ZIPs of your extension, both generated by wxt zip. The first contains a previous version of your extension, the second contains the latest version. Make sure the second ZIP's version is higher than the first's.
  2. Unzip the two ZIP files somewhere next to each other that's easy to locate.
  3. In Chrome, open chrome://extensions and make sure developer mode is enabled
  4. Pack the first extension into a CRX, generating a new private key:
    1. Click "Pack Extension" in the top left
    2. For "Extension root directory", enter the path to the first extracted zip directory. The directory should contain a manifest.json file
    3. Leave "Private key file" blank
    4. Click "Pack Extension". This will generate a .crx and .pem file
  5. Pack the second extension into a CRX, reusing the private key generated by the previous step
    1. Click "Pack Extension" in the top left
    2. For "Extension root directory", enter the path to the second extracted zip directory.
    3. For "Private key file", enter the path to the generated .pem private key file
    4. Click "Pack Extension". This will generate a second .crx file.
  6. Install the first CRX file by dragging and dropping it onto the chrome://extensions page
  7. Install the second CRX file by dragging and dropping it onthe the chrome://extensions page

If a new permission must be accepted, you'll be prompted to accept it after dropping the second CRX file onto the page.

:::Info Note Note: Chrome no longer allows self-signed CRX extensions to run, but that's OK for this test case. We're still prompted to accept new permissions, even if we can't interact with the installed extension.

To validate this, you can create a third ZIP file with a rare permission like geolocation in the manifest, that's guarenteed to reprompt permissions when added. :::

Update Event

You can setup a callback that runs after your extension updates like so:

browser.runtime.onInstalled.addListener(({ reason }) => {
  if (reason === 'update') {
    // Do something

If the logic is simple, write a unit test to cover this logic. If you feel the need to manually test this callback, you can either:

  1. In dev mode, remove the if statement and reload the extension from chrome://extensions
  2. Build two ZIPs with the same runtime ID and actually update the extension

The first approach is very straightforward. The second is more complicated...

Here are the steps:

So the steps:

  1. Checkout an old commit.
  2. Add a key to the manifest in your wxt.config.ts.
  3. Run wxt zip to create the first ZIP.
  4. Stash or reset changes and checkout your latest code.
  5. Add back the same key to your manifest.
  6. Make sure the extension's version is higher than the first zip. It can be any version that's higher, since you won't be releasing this version.
  7. Run wxt zip to create the second ZIP.
  8. In a fresh chrome profile, go to chrome://extensions, enable dev mode, and drag and drop the first zip onto the page to install it.
  9. In the extension, play around and setup your test case.
  10. Back on chrome://extensions, drag and drop your second zip onto the page.

If you setup the key correctly, it will cause the extension to act like it was updated instead of installing a second version of your extension.