Back to articles

runtime.onEnabled shipped in Chrome 155, onExtensionLoaded is next: how to wire both

Chrome 155 added runtime.onEnabled; runtime.onExtensionLoaded follows in 157. Which event fires when, one listener pattern for both, and the edge cases.

Maxim Kosterin
7 min read
A hairline toggle switch flipped on, its track filling with soft orange watercolor inside an open ink enso circle.
A hairline toggle switch flipped on, its track filling with soft orange watercolor inside an open ink enso circle.

Chrome 155 reached the stable channel on October 6 with a new extension event, runtime.onEnabled, that fires when your own extension is switched back on. It never made Chrome's "What's new in Chrome extensions" page; on Chrome's own docs it appears only in the chrome.runtime API reference, tagged "Chrome 155+".

It slipped past me too. My first pass over Chrome 155 concluded that no extension-API changes rode the milestone, and it took a second scan two weeks later to find this one. A broader sibling, runtime.onExtensionLoaded, is already queued behind it. Here's what each one does, how to listen for both without double-initializing, and the edge cases.

The gap these events close

Before 155, an extension had two lifecycle hooks of its own. runtime.onInstalled fires on install and update. runtime.onStartup fires when a browser profile that has the extension starts. The third case — the user switches the extension off and back on while the browser keeps running — sent the extension nothing.

The nearest hook pointed the wrong way. management.onEnabled reports other extensions being enabled, and it needs the management permission, a broad ask most extensions can't justify. The W3C WebExtensions issue #353, open since 2023, listed three fixes: a new runtime.onEnabled, a new "enable" reason on onInstalled, or letting management.onEnabled work without the permission when an extension listens for itself.

So a re-enabled extension came back silently. Anything it set up once — a native-messaging handshake, a license check, a sync session — stayed down until the next browser start, and it had no way to tell "the user turned me back on" from "the machine rebooted."

What landed in Chrome 155

The commit is 61c52ec, "[Extensions] Implement runtime.onEnabled lifecycle event", committed September 2, 2026. Chrome 155 branched on September 14 per the ChromiumDash schedule, so it made the cut with twelve days to spare. The commit message defines the behavior:

"With this change, extensions can listen to runtime.onEnabled, which fires whenever an extension transitions from a disabled state to an enabled state while the browser is running."

The listener takes no arguments. What counts as enabling was settled in the issue thread: "flipping the switch that toggles the extension either manually or through sync/policies/setEnabled API." So the chrome://extensions toggle, an enterprise policy change, a sync from another device and another extension calling management.setEnabled() all reach your handler. Chrome 156 followed on October 20 and carries it too.

The next one: onExtensionLoaded

On September 30, two days after Chrome 156 branched, Chromium landed 4f1b414, "Implement runtime.onExtensionLoaded lifecycle event". Its commit message names the problem plainly: extensions have "multiple disjoint lifecycle events (onInstalled, onStartup, onEnabled)" and no single one that fires whenever the extension becomes active.

onExtensionLoaded passes details.reason — install, update, browser_update, enable, startup or reload — plus details.previousVersion on updates. Since it landed after the 156 branch point, the first stable release it can reach is Chrome 157, scheduled for November 3. The API reference lists it as "Pending" today.

The name held up under one challenge. At the October 8 WECG meeting, a request to rename it runtime.onLoaded came up. Oliver Dunk (Chrome) preferred to keep the current name and Rob Wu (Mozilla) agreed; Timothy Hatcher (Apple) said he didn't like the redundancy. The name stays onExtensionLoaded, and there is no onLoaded to code against.

Which event fires when

Event Fires when Available
runtime.onInstalled install, update, browser update long-standing
runtime.onStartup a profile with the extension starts long-standing
runtime.onEnabled disabled → enabled while the browser runs Chrome 155+
runtime.onExtensionLoaded any of the above, plus reload, with a reason landed for Chrome 157
management.onEnabled another extension is enabled (needs management) long-standing

Both new events are Chrome-only for now. Firefox and Safari carry "supportive" labels on issue #353 but haven't shipped either one.

One listener pattern for every Chrome version

Register at the top level of the service worker, prefer onExtensionLoaded where it exists, and fall back to the older trio. Don't register both paths: onInstalled keeps firing in Chrome 157, so an extension listening to both would set itself up twice on every install.

// background.js — top level, so the listener survives service worker restarts
async function bringUp(reason) {
	// Must be idempotent: see the 157 edge case below.
	await connectNativeHost();
	await refreshLicense();
}
 
if (chrome.runtime.onExtensionLoaded) {
	// Chrome 157+: one event, with a reason.
	chrome.runtime.onExtensionLoaded.addListener(({ reason }) => bringUp(reason));
} else {
	chrome.runtime.onInstalled.addListener(({ reason }) => bringUp(reason));
	chrome.runtime.onStartup.addListener(() => bringUp("startup"));
	// Chrome 155–156 only; undefined on older Chrome and on Firefox/Safari.
	chrome.runtime.onEnabled?.addListener(() => bringUp("enable"));
}

connectNativeHost() and refreshLicense() stand in for your own setup code.

Edge cases worth knowing

Updated while disabled. If the extension updates while it's switched off and the user then turns it on, Chrome 157 can deliver both an update and an enable reason. A follow-up commit, 958dc38, makes only the higher-precedence reason fire (update beats install, reload, enable, browser_update and startup) and adds previousVersion to enable. It landed on October 20, after Chrome 157 branched, so expect it in 158 (stable November 17) unless it gets merged back. Until then, keep your setup safe to run twice.

Only while the browser runs. The commit scopes onEnabled to a switch flipped during a session. A browser launch is still onStartup's job.

Not every user has 155 yet. Chrome rolls stable out gradually, and managed fleets often pin older versions. The ?. above is what keeps those users working.

What to do now

  1. Move your re-initialization into one idempotent function. Native-host reconnects, license checks and sync restarts all belong there, whichever event calls it.
  2. Wire onEnabled today, behind the feature check. It's live on Chrome 155 and 156 stable.
  3. Switch to onExtensionLoaded when 157 lands, and keep the fallback branch for older Chrome, Firefox and Safari.
  4. Use the reason. Telling enable from install lets you greet a returning user differently from a new one.

If you're touching background code anyway, the free Manifest V3 generator builds a cross-browser manifest with live store-policy validation, and npx @extenshi/cli scan checks your own build before you upload it — 3 scans + 10 reads free, one-time, with a free account; prepaid packs past that.

See where your extension stands in the catalog → — manifest version, permissions and store status on Chrome, Firefox and Edge.

Sources


Technical claims come from the cited Chromium commits, the Chrome API reference, the W3C issue and meeting notes, and ChromiumDash milestone schedules, checked on October 21, 2026. Release dates are scheduled dates and can move; a change can also be merged back into an earlier milestone. If you think something here is inaccurate, tell us at [email protected].

Related Articles