{
	"schemaVersion": 2,
	"packVersion": "e4329885090b",
	"packDate": "2026-10-22",
	"advisories": [
		{
			"id": "alarms-clearall-resolves-undefined",
			"revision": 1,
			"kind": "breaks",
			"title": "alarms.clearAll() no longer resolves to a boolean",
			"browsers": {
				"firefox": {
					"version": "157",
					"date": "2026-09-29"
				},
				"chrome": {
					"version": "157",
					"date": "2026-11-03",
					"forecastEvent": "w3c-alarms-clearall-return-undefined"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "alarms.clearAll",
						"resultUsed": true
					}
				],
				"confidence": "high"
			},
			"why": "alarms.clearAll() now resolves to undefined instead of a boolean. Chrome used to resolve true every time, Firefox only when something was cleared. Any branch on the result is dead code on Firefox 157 already and on Chrome from Chrome 157 (stable November 3, 2026). WebKit merged the same change, so Safari returns undefined too.",
			"fix": "Stop branching on the value clearAll() returns. If you need to know whether alarms existed, call alarms.getAll() before clearing. Check alarms.clear() call sites while you are there: it was not part of this change, but code that branches on one often branches on the other.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://chromium-review.googlesource.com/c/chromium/src/+/8182627",
					"https://github.com/w3c/webextensions/issues/1055",
					"https://github.com/WebKit/WebKit/pull/76286"
				]
			},
			"verifiedAt": "2026-10-21",
			"fixtures": {
				"positive": [
					"if (await browser.alarms.clearAll()) console.log('cleared')",
					"const wasCleared = await chrome.alarms.clearAll()",
					"chrome.alarms.clearAll().then((ok) => ok && reset())",
					"chrome.alarms.clearAll((wasCleared) => { if (wasCleared) reset() })",
					"return chrome.alarms.clearAll()"
				],
				"negative": [
					"await chrome.alarms.clearAll()",
					"function stop() {\n  chrome.alarms.clearAll()\n}",
					"button.onclick = () => browser.alarms.clearAll();",
					"// if (await chrome.alarms.clearAll()) never runs on Firefox",
					"if (await chrome.alarms.clear('poll')) reset()"
				]
			}
		},
		{
			"id": "chrome-142-sidepanel-onclosed-event-reports-when-the-side",
			"revision": 1,
			"kind": "opportunity",
			"title": "chrome.sidePanel.onClosed event reports when the side panel closes",
			"browsers": {
				"chrome": {
					"version": "142",
					"date": "2025-10-28"
				}
			},
			"why": "Chrome 142 adds sidePanel.onClosed, fired whenever the extension side panel is closed or replaced. It reports windowId and path, plus tabId for tab-specific panels. Side panel extensions no longer have to infer closing indirectly.",
			"fix": "Use sidePanel.onClosed to release per-panel state or sync UI. Feature-detect chrome.sidePanel.onClosed so the extension still loads on Chrome 141 and older.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/sidePanel",
					"https://chromium.googlesource.com/chromium/src/+/f7bbb7bc33939a62ded71554c29a24ddda72b995",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=142"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-143-extensions-can-no-longer-open-devtools-windows",
			"revision": 1,
			"kind": "breaks",
			"title": "Extensions can no longer open DevTools windows or tabs through extension APIs",
			"browsers": {
				"chrome": {
					"version": "143",
					"date": "2025-12-02"
				}
			},
			"detect": {
				"code": [
					{
						"stringLiteral": "devtools://"
					},
					{
						"stringLiteral": "chrome-devtools://"
					}
				],
				"confidence": "medium"
			},
			"why": "Chrome 143 removes a carve-out that let extensions holding the devtools or debugger permission create a DevTools window or tab through the extension APIs that open tabs and windows. Chrome says opening DevTools should be left to the user, so those navigations are now blocked.",
			"fix": "Stop opening devtools:// pages programmatically. Ask the user to open DevTools, or build the feature as a devtools_page panel.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/2f952b7796483626f16d5995221b4dca8c3680c2",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=143"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"chrome.tabs.create({ url: 'devtools://devtools/bundled/inspector.html' })"
				],
				"negative": [
					"chrome.tabs.create({ url: 'https://developer.chrome.com/docs/devtools' })"
				]
			}
		},
		{
			"id": "chrome-144-browsingdata-password-removal-became-a-silent-no",
			"revision": 1,
			"kind": "behavior-change",
			"title": "browsingData password removal became a silent no-op",
			"browsers": {
				"chrome": {
					"version": "144",
					"date": "2026-01-13"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "browsingData.removePasswords"
					},
					{
						"regex": "\\bpasswords\\s*:\\s*true\\b"
					}
				],
				"confidence": "high"
			},
			"why": "Since Chrome 144, browsingData.removePasswords() and the passwords data type of browsingData.remove() do nothing: the call succeeds, the passwords are not deleted, and a console warning is logged. Extensions that promise to clear saved passwords no longer do so.",
			"fix": "Remove the passwords option from clear-data features and stop telling users that passwords are cleared; direct them to chrome://settings/passwords.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/browsingData",
					"https://chromium.googlesource.com/chromium/src/+/9fb257e9fbd293e1e760d9364e8e49362e7fa7b0",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=144"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"chrome.browsingData.removePasswords({ since: 0 })",
					"chrome.browsingData.remove({ since: 0 }, { cookies: true, passwords: true })"
				],
				"negative": [
					"chrome.browsingData.remove({ since: 0 }, { cookies: true, history: true })"
				]
			}
		},
		{
			"id": "chrome-144-mv3-webrequest-listeners-persist-across-restarts-and",
			"revision": 1,
			"kind": "behavior-change",
			"title": "MV3 webRequest listeners persist across restarts and wake the service worker",
			"browsers": {
				"chrome": {
					"version": "144",
					"date": "2026-01-13"
				}
			},
			"why": "From Chrome 144 the WebRequestPersistFilteredEvents behaviour is on by default: filtered webRequest listeners of service-worker extensions are persisted and restored at browser startup. A matching network request can then start the worker instead of being missed while it was not running. Later releases (see the Chrome 153 item on startup spin-up) rely on this.",
			"fix": "Register webRequest listeners synchronously at the top level of the service worker, keep the filter stable, and test cold-start request handling after a browser restart.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/2fdf5bdd379b2d68bbb3064e654e225546b61b15",
					"https://chromium.googlesource.com/chromium/src/+/49be6c38388bf89a2edf0a962eb78b5ffa4bbffc",
					"https://chromium.googlesource.com/chromium/src/+/e244cb63a45ee318e20d1473e4dcd9cd01900f11",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=144"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-145-declarativenetrequest-can-match-on-the-top-level",
			"revision": 1,
			"kind": "opportunity",
			"title": "declarativeNetRequest can match on the top-level site (topDomains, excludedTopDomains)",
			"browsers": {
				"chrome": {
					"version": "145",
					"date": "2026-02-10"
				}
			},
			"why": "Chrome 145 adds the topDomains and excludedTopDomains rule conditions, so a DNR rule can target requests by the domain of the top-level frame. It also adds the RuleConditionKeys enum for feature-detecting condition keys, and a topUrl field in TestMatchRequestDetails for testMatchOutcome(). Before, conditions could only look at the request initiator or the request URL.",
			"fix": "Replace per-site rule toggling or broad initiatorDomains approximations with topDomains / excludedTopDomains where you need page-scoped rules. Guard with minimum_chrome_version 145.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest",
					"https://chromium.googlesource.com/chromium/src/+/19402ff3a930f1a09ca027a06682614570ae10c1",
					"https://chromium.googlesource.com/chromium/src/+/ab42e9f18521b4c85e3424ecf35454604e9fd0e8",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=145"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-145-sidepanel-close-tabid-no-longer-falls-back",
			"revision": 1,
			"kind": "behavior-change",
			"title": "sidePanel.close({tabId}) no longer falls back to the global panel",
			"browsers": {
				"chrome": {
					"version": "145",
					"date": "2026-02-10"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "sidePanel\\s*\\??\\.\\s*close\\s*\\(\\s*\\{[^}]*\\btabId\\b"
					}
				],
				"confidence": "high"
			},
			"why": "In Chrome 145 sidePanel.close() with a tabId closes only a tab-specific panel for that tab. If just the global panel is open the returned promise rejects with an error, where earlier versions closed the global panel.",
			"fix": "Pass windowId to close a global panel, or catch the rejection when closing by tabId and retry with windowId.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/sidePanel",
					"https://chromium.googlesource.com/chromium/src/+/cf0517b3d0f7937886c1c4e9cdf3ea9f5c20d4fc",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=145"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"await chrome.sidePanel.close({ tabId: tab.id })"
				],
				"negative": [
					"await chrome.sidePanel.close({ windowId })",
					"await chrome.sidePanel.open({ tabId: tab.id })"
				]
			}
		},
		{
			"id": "chrome-147-tabs-update-now-throws-when-it-would",
			"revision": 1,
			"kind": "breaks",
			"title": "tabs.update now throws when it would unhighlight every tab in a window",
			"browsers": {
				"chrome": {
					"version": "147",
					"date": "2026-04-07"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "tabs\\s*\\??\\.\\s*update\\s*\\([^)]*\\bhighlighted\\s*:\\s*false"
					}
				],
				"confidence": "medium"
			},
			"why": "Chrome 147 changes tabs.update({highlighted: false}) so that trying to unselect the only highlighted tab (at least one tab per window must be highlighted) throws an error. Earlier versions silently ignored the request.",
			"fix": "Do not unhighlight the last highlighted tab; wrap the call in try/catch or check tab.highlighted first.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/ebae198bc22dadaa9a84c51d42f48a3d3b777bbd",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=147"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"await chrome.tabs.update(tab.id, { highlighted: false })"
				],
				"negative": [
					"await chrome.tabs.update(tab.id, { highlighted: true })",
					"await chrome.tabs.update(tab.id, { active: false })"
				]
			}
		},
		{
			"id": "chrome-148-browser-namespace-is-defined-in-webextension-polyfill",
			"revision": 1,
			"kind": "behavior-change",
			"title": "browser.* namespace is defined in Chrome 148; webextension-polyfill becomes a no-op",
			"browsers": {
				"chrome": {
					"version": "148",
					"date": "2026-05-05",
					"forecastEvent": "chrome-148-browser-namespace"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "typeof\\s+browser\\s*[!=]==?\\s*[\\'\"]undefined[\\'\"]"
					}
				],
				"confidence": "low"
			},
			"why": "From Chrome 148 all extension APIs are also available as browser.*, pointing at the same objects as chrome.*, in content scripts, service workers and offscreen documents. webextension-polyfill skips wrapping when browser already exists, so it does nothing on 148+. Code that treats a defined browser global as proof of Firefox will take its Firefox branch on Chrome. Extensions that declare a devtools_page only get browser from Chrome 152.",
			"fix": "Replace browser-sniffing of the form typeof browser !== \"undefined\" with feature detection, test polyfill-dependent code on 148+, and drop the polyfill once your users are on 148 (or set minimum_chrome_version to 148).",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w31/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w32/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w35/"
				],
				"sources": [
					"https://developer.chrome.com/docs/extensions/develop/concepts/browser-namespace",
					"https://developer.chrome.com/docs/extensions/whats-new",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=148"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"const isFirefox = typeof browser !== 'undefined'",
					"if (typeof browser === 'undefined') useChrome()"
				],
				"negative": [
					"const api = globalThis.chrome",
					"// typeof browser !== 'undefined'",
					"if (typeof browser === 'boolean') x()"
				]
			}
		},
		{
			"id": "chrome-148-opt-in-to-structured-clone-serialization-for",
			"revision": 1,
			"kind": "opportunity",
			"title": "Opt in to structured-clone serialization for extension messaging",
			"browsers": {
				"chrome": {
					"version": "148",
					"date": "2026-05-05",
					"forecastEvent": "w3c-structured-clone-messaging"
				}
			},
			"why": "Chrome 148 supports \"message_serialization\": \"structured_clone\" in manifest.json. Messages then use the structured clone algorithm instead of JSON, so Map, Set, BigInt, Date, Error, Blob and similar types survive runtime.sendMessage and ports. It is opt-in and extensions with different formats cannot message each other.",
			"fix": "Add the manifest key if you can require Chrome 148+, remove manual serialization workarounds, and mind the limits: SharedArrayBuffer is not supported, transferables are copied, native messaging stays JSON.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w31/"
				],
				"sources": [
					"https://developer.chrome.com/blog/structured-clone-messaging",
					"https://developer.chrome.com/docs/extensions/whats-new",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=148"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-148-runtime-onmessage-listeners-can-return-a-promise",
			"revision": 1,
			"kind": "opportunity",
			"title": "runtime.onMessage listeners can return a Promise",
			"browsers": {
				"chrome": {
					"version": "148",
					"date": "2026-05-05"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "onMessage\\s*\\??\\.\\s*addListener\\s*\\(\\s*async\\b"
					}
				],
				"confidence": "medium"
			},
			"why": "Chrome 148 lets runtime.onMessage listeners answer asynchronously by returning a Promise (or being async), with chrome.* and browser.* alike; return true plus sendResponse still works. It shipped together with the browser namespace because the polyfill used to provide this behaviour. It is not available in extensions that declare a devtools_page.",
			"fix": "Simplify async handlers or remove the polyfill, and review existing async onMessage listeners: their resolved value is now what the sender receives. Keep return true where you must support Chrome 147 and older.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/develop/concepts/browser-namespace",
					"https://chromium.googlesource.com/chromium/src/+/31bffd716e2bbcdf747aa35a0de011ba09caba24",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=148"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"chrome.runtime.onMessage.addListener(async (msg) => handle(msg))",
					"chrome.runtime.onMessage.addListener(async function (msg) { return handle(msg) })"
				],
				"negative": [
					"chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { handle(msg).then(sendResponse); return true })"
				]
			}
		},
		{
			"id": "chrome-149-dnr-redirect-rules-need-file-access-for",
			"revision": 1,
			"kind": "breaks",
			"title": "DNR redirect rules need file access for file: URLs",
			"browsers": {
				"chrome": {
					"version": "149",
					"date": "2026-06-02"
				}
			},
			"why": "From Chrome 149 declarativeNetRequest rules (redirect) only act on requests from file: URLs, or redirect a request to a file: URL, when the extension has been granted access to file URLs by the user. Without it the rules are skipped.",
			"fix": "Do not depend on DNR redirects involving file: URLs unless you tell users to enable \"Allow access to file URLs\" for the extension.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/53caa53855606e2a8bc96efc30bc040f6350c9eb",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=149"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-149-extensions-can-set-the-forbidden-origin-header",
			"revision": 1,
			"kind": "opportunity",
			"title": "Extensions can set the forbidden Origin header in fetch() (feature enabled)",
			"browsers": {
				"chrome": {
					"version": "149",
					"date": "2026-06-02"
				}
			},
			"why": "Chrome 149 enables BypassRequestForbiddenHeadersCheck in the network and Blink layers. Extensions (and CDP) that are registered in the origin access list may set the Origin request header from fetch() if they choose; other fetch behaviour is unchanged. This can replace header-rewriting rules that existed only to spoof Origin.",
			"fix": "If you use declarativeNetRequest modifyHeaders or webRequest only to set Origin on your own requests, test setting it directly in fetch(), with host permissions for the target.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/ed7bebf1ba4b290d758045529db1814b6b0941a5",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=149"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-149-runtime-getplatforminfo-nacl-arch-is-deprecated",
			"revision": 1,
			"kind": "deprecated",
			"title": "runtime.getPlatformInfo().nacl_arch is deprecated",
			"browsers": {
				"chrome": {
					"version": "149",
					"date": "2026-06-02",
					"forecastEvent": "w3c-runtime-getplatforminfo-nacl-arch-deprecation"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "\\.\\s*nacl_arch\\b"
					},
					{
						"apiCall": "PlatformNaclArch.*"
					}
				],
				"confidence": "high"
			},
			"why": "Chrome 149 documents PlatformNaclArch and the nacl_arch field of runtime.getPlatformInfo() as deprecated because Native Client was removed in Chrome 139. The field is still returned; the WECG is separately proposing to retire it.",
			"fix": "Stop reading nacl_arch and use arch instead.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w34/"
				],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/runtime",
					"https://chromium.googlesource.com/chromium/src/+/6f14e2ad9b7b5ea0b061e27feca49f08a57e2272",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=149"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"const { nacl_arch } = info; send(info.nacl_arch)"
				],
				"negative": [
					"send(info.arch)"
				]
			}
		},
		{
			"id": "chrome-150-alarms-create-gets-persistacrosssessions-to-control-alarm",
			"revision": 1,
			"kind": "opportunity",
			"title": "alarms.create gets persistAcrossSessions to control alarm persistence",
			"browsers": {
				"chrome": {
					"version": "150",
					"date": "2026-06-30"
				}
			},
			"why": "Chrome 150 adds the persistAcrossSessions flag to alarms. true (the default) persists the alarm until the extension updates; false clears it when the extension reloads or the browser restarts. Earlier versions behaved unpredictably, and other browsers do not support the flag yet.",
			"fix": "Set persistAcrossSessions explicitly where behaviour matters, and keep re-creating important alarms at service worker startup for older Chrome and other browsers.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/alarms",
					"https://chromium.googlesource.com/chromium/src/+/900c2b00c42c377b93d3009372fa4f7f7d7e10cc",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=150"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-150-alarms-create-throws-a-typeerror-for-alarm",
			"revision": 1,
			"kind": "breaks",
			"title": "alarms.create() throws a TypeError for alarm names over 1024 bytes",
			"browsers": {
				"chrome": {
					"version": "150",
					"date": "2026-06-30"
				}
			},
			"why": "Starting in Chrome 150, chrome.alarms.create() enforces a 1024-byte limit on alarm names; a longer name throws a TypeError. Chrome 148 only logged a warning. A Finch kill-switch exists but should not be relied on.",
			"fix": "Keep alarm names short (use IDs or hashes) and store extra data elsewhere.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/whats-new",
					"https://chromium.googlesource.com/chromium/src/+/c0032deced7154bddd385a90cf094de5cb0b99a9",
					"https://chromium.googlesource.com/chromium/src/+/432c30e3e1dcd03cecb869a1e23162d8ea1b9af2",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=150"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-150-contextmenus-supports-the-tab-context-for-the",
			"revision": 1,
			"kind": "opportunity",
			"title": "contextMenus supports the \"tab\" context for the tab strip menu",
			"browsers": {
				"chrome": {
					"version": "150",
					"date": "2026-06-30",
					"forecastEvent": "w3c-tab-context-menu-cross-browser"
				}
			},
			"why": "Starting in Chrome 150, chrome.contextMenus accepts the \"tab\" context, adding items to the tab strip right-click menu. Items receive all selected tabs when several are selected. The \"all\" context does not include \"tab\".",
			"fix": "Add contexts: [\"tab\"] where you want tab-strip actions, and check info and tab arguments for multi-selection. Feature-detect for older Chrome.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w34/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://developer.chrome.com/docs/extensions/whats-new",
					"https://chromium.googlesource.com/chromium/src/+/ad49992f531ef58f890aaf8d0cdef30e79d58427",
					"https://chromium.googlesource.com/chromium/src/+/04fa747f791581842efe65d5f86d51fe0ba83724",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=150"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-150-offscreen-hasdocument-reports-whether-an-offscreen-document",
			"revision": 1,
			"kind": "opportunity",
			"title": "offscreen.hasDocument() reports whether an offscreen document exists",
			"browsers": {
				"chrome": {
					"version": "150",
					"date": "2026-06-30"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "getContexts\\s*\\(\\s*\\{[^}]*OFFSCREEN_DOCUMENT"
					}
				],
				"confidence": "high"
			},
			"why": "Chrome 150 documents chrome.offscreen.hasDocument(), which resolves to a boolean telling whether the extension has an active offscreen document. It avoids ad hoc checks before calling createDocument().",
			"fix": "Call hasDocument() before createDocument() instead of try/catch or context lookups, and guard for Chrome < 150.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/offscreen",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=150"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"const contexts = await chrome.runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'] })",
					"await chrome.runtime.getContexts({ contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT] })"
				],
				"negative": [
					"await chrome.runtime.getContexts({ contextTypes: ['SIDE_PANEL'] })"
				]
			}
		},
		{
			"id": "chrome-150-pagecapture-saveasmhtml-respects-block-extensions-on-this",
			"revision": 1,
			"kind": "behavior-change",
			"title": "pageCapture.saveAsMHTML respects \"block extensions on this site\"",
			"browsers": {
				"chrome": {
					"version": "150",
					"date": "2026-06-30"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "pageCapture.saveAsMHTML"
					}
				],
				"confidence": "medium"
			},
			"why": "Chrome 150 fixes pageCapture so it checks whether the user blocked extension access to the page. Extensions that captured MHTML of such pages, with or without host permissions, now get no capture. Chrome 156 also validates subframes of the capture.",
			"fix": "Handle a failed saveAsMHTML on restricted sites and tell users when a page is blocked.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/c0e115725023ce4352650e22ed2c25baab0d1cfc",
					"https://chromium-review.googlesource.com/c/chromium/src/+/8417682",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=150"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"const blob = await chrome.pageCapture.saveAsMHTML({ tabId })"
				],
				"negative": [
					"const shot = await chrome.tabs.captureVisibleTab()"
				]
			}
		},
		{
			"id": "chrome-151-devtools-inspectedwindow-eval-and-devtools-network-gethar",
			"revision": 1,
			"kind": "opportunity",
			"title": "devtools.inspectedWindow.eval and devtools.network.getHAR return promises",
			"browsers": {
				"chrome": {
					"version": "151",
					"date": "2026-07-28"
				}
			},
			"why": "Starting in Chrome 151, chrome.devtools.inspectedWindow.eval(), inspectedWindow.getResources() and chrome.devtools.network.getHAR() (plus Resource/Request.getContent()) return promises. Callbacks keep working.",
			"fix": "Use async/await in DevTools pages and drop callback wrappers when the minimum Chrome version allows.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/devtools/inspectedWindow",
					"https://developer.chrome.com/docs/extensions/reference/api/devtools/network",
					"https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7802274",
					"https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7802275",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=151"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-151-dnr-session-rules-stop-eating-the-global",
			"revision": 1,
			"kind": "behavior-change",
			"title": "DNR session rules stop eating the global static rule quota",
			"browsers": {
				"chrome": {
					"version": "151",
					"date": "2026-07-28"
				}
			},
			"why": "Chrome 151 excludes session rulesets when computing rule counts, so session rules no longer reduce getAvailableStaticRuleCount() or the shared global static rule budget. The number returned grows for extensions that keep many session rules.",
			"fix": "Do not hard-code assumptions about available static rules based on session rule usage; re-read getAvailableStaticRuleCount() when enabling rulesets.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/b2e95e6317f67c5e553bda44236397541e279888",
					"https://developer.chrome.com/docs/extensions/reference/api/declarativeNetRequest",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=151"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-151-mimehandler-lets-an-extension-handle-pdfs-and",
			"revision": 1,
			"kind": "opportunity",
			"title": "chrome.mimeHandler lets an extension handle PDFs and other MIME streams natively",
			"browsers": {
				"chrome": {
					"version": "151",
					"date": "2026-07-28"
				}
			},
			"why": "Chrome 151 adds the chrome.mimeHandler API and the \"mime_types_handler\" manifest key. An extension (for example a PDF viewer) can register for a MIME type and keeps the original URL in the address bar, reuses the response Chrome already fetched, works for POST responses and embedded documents, and handles file: URLs without the file-access toggle. As of Chrome 151 only application/pdf is available to public handlers.",
			"fix": "Document viewers that redirect navigations with webRequest or declarativeNetRequest should evaluate mime_types_handler. Feature-detect the API, since earlier versions ignore the key.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/mimeHandler",
					"https://developer.chrome.com/docs/extensions/reference/api",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=151"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-151-userscripts-execute-rejects-invalid-file-sources-immediately",
			"revision": 1,
			"kind": "behavior-change",
			"title": "userScripts.execute() rejects invalid file sources immediately instead of hanging",
			"browsers": {
				"chrome": {
					"version": "151",
					"date": "2026-07-28"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "userScripts.execute"
					}
				],
				"confidence": "medium"
			},
			"why": "Starting in Chrome 151, chrome.userScripts.execute() returns an error right away when its file sources fail validation (duplicate files, empty paths, files with an invalid MIME type). Before, the failed check was ignored and the returned promise never settled. The vendor \"What's new\" note claiming synchronous syntax validation with diagnostics in Chrome 149 is not reflected in the API reference or in any Chromium change found.",
			"fix": "Handle rejections from userScripts.execute(), add timeouts only for real injection waits, and validate file lists before calling.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium-review.googlesource.com/c/chromium/src/+/7929886",
					"https://developer.chrome.com/docs/extensions/whats-new",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=151"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"await chrome.userScripts.execute({ target: { tabId }, js: [{ file: 'script.js' }] })"
				],
				"negative": [
					"await chrome.userScripts.register([{ id: 'a', matches: ['<all_urls>'], js: [{ code: 'x()' }] }])"
				]
			}
		},
		{
			"id": "chrome-152-action-setbadgetext-rejects-text-longer-than-100",
			"revision": 1,
			"kind": "breaks",
			"title": "action.setBadgeText rejects text longer than 100 bytes",
			"browsers": {
				"chrome": {
					"version": "152",
					"date": "2026-08-25",
					"forecastEvent": "chrome-badge-text-limit-chrome152"
				}
			},
			"why": "Chrome 152 limits the text passed to action.setBadgeText() (and browserAction.setBadgeText) to 100 bytes. Longer strings return the error \"Badge text size is N bytes which exceeds the limit of 100 bytes.\" instead of being set. The switch was enabled by default and later removed.",
			"fix": "Keep badge text to a few characters and truncate before calling; catch the rejection.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w31/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w35/"
				],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/8170d1b52c226e2550811aefdca87ec35ba496e9",
					"https://chromium.googlesource.com/chromium/src/+/eae37c51bcc2b29b791c65c5ca24bb34a6864bd4",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=152"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-152-alarms-create-accepts-name-inside-the-alarm",
			"revision": 1,
			"kind": "opportunity",
			"title": "alarms.create accepts name inside the alarm info object",
			"browsers": {
				"chrome": {
					"version": "152",
					"date": "2026-08-25",
					"forecastEvent": "w3c-alarms-create-name-proposal"
				}
			},
			"why": "Chrome 152 adds a name property to AlarmCreateInfo, so alarms.create can take the alarm name in its options object, as agreed in the WebExtensions Community Group. The older name-as-first-argument form still exists.",
			"fix": "Use {name} in the info object for code shared with other browsers, and keep the old form for Chrome < 152.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w31/"
				],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/alarms",
					"https://chromium.googlesource.com/chromium/src/+/d458c841b34f246ebaeb05ed7a6687ba5d0028e6",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=152"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-152-browsingdata-appcache-and-websql-data-types-are",
			"revision": 1,
			"kind": "deprecated",
			"title": "browsingData appcache and WebSQL data types are deprecated",
			"browsers": {
				"chrome": {
					"version": "152",
					"date": "2026-08-25"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "browsingData.removeAppcache"
					},
					{
						"apiCall": "browsingData.removeWebSQL"
					},
					{
						"regex": "\\b(?:appcache|webSQL)\\s*:\\s*true\\b"
					}
				],
				"confidence": "high"
			},
			"why": "Chrome 152 marks browsingData.removeAppcache(), removeWebSQL() and the appcache and webSQL data types as deprecated because the underlying web platform features are gone. The commit states there is no behaviour change.",
			"fix": "Remove these data types from clear-data options.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/browsingData",
					"https://chromium.googlesource.com/chromium/src/+/5a8eba591742c544b720162930bfca924b44ebbc",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=152"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"chrome.browsingData.removeWebSQL({ since: 0 })",
					"chrome.browsingData.remove({ since: 0 }, { appcache: true, cookies: true })"
				],
				"negative": [
					"chrome.browsingData.remove({ since: 0 }, { cache: true, cookies: true })"
				]
			}
		},
		{
			"id": "chrome-153-content-scripts-webrequest-and-dnr-no-longer",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Content scripts, webRequest and DNR no longer see privileged Chrome content",
			"browsers": {
				"chrome": {
					"version": "153",
					"date": "2026-09-08",
					"forecastEvent": "chrome-153-content-script-privileged-block"
				}
			},
			"why": "Chrome 153 stops delivering extension content scripts to privileged renderer processes (privileged WebContents), and hides their network requests from webRequest and declarativeNetRequest, including their own top-level navigation. Extensions can no longer observe, block or redirect that content.",
			"fix": "Do not rely on injecting into or intercepting Chrome-owned privileged pages; no manifest change needed.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w33/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w35/"
				],
				"sources": [
					"https://chromium-review.googlesource.com/c/chromium/src/+/8201204",
					"https://chromium.googlesource.com/chromium/src/+/10125b474fa6414ff7787400c5cfb12720dd13af",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=153"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-153-cookies-now-respects-user-blocked-sites-and",
			"revision": 1,
			"kind": "breaks",
			"title": "chrome.cookies now respects user-blocked sites and withheld host permissions",
			"browsers": {
				"chrome": {
					"version": "153",
					"date": "2026-09-08",
					"forecastEvent": "chrome-153-cookies-user-site-restrictions"
				}
			},
			"why": "Chrome 153 checks full page access, not just manifest host permissions, in the cookies API. When a user turns off extensions for a site or withholds host permissions, cookies.get, set and remove no longer work for that domain, which they did before.",
			"fix": "Handle cookie API failures for restricted sites; request host access at runtime (permissions API) and show a message when the user has blocked the site.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w34/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/6813284e59b397be04f5aadd5f4b9ffe87edddff",
					"https://chromium-review.googlesource.com/c/chromium/src/+/8259390",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=153"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-153-match-patterns-handle-non-ascii-utf-8",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Match patterns handle non-ASCII (UTF-8) paths correctly",
			"browsers": {
				"chrome": {
					"version": "153",
					"date": "2026-09-08",
					"forecastEvent": "chrome-153-urlpattern-utf8"
				}
			},
			"why": "Chrome 153 fixes URLPattern path matching for UTF-8 characters, including \"/*\" variants, invalid UTF-8 and case-insensitive matching. The commit notes it also fixes UTF-8 paths for web_accessible_resources. Patterns with non-ASCII paths may start matching, or stop matching, URLs that matched wrongly before.",
			"fix": "Re-test content_scripts, host_permissions and web_accessible_resources entries that contain non-ASCII characters.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w34/"
				],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/6803303fc22df001114e987cb0e0d86c7b421c3d",
					"https://chromium-review.googlesource.com/c/chromium/src/+/8258871",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=153"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-153-mv3-alarms-can-fire-as-often-as",
			"revision": 1,
			"kind": "opportunity",
			"title": "MV3 alarms can fire as often as every second",
			"browsers": {
				"chrome": {
					"version": "153",
					"date": "2026-09-08"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "\\b(?:periodInMinutes|delayInMinutes)\\s*:\\s*0?\\.[0-4]"
					}
				],
				"confidence": "medium"
			},
			"why": "Chrome 153 lowers the minimum alarm granularity of packed MV3 extensions from 30 seconds to 1 second, reasoning that the service worker stays alive 30 seconds anyway and alarms are cheaper than offscreen-document timer workarounds. The alarms API page still says 30 seconds.",
			"fix": "If you run an offscreen document only to drive a short setInterval, evaluate alarms with short periods; keep a fallback for Chrome < 153.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/45a017bab307d72ee8596cacd38d9ecb2daeb674",
					"https://developer.chrome.com/docs/extensions/reference/api/alarms",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=153"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"chrome.alarms.create('tick', { periodInMinutes: 0.1 })",
					"chrome.alarms.create('soon', { delayInMinutes: .25 })"
				],
				"negative": [
					"chrome.alarms.create('tick', { periodInMinutes: 0.5 })",
					"chrome.alarms.create('tick', { periodInMinutes: 1 })"
				]
			}
		},
		{
			"id": "chrome-153-protocol-handlers-registered-by-extensions-are-removed",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Protocol handlers registered by extensions are removed with the extension",
			"browsers": {
				"chrome": {
					"version": "153",
					"date": "2026-09-08",
					"forecastEvent": "chrome-153-protocol-handler-extension-binding"
				}
			},
			"why": "Chrome 153 ties protocol handlers registered from extension frames to the registering extension, so disabling or uninstalling the extension removes them. Before, a handler registered with elevated extension privileges could outlive its owner.",
			"fix": "Re-register handlers if the extension is re-enabled and do not depend on handlers surviving uninstall.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w34/"
				],
				"sources": [
					"https://chromium-review.googlesource.com/c/chromium/src/+/8259429",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=153"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-153-publicsuffix-api-query-the-browser-public-suffix",
			"revision": 1,
			"kind": "opportunity",
			"title": "chrome.publicSuffix API: query the browser Public Suffix List",
			"browsers": {
				"chrome": {
					"version": "153",
					"date": "2026-09-08",
					"forecastEvent": "chrome-154-publicsuffix-api"
				},
				"firefox": {
					"version": "153",
					"date": "2026-07-21",
					"forecastEvent": "firefox-153-new-apis"
				}
			},
			"detect": {
				"code": [
					{
						"stringLiteral": "publicsuffix.org",
						"caseInsensitive": true
					},
					{
						"stringLiteral": "public_suffix_list"
					}
				],
				"confidence": "medium"
			},
			"why": "Chrome 153 adds chrome.publicSuffix (permission \"publicSuffix\") with getDomain(), getKnownSuffix() and isKnownSuffix() to look up the registrable domain or public suffix of a hostname from the browser Public Suffix List. Extensions that bundle their own copy of the list can drop it. Chrome 157 makes hostnames with a leading dot invalid (separate item).",
			"fix": "Add the publicSuffix permission, replace bundled PSL code with the API, validate inputs first, and keep a fallback for Chrome < 153.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w35/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w30/"
				],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/publicSuffix",
					"https://developer.chrome.com/docs/extensions/whats-new",
					"https://chromium.googlesource.com/chromium/src/+/52aa1573bb40a536a1d2d6ed3f9bc7c8c817fb2d",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=153",
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/153#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/07/23/firefox-153-webextensions-api-updates/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1315558"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"const PSL_URL = 'https://publicsuffix.org/list/public_suffix_list.dat'"
				],
				"negative": [
					"// rules come from publicsuffix.org",
					"const domain = chrome.publicSuffix.getDomain(host)"
				]
			}
		},
		{
			"id": "chrome-153-webrequest-extensions-no-longer-start-their-service",
			"revision": 1,
			"kind": "behavior-change",
			"title": "webRequest extensions no longer start their service worker eagerly at launch",
			"browsers": {
				"chrome": {
					"version": "153",
					"date": "2026-09-08"
				}
			},
			"why": "Chrome 153 removes the startup spin-up of service-worker extensions that hold the webRequest permission. Because filtered listeners are persisted, those workers stay dormant after a browser restart and wake up on demand when a matching request occurs.",
			"fix": "Do not assume the service worker is running at browser start; initialize state lazily and register all listeners at the top level of the worker.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/a3dcf5bbc749299346fd5cf7cf94e43bcc90f39a",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=153"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-154-error-pages-can-no-longer-carry-the",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Error pages can no longer carry the failed URL's extension permissions",
			"browsers": {
				"chrome": {
					"version": "154",
					"date": "2026-09-22",
					"forecastEvent": "chrome-error-document-permission-spoof-fix"
				}
			},
			"why": "Chrome 154 ignores error documents (chrome-error://chromewebdata/) in extension permission checks. They cannot claim host permissions, receive activeTab grants, connect to externally_connectable extensions or spoof declarativeContent identities for the failed URL. Chrome 151 already blocked externally_connectable messaging from error pages.",
			"fix": "Do not rely on extension features inside error pages; handle failed connections from web pages that failed to load.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/ac5658b6d7bb7ca9417f636dc83b09d226a0a2ec",
					"https://chromium-review.googlesource.com/c/chromium/src/+/8246242",
					"https://chromium.googlesource.com/chromium/src/+/71365f8920c4b1af93544ed85929e3fecf058c44",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=154"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-154-manifest-csp-validation-rejects-control-characters",
			"revision": 1,
			"kind": "breaks",
			"title": "Manifest CSP validation rejects control characters",
			"browsers": {
				"chrome": {
					"version": "154",
					"date": "2026-09-22"
				}
			},
			"why": "Chrome 154 makes the manifest CSP check reject content_security_policy strings (extension_pages and sandbox) that contain non-printable characters such as DEL or vertical tab, so the extension fails to load with an invalid manifest key error. Earlier validation accepted them while the runtime parser dropped directives, which could be used to bypass sandbox directives.",
			"fix": "Make sure content_security_policy strings contain only printable ASCII and normal whitespace.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/ed31df3a218ec75a247e7f82233b3157741442da",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=154"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-154-own-page-tab-metadata-is-readable-without",
			"revision": 1,
			"kind": "opportunity",
			"title": "Own-page tab metadata is readable without the \"tabs\" permission",
			"browsers": {
				"chrome": {
					"version": "154",
					"date": "2026-09-22",
					"forecastEvent": "w3c-own-tab-metadata-parity"
				}
			},
			"detect": {
				"manifest": {
					"permissionsAny": [
						"tabs"
					]
				},
				"code": [
					{
						"regex": "tabs\\s*\\??\\.\\s*query\\s*\\(\\s*\\{[^}]*\\burl\\s*:\\s*[\\w$.]*getURL\\s*\\("
					}
				],
				"confidence": "medium"
			},
			"why": "Chrome 154 stops scrubbing url, title and favIconUrl of tabs that show the extension's own pages, so tabs.query can find the extension's own tab without the tabs permission. The tabs permission triggers a \"Read your browsing history\" warning.",
			"fix": "If tabs is requested only to find or dedupe your own options/popup pages, remove it and bump minimum_chrome_version to 154; keep it for other browsers that still scrub.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://chromium-review.googlesource.com/c/chromium/src/+/8253459",
					"https://chromium.googlesource.com/chromium/src/+/f1284f663557d4c224c6afac6a56710d86d0a15f",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=154"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"const [tab] = await chrome.tabs.query({ url: chrome.runtime.getURL('inbox.html') })"
				],
				"negative": [
					"const [tab] = await chrome.tabs.query({ url: 'https://mail.example.com/*' })"
				]
			}
		},
		{
			"id": "chrome-155-onmessage-error-responses-no-longer-start-with",
			"revision": 1,
			"kind": "behavior-change",
			"title": "onMessage error responses no longer start with \"Uncaught Error: \"",
			"browsers": {
				"chrome": {
					"version": "155",
					"date": "2026-10-06"
				}
			},
			"detect": {
				"code": [
					{
						"stringLiteral": "Uncaught Error"
					}
				],
				"confidence": "high"
			},
			"why": "Chrome 155 changes the error text delivered to a sender when a runtime.onMessage listener throws or returns a rejected promise: only the Error's message is sent, for example \"test reject\", instead of \"Uncaught Error: test reject\".",
			"fix": "Stop matching on the \"Uncaught Error: \" prefix; compare error.message values or send structured error objects.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/810db4689e8729d4569efd1985429f921763f0c3",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=155"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"if (err.message.startsWith('Uncaught Error: ')) retry()"
				],
				"negative": [
					"// Chrome used to prefix Uncaught Error",
					"if (err.message === 'timeout') retry()"
				]
			}
		},
		{
			"id": "chrome-155-runtime-onenabled-fires-when-your-extension-is",
			"revision": 1,
			"kind": "opportunity",
			"title": "runtime.onEnabled fires when your extension is re-enabled",
			"browsers": {
				"chrome": {
					"version": "155",
					"date": "2026-10-06",
					"forecastEvent": "chrome-155-runtime-onenabled"
				}
			},
			"why": "Chrome 155 adds chrome.runtime.onEnabled, fired when the extension goes from disabled to enabled while the browser runs. Until now install/startup events did not cover a user turning the extension back on.",
			"fix": "Re-initialize native host handshakes, license checks or sync in an onEnabled listener, guarded with a feature check for Chrome < 155.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/runtime",
					"https://chromium.googlesource.com/chromium/src/+/61c52ec43b0aaa5d1fea4fa6409cc41166acc75e",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=155"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-155-tabs-createsplit-tabs-unsplit-and-tabs-create",
			"revision": 1,
			"kind": "opportunity",
			"title": "tabs.createSplit, tabs.unsplit and tabs.create({splitWithTabId}) manage split view",
			"browsers": {
				"chrome": {
					"version": "155",
					"date": "2026-10-06",
					"forecastEvent": "chrome-153-tabs-split-api"
				}
			},
			"why": "Chrome 155 documents chrome.tabs.createSplit(tabIds) and chrome.tabs.unsplit(splitViewId), plus a splitWithTabId option for tabs.create to open a tab in a split view with an existing one. The feature flag kApiTabsSplitView is on by default at tag 155. Earlier versions only exposed splitViewId on Tab.",
			"fix": "Feature-detect the new methods before using them; tab managers can now arrange split views.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w34/"
				],
				"sources": [
					"https://developer.chrome.com/docs/extensions/reference/api/tabs",
					"https://chromium.googlesource.com/chromium/src/+/ccf3a2e65da421f0f17ef8a6a3c3a7f10138579f",
					"https://chromium.googlesource.com/chromium/src/+/3494ec50059d93ab606f601ced4afedbf60e52e9",
					"https://chromium.googlesource.com/chromium/src/+/250120b3d1717388e0981349f4f6c6adbb63b7ce",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=155"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-156-dnr-modifyheaders-cannot-change-the-response-headers",
			"revision": 1,
			"kind": "behavior-change",
			"title": "DNR modifyHeaders cannot change the response headers Chrome hides from extensions",
			"browsers": {
				"chrome": {
					"version": "156",
					"date": "2026-10-20",
					"forecastEvent": "chrome-156-dnr-hidden-response-headers"
				}
			},
			"detect": {
				"code": [
					{
						"stringLiteral": "X-Chrome-ID-Consistency-Response",
						"caseInsensitive": true
					}
				],
				"confidence": "high"
			},
			"why": "Chrome 156 makes declarativeNetRequest modifyHeaders follow the same hidden-header rule as webRequest: response-header modifications that target a header Chrome hides from extensions are dropped, while the rule's other modifications still apply. In Chrome 156 the hidden set is only the X-Chrome-ID-Consistency-Response (Dice) header on the Google accounts host; Chrome 157 adds X-Chrome-Manage-Accounts.",
			"fix": "Do not rely on DNR to inject, overwrite or remove Google sign-in headers on accounts.google.com; other response-header rules are unaffected.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/ca8d9897639700c01a3983f85b926a6fe25dff7c",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=156"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"{ \"header\": \"x-chrome-id-consistency-response\", \"operation\": \"remove\" }"
				],
				"negative": [
					"const h = 'X-Chrome-Manage-Accounts-Extra'"
				]
			}
		},
		{
			"id": "chrome-156-tabcapture-no-longer-captures-sites-blocked-by",
			"revision": 1,
			"kind": "behavior-change",
			"title": "tabCapture no longer captures sites blocked by the user or policy",
			"browsers": {
				"chrome": {
					"version": "156",
					"date": "2026-10-20"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "tabCapture.capture"
					},
					{
						"apiCall": "tabCapture.getMediaStreamId"
					}
				],
				"confidence": "medium"
			},
			"why": "Chrome 156 adds checks so chrome.tabCapture refuses tabs whose URL is blocked by enterprise policy or the user's site restrictions (and file: URLs without access) and disconnects an active capture when navigation reaches such a site.",
			"fix": "Handle tabCapture failures and mid-capture disconnects for restricted sites.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/473b176852a36fcf347a14123b61493337ea4634",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=156"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"const id = await chrome.tabCapture.getMediaStreamId({ targetTabId: tab.id })"
				],
				"negative": [
					"const stream = await navigator.mediaDevices.getDisplayMedia()"
				]
			}
		},
		{
			"id": "chrome-156-url-scoped-events-respect-user-and-policy",
			"revision": 1,
			"kind": "behavior-change",
			"title": "URL-scoped events respect user and policy blocked sites",
			"browsers": {
				"chrome": {
					"version": "156",
					"date": "2026-10-20"
				}
			},
			"why": "Chrome 156 applies runtime host restrictions to events scoped to a URL, such as cookies.onChanged. Extensions with broad host permissions stop receiving those events, including HttpOnly cookie values, for hosts blocked by policy or by the user.",
			"fix": "Do not assume cookies.onChanged covers every site; reconcile state after the user allows the site.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/131714ab59d24d7206f34fef918d94363df696b2",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=156"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-156-webrequest-redirects-to-file-urls-need-file",
			"revision": 1,
			"kind": "breaks",
			"title": "webRequest redirects to file: URLs need file access",
			"browsers": {
				"chrome": {
					"version": "156",
					"date": "2026-10-20"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "\\bredirectUrl\\s*:\\s*[\\'\"`]file:"
					}
				],
				"confidence": "high"
			},
			"why": "Chrome 156 rejects webRequest redirects to a file:// URL unless the user granted the extension access to file URLs. The redirect is refused and, if applicable, an extension with lower precedence may handle the request.",
			"fix": "Do not redirect to file:// without the file-access permission; check and tell users to enable it.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/fb1c4a8a3eb9e10fa8cf4cb2113831c0ad9db429",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=156"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"return { redirectUrl: 'file:///Users/me/blocked.html' }"
				],
				"negative": [
					"return { redirectUrl: chrome.runtime.getURL('blocked.html') }"
				]
			}
		},
		{
			"id": "chrome-157-navigator-clipboard-writetext-works-in-extension-service",
			"revision": 1,
			"kind": "opportunity",
			"title": "navigator.clipboard.writeText works in extension service workers",
			"browsers": {
				"chrome": {
					"version": "157",
					"date": "2026-11-03"
				}
			},
			"why": "Chrome 157 enables navigator.clipboard in extension service workers: a worker holding clipboardWrite can write to the clipboard, and revoking the permission applies to a running worker. Reads stay unsupported. This can replace an offscreen document created only for clipboard writes.",
			"fix": "Test clipboard writes from the service worker and drop the offscreen document if it exists only for writing.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/1387a35486c0894e07c9d478068f622339564ef1",
					"https://chromium.googlesource.com/chromium/src/+/6eb0b84e3359968e9c054cb5f3a5133dd699ccac",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=157"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-157-protocol-handlers-manifest-key-moves-to-the",
			"revision": 1,
			"kind": "opportunity",
			"title": "protocol_handlers manifest key moves to the stable channel",
			"browsers": {
				"chrome": {
					"version": "157",
					"date": "2026-11-03"
				}
			},
			"why": "Chrome 157 enables ExtensionProtocolHandlers by default and changes the MV3 protocol_handlers manifest feature from dev to stable, so extensions can declare custom protocol handlers, including http(s) endpoints such as web mail or search.",
			"fix": "Evaluate declarative protocol_handlers instead of runtime registration, and restrict handler URLs to your own resources or http(s).",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/b47bdaeb9682aaf58c58e2763e0f962696e7517f",
					"https://chromium.googlesource.com/chromium/src/+/f9e0841db397a09d204f246b7e78d807ceda9e4d",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=157"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-157-runtime-onextensionloaded-reports-why-your-extension-became",
			"revision": 1,
			"kind": "opportunity",
			"title": "runtime.onExtensionLoaded reports why your extension became active",
			"browsers": {
				"chrome": {
					"version": "157",
					"date": "2026-11-03"
				}
			},
			"why": "Chrome 157 adds chrome.runtime.onExtensionLoaded, fired when the extension is added to the active set with a reason (install, update, browser_update, enable, startup, reload) and previousVersion on update. It unifies onInstalled, onStartup and the new onEnabled.",
			"fix": "Use it for a single initialization hook, feature-detected; the reference page lists it as pending.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/4f1b414d2d6a4434e8b5a35c34101024440b97bc",
					"https://developer.chrome.com/docs/extensions/reference/api/runtime",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=157"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-158-blob-and-filesystem-urls-are-rejected-as",
			"revision": 1,
			"kind": "breaks",
			"title": "blob: and filesystem: URLs are rejected as extension popups",
			"browsers": {
				"chrome": {
					"version": "158",
					"date": "2026-11-17"
				}
			},
			"why": "Scheduled for Chrome 158 (code merged 2026-10-14, before the 2026-10-26 branch point): action popup URLs must use the chrome-extension scheme. blob: and filesystem: URLs are rejected both in default_popup and in action.setPopup.",
			"fix": "Use a normal extension page for the popup instead of a blob URL.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/7811f3b45845a8ac68aff0d50d34edebd719af01",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=158"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-158-dnr-response-header-value-matching-needs-host",
			"revision": 1,
			"kind": "breaks",
			"title": "DNR response-header value matching needs host permissions",
			"browsers": {
				"chrome": {
					"version": "158",
					"date": "2026-11-17"
				}
			},
			"why": "Scheduled for Chrome 158: DNR rules that match response header values (values/excludedValues in responseHeaders or excludedResponseHeaders) only work without host permissions for CORS-safelisted headers and Content-Encoding. Other rules are skipped for requests the extension has no host access to. Hidden headers are treated as absent in such conditions. The feature flag is enabled by default.",
			"fix": "Request host permissions for sites whose response header values you match, or match only on header presence.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/22a2af917b63108976004544d45feb1c0053aca2",
					"https://chromium.googlesource.com/chromium/src/+/b878de30c0996d9fcabba2648e99502416a0d841",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=158"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-158-web-accessible-resources-no-longer-exposes-favicon",
			"revision": 1,
			"kind": "behavior-change",
			"title": "web_accessible_resources \"*\" no longer exposes _favicon and generated pages",
			"browsers": {
				"chrome": {
					"version": "158",
					"date": "2026-11-17"
				}
			},
			"why": "Scheduled for Chrome 158: generated paths such as _favicon and the generated background page are no longer matched by blanket wildcard web_accessible_resources entries, so web origins and content scripts can only load them if the extension declares them explicitly, for example \"_favicon/*\".",
			"fix": "List \"_favicon/*\" in web_accessible_resources if content scripts or pages fetch favicons; stop using \"*\" wildcards.",
			"links": {
				"pulse": [],
				"sources": [
					"https://chromium.googlesource.com/chromium/src/+/fad4d6c9a5528e778e0bdda19b82ff74240e534e",
					"https://chromium.googlesource.com/chromium/src/+/5095ebef6615f79ca5a4b4eb7044d3203ed2f0c0",
					"https://chromiumdash.appspot.com/fetch_milestone_schedule?mstone=158"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-na-extensions-on-googlebook-os-getplatforminfo-says-android",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Extensions on Googlebook OS: getPlatformInfo says android, nativeMessaging fails",
			"browsers": {
				"chrome": {}
			},
			"why": "Chrome extensions run on Googlebook OS (Android stack with ChromeOS foundations) but runtime.getPlatformInfo().os returns \"android\" on a laptop, native messaging connections fail, commands read the linux key bindings, and ChromeOS-only APIs are undefined. Platform checks and native host flows can misfire.",
			"fix": "Use feature detection; combine getPlatformInfo with navigator.userAgentData.mobile when you must tell Googlebook from a phone; handle nativeMessaging failures with a web fallback.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.chrome.com/docs/extensions/how-to/googlebook"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "chrome-na-web-store-removes-all-remaining-manifest-v2",
			"revision": 1,
			"kind": "deprecated",
			"title": "Chrome Web Store removes all remaining Manifest V2 extensions (31 Aug 2026)",
			"browsers": {
				"chrome": {
					"forecastEvent": "cws-mv2-store-removal-aug-2026",
					"date": "2026-08-31",
					"note": "Chrome Web Store removal; MV2 stopped running in Chrome 138/139"
				}
			},
			"detect": {
				"manifest": {
					"manifestVersion": 2
				},
				"confidence": "high"
			},
			"why": "The official MV2 timeline says all remaining MV2 extensions were removed from the Chrome Web Store on 2026-08-31. Existing installs can no longer receive updates or be reinstalled. MV2 extensions already stopped running in Chrome 138/139 (July/August 2025); later Chromium commits that delete MV2 flags and policies state there is no production behaviour change.",
			"fix": "Ship an MV3 version; MV2 listings and updates are gone from the store.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w29/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w35/"
				],
				"sources": [
					"https://developer.chrome.com/docs/extensions/develop/migrate/mv2-deprecation-timeline",
					"https://chromium-review.googlesource.com/c/chromium/src/+/7903742",
					"https://chromium-review.googlesource.com/c/chromium/src/+/7890750"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"{\"manifest_version\": 2, \"name\": \"x\", \"version\": \"1\"}"
				],
				"negative": [
					"{\"manifest_version\": 3, \"name\": \"x\", \"version\": \"1\"}"
				]
			}
		},
		{
			"id": "edge-na-starts-turning-off-manifest-v2-extensions-for",
			"revision": 1,
			"kind": "deprecated",
			"title": "Edge starts turning off Manifest V2 extensions for consumers (2026-2027)",
			"browsers": {
				"edge": {
					"forecastEvent": "edge-mv2-deprecation-2026",
					"note": "gradual: notices from August 2026, then off by default over the following months"
				}
			},
			"detect": {
				"manifest": {
					"manifestVersion": 2
				},
				"confidence": "high"
			},
			"why": "Microsoft announced on 2026-08-07 that Edge begins the consumer transition away from MV2 in August 2026: notices first, then MV2 extensions turned off by default over the following months in Canary, Dev and Beta before Stable, aiming to finish by the end of 2026. Enterprise deprecation follows in early 2027. Edge docs also list deprecation warnings for some users from mid-September 2026.",
			"fix": "Submit an MV3 build through Partner Center now; MV2 extensions will be disabled by default for consumers before the end of 2026.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w34/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w35/"
				],
				"sources": [
					"https://blogs.windows.com/msedgedev/2026/08/07/moving-the-microsoft-edge-extensions-ecosystem-forward-with-manifest-version-3/",
					"https://learn.microsoft.com/en-us/microsoft-edge/extensions/developer-guide/manifest-v3"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"{\"manifest_version\": 2, \"name\": \"x\", \"version\": \"1\"}"
				],
				"negative": [
					"{\"manifest_version\": 3, \"name\": \"x\", \"version\": \"1\"}"
				]
			}
		},
		{
			"id": "firefox-144-declarative-css-injection-can-choose-the-user",
			"revision": 1,
			"kind": "opportunity",
			"title": "Declarative CSS injection can choose the user or author origin",
			"browsers": {
				"firefox": {
					"version": "144",
					"date": "2025-10-14"
				}
			},
			"why": "Firefox 144 adds a css_origin key to manifest content_scripts entries and a cssOrigin property to scripting.registerContentScripts() (scripting.RegisteredContentScript) and contentScripts.register(), with values 'user' or 'author' (default 'author'). The origin value of scripting.insertCSS() also became case-insensitive. Extensions that inject user-origin stylesheets only through insertCSS/tabs.insertCSS calls can now declare that origin statically.",
			"fix": "If you inject user-origin CSS imperatively (insertCSS with origin/cssOrigin 'user') only to get it declared at document load, move it to content_scripts with \"css_origin\": \"user\" or to a registered script with cssOrigin: 'user'. Remember user-origin rules only beat page rules when they are !important. Set strict_min_version 144 or keep the imperative path for older Firefox.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/144#changes_for_add-on_developers",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1679997",
					"https://www.mozilla.org/en-US/firefox/144.0/releasenotes/"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-144-storage-local-and-storage-managed-get-getbytesinuse",
			"revision": 1,
			"kind": "opportunity",
			"title": "storage.local and storage.managed get getBytesInUse()",
			"browsers": {
				"firefox": {
					"version": "144",
					"date": "2025-10-14"
				}
			},
			"why": "Firefox 144 implements getBytesInUse() on storage.local and storage.managed, matching Chrome. Before this the call was not supported in those areas in Firefox, so cross-browser code had to guard it or estimate sizes by hand.",
			"fix": "Remove Firefox-specific fallbacks that estimate storage size (for example JSON.stringify of storage.local.get(null)) when your minimum Firefox version is 144 or higher.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/144#changes_for_add-on_developers",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1385832"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-145-cookies-set-now-rejects-invalid-cookies",
			"revision": 1,
			"kind": "breaks",
			"title": "cookies.set() now rejects invalid cookies",
			"browsers": {
				"firefox": {
					"version": "145",
					"date": "2025-11-11"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "\\b(?:browser|chrome)\\s*\\??\\.\\s*cookies\\s*\\??\\.\\s*set\\s*\\("
					}
				],
				"confidence": "low"
			},
			"why": "From Firefox 145 cookies created with cookies.set() are validated and invalid ones are rejected (the validator ran in Nightly only since 142). Extensions that built sloppy cookie values and relied on Firefox to accept them now see failed calls. MDN does not list which values count as invalid.",
			"fix": "Wrap cookies.set() in try/catch, log the rejection and test with edge cases: name or value with illegal characters, mismatched prefix (__Secure-/__Host-) and attribute combinations, and bad domain/path.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/145#changes_for_add-on_developers",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1976509"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"await browser.cookies.set({ url, name: 'session', value })"
				],
				"negative": [
					"await browser.cookies.get({ url, name: 'session' })",
					"await cookies.set(name, value, options)"
				]
			}
		},
		{
			"id": "firefox-146-browsingdata-localstorage-removal-also-clears-sessionstorage",
			"revision": 1,
			"kind": "behavior-change",
			"title": "browsingData localStorage removal also clears sessionStorage",
			"browsers": {
				"firefox": {
					"version": "146",
					"date": "2025-12-09"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "browsingData.removeLocalStorage"
					}
				],
				"confidence": "high"
			},
			"why": "browsingData.removeLocalStorage() and browsingData.remove() with localStorage set now also delete sessionStorage objects (parity with the 'Clear cookies and site data' UI). Cleaner or privacy extensions that expected sessions to survive a localStorage clear will lose them.",
			"fix": "If your extension must keep session data, do not call the localStorage clear for origins where that data matters, or restore what you need afterwards.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/146#changes_for_add-on_developers",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1886894"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"await browser.browsingData.removeLocalStorage({ since: 0 })"
				],
				"negative": [
					"await browser.browsingData.removeCookies({ since: 0 })"
				]
			}
		},
		{
			"id": "firefox-147-clipboard-read-without-clipboardread-shows-a-paste",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Clipboard read without clipboardRead shows a paste button",
			"browsers": {
				"firefox": {
					"version": "147",
					"date": "2026-01-13"
				}
			},
			"why": "Without the clipboardRead permission, navigator.clipboard.readText() and read() in an extension now show a paste button to get user confirmation. With clipboardRead the call still reads silently. Extensions that read the clipboard without the permission, expecting a plain failure or a different prompt, now show a button the user has to click.",
			"fix": "Request clipboardRead if silent reads are part of the product, or design the flow around the paste confirmation. See MDN 'Interact with the clipboard'.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/147#changes_for_add-on_developers",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1773681"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-147-keyboardevent-initkeyevent-stops-working-in-extensions",
			"revision": 1,
			"kind": "breaks",
			"title": "KeyboardEvent.initKeyEvent() stops working in extensions",
			"browsers": {
				"firefox": {
					"version": "147",
					"date": "2026-01-13"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "*.initKeyEvent"
					}
				],
				"confidence": "high"
			},
			"why": "initKeyEvent() was deprecated in Firefox 93 but kept working for add-on scripts through the preference dom.keyboardevent.init_key_event.enabled_in_addons. In 147 release builds that preference became false by mistake (bug 1848384), which broke old password-manager autofill. Mozilla then decided to keep it false everywhere (bug 2015079, target 149), as the first step to removing the method.",
			"fix": "Replace initKeyEvent() with new KeyboardEvent(type, init). No workaround is promised for users.",
			"links": {
				"pulse": [],
				"sources": [
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2013772",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2015079",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1848384"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"evt.initKeyEvent('keypress', true, true, null, false, false, false, false, 13, 0)"
				],
				"negative": [
					"evt.initKeyboardEvent('keydown')",
					"// evt.initKeyEvent(...) was removed"
				]
			}
		},
		{
			"id": "firefox-148-ships-trusted-types-content-scripts-are-exempt",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Firefox 148 ships Trusted Types; content scripts are exempt, main-world code is not",
			"browsers": {
				"firefox": {
					"version": "148",
					"date": "2026-02-24"
				}
			},
			"why": "Firefox 148 supports the Trusted Types API for web pages. Isolated-world content scripts are exempt from page Trusted Types policies (bug 1947732 fixed 140, bug 1986632 fixed 145 for document.write, eval, script enforcement and importScripts); worker eval/new Function cases are still open (bug 1990732). Mozilla states that MAIN-world content scripts cannot be exempted because they behave like page scripts, so on a page that sends require-trusted-types-for 'script', their innerHTML, eval and similar sink calls throw.",
			"fix": "Test injection on pages that send Content-Security-Policy: require-trusted-types-for 'script'. Prefer isolated-world content scripts and avoid innerHTML or script-element injection into the page world.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/148",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1947732",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1986632",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1990732"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-149-action-isenabled-takes-a-bare-tabid-and",
			"revision": 1,
			"kind": "opportunity",
			"title": "action.isEnabled() takes a bare tabId and no longer throws with no args",
			"browsers": {
				"firefox": {
					"version": "149",
					"date": "2026-03-24"
				}
			},
			"why": "action.isEnabled and browserAction.isEnabled now take tabId as a top-level parameter, like Chrome, and calling them with no argument returns the global state instead of throwing 'Incorrect argument types'.",
			"fix": "Drop the {tabId} wrapper object and try/catch workarounds on Firefox 149+.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/149#changes_for_add-on_developers",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2013477",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2012727"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-149-action-openpopup-no-longer-needs-a-user",
			"revision": 1,
			"kind": "opportunity",
			"title": "action.openPopup() no longer needs a user gesture on desktop",
			"browsers": {
				"firefox": {
					"version": "149",
					"date": "2026-03-24"
				}
			},
			"why": "action.openPopup() and browserAction.openPopup() open the popup without a user action on Firefox desktop, as in Chrome and Safari. You can open it from an alarm, native messaging event or background condition. Android is not covered.",
			"fix": "Remove click-handler workarounds that only existed to get a gesture. Keep a fallback (notification or badge) for Firefox below 149 and for failed calls.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/149#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/04/23/webextensions-api-changes-firefox-149-152/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1799344"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-149-injecting-scripts-or-css-into-moz-extension",
			"revision": 1,
			"kind": "deprecated",
			"title": "Injecting scripts or CSS into moz-extension pages is deprecated",
			"browsers": {
				"firefox": {
					"version": "149",
					"date": "2026-03-24"
				}
			},
			"why": "Firefox 149 deprecates running code in the extension's own moz-extension: documents with tabs.executeScript/insertCSS/removeCSS and scripting.executeScript/insertCSS/removeCSS. Nightly stops it, beta and release log a console warning. The restriction applies to every channel from Firefox 152.",
			"fix": "Load scripts from the page's HTML, or register a runtime.onMessage listener in the document and send a message to trigger the code.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/149#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/04/23/webextensions-api-changes-firefox-149-152/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2011234"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-149-openpopup-rejects-when-the-target-window-is",
			"revision": 1,
			"kind": "behavior-change",
			"title": "openPopup() rejects when the target window is not focused or other UI is open",
			"browsers": {
				"firefox": {
					"version": "149",
					"date": "2026-03-24"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "action.openPopup"
					},
					{
						"apiCall": "browserAction.openPopup"
					},
					{
						"apiCall": "pageAction.openPopup"
					}
				],
				"confidence": "medium"
			},
			"why": "If windowId is passed to openPopup(), the window must be focused or the call rejects; call windows.update(windowId, {focused: true}) first. Bug 2022281 (fixed 149/150) also makes openPopup() reject while another panel or context menu is open. Combined with the removal of the gesture requirement, calls that used to be silently blocked now reject with an error you must handle.",
			"fix": "Catch the rejection, focus the window first, and do not retry in a tight loop. Bug 2062229 (fixed 157) stops tab hover previews from counting as a blocking panel.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/149#changes_for_add-on_developers",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2011516",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2022281",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2062229"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"await browser.action.openPopup({ windowId })",
					"browser.browserAction.openPopup()"
				],
				"negative": [
					"browser.action.setPopup({ popup: 'popup.html' })"
				]
			}
		},
		{
			"id": "firefox-149-split-view-adds-tabs-tab-splitviewid-and",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Split view adds tabs.Tab.splitViewId and changes tabs.move/group behavior",
			"browsers": {
				"firefox": {
					"version": "149",
					"date": "2026-03-24"
				}
			},
			"why": "Firefox 149 exposes splitViewId in tabs.query, tabs.onUpdated and tabs.Tab. In 150 tabs.move can swap tabs inside a split view and unsplits a view when its tabs are listed non-adjacently; in 151 tabs.group, tabs.ungroup and tabs.move keep split views together instead of failing or separating them. Extensions that reorder or group tabs may now see split views disappear or move as a unit.",
			"fix": "Handle tabs.SPLIT_VIEW_ID_NONE and the new property in tab managers, and re-test reorder, group and stash flows with a split view open.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/149#changes_for_add-on_developers",
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/150#changes_for_add-on_developers",
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/151#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/04/23/webextensions-api-changes-firefox-149-152/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1993037",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2022549",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2029099"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-149-window-structuredclone-in-content-scripts-clones-into",
			"revision": 1,
			"kind": "behavior-change",
			"title": "window.structuredClone() in content scripts clones into the page realm",
			"browsers": {
				"firefox": {
					"version": "149",
					"date": "2026-03-24"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "window.structuredClone"
					}
				],
				"confidence": "medium"
			},
			"why": "structuredClone() now instantiates objects in the realm it is called on, not the caller's. For compatibility the content-script global got its own structuredClone that shadows window.structuredClone. So a bare structuredClone(v) behaves as before (value lives in the content script), while an explicit window.structuredClone(v) now creates a value in the page scope.",
			"fix": "Use window.structuredClone(v) when you want the page to read the value; use cloneInto(v, window) if you must support Firefox 148 and older. Re-test code that shares objects with page scripts through wrappedJSObject.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/149#changes_for_add-on_developers",
					"https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2017797",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2020773"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"const copy = window.structuredClone(state)"
				],
				"negative": [
					"const copy = structuredClone(state)",
					"const copy = globalThis.structuredClone(state)"
				]
			}
		},
		{
			"id": "firefox-150-extension-pages-can-use-webauthn-with-a",
			"revision": 1,
			"kind": "opportunity",
			"title": "Extension pages can use WebAuthn with a host-permitted RP ID",
			"browsers": {
				"firefox": {
					"version": "150",
					"date": "2026-04-21"
				}
			},
			"why": "Extension documents can call navigator.credentials.create()/get() with an RP ID for any domain covered by their host_permissions. Before this Firefox rejected a custom RP ID with a SecurityError. Servers validating the credential will see a stable moz-extension://hash origin in clientDataJSON, which differs from Chrome's chrome-extension://id.",
			"fix": "If your extension offers passkey login for your own web service, add the RP ID host to host_permissions and teach the server to accept the Firefox origin format.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/150#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/04/23/webextensions-api-changes-firefox-149-152/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1956484"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-151-webrequest-onerroroccurred-error-strings-changed",
			"revision": 1,
			"kind": "behavior-change",
			"title": "webRequest.onErrorOccurred error strings changed",
			"browsers": {
				"firefox": {
					"version": "151",
					"date": "2026-05-19"
				}
			},
			"detect": {
				"code": [
					{
						"stringLiteral": "NS_ERROR_NET_"
					}
				],
				"confidence": "high"
			},
			"why": "The error property of webRequest.onErrorOccurred now falls back to NS_ERROR_NET_ON_RECEIVING_FROM instead of a string that starts with NS_ERROR_NET_ON_ or NS_ERROR_NET_UNKNOWN_. Mozilla describes it as part of performance and reliability work. Code that matches the old strings stops matching.",
			"fix": "Do not branch on exact Firefox error strings; treat unknown values as a generic network failure and log them.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/151#changes_for_add-on_developers",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1881986"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"if (details.error.startsWith('NS_ERROR_NET_ON_')) retry()"
				],
				"negative": [
					"if (details.error === 'NS_BINDING_ABORTED') skip()",
					"// NS_ERROR_NET_ON_ strings changed"
				]
			}
		},
		{
			"id": "firefox-152-automatic-dark-theme-css-filter-on-pageaction",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Automatic dark-theme CSS filter on pageAction SVG icons is gone",
			"browsers": {
				"firefox": {
					"version": "152",
					"date": "2026-06-16",
					"forecastEvent": "firefox-152-pageaction-svg-filter-removal"
				}
			},
			"why": "Firefox no longer applies a greyscale/brightness CSS filter to SVG pageAction (address bar) icons on dark themes. Icons that relied on the filter now render in their own colors. Mozilla suggests @media (prefers-color-scheme: dark) inside the SVG, or theme_icons for the MV3 action.",
			"fix": "Add a prefers-color-scheme: dark rule to the SVG, or provide theme_icons. Test with the about:config boolean extensions.webextensions.pageActionIconDarkModeFilter.enabled set to false before 152.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w28/"
				],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/149#changes_for_add-on_developers",
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/152#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/04/23/webextensions-api-changes-firefox-149-152/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2016509",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2001318"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-152-executescript-insertcss-into-moz-extension-pages-is",
			"revision": 1,
			"kind": "breaks",
			"title": "executeScript/insertCSS into moz-extension pages is removed",
			"browsers": {
				"firefox": {
					"version": "152",
					"date": "2026-06-16",
					"forecastEvent": "firefox-152-scripting-injection-removal"
				}
			},
			"why": "Firefox 152 removes the ability to run code in an extension's own moz-extension: documents with tabs.executeScript/insertCSS/removeCSS and scripting.executeScript/insertCSS/removeCSS. Calls aimed at your popup, options page or other extension tabs now stop working.",
			"fix": "Import scripts directly in the extension page's HTML, or register a runtime.onMessage listener in the document and send a message to trigger the code.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/152#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/04/23/webextensions-api-changes-firefox-149-152/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2015559"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-152-sharedarraybuffer-is-hidden-from-content-scripts-by",
			"revision": 1,
			"kind": "breaks",
			"title": "SharedArrayBuffer is hidden from content scripts by default",
			"browsers": {
				"firefox": {
					"version": "152",
					"date": "2026-06-16"
				}
			},
			"why": "Bug 2033743 (fixed 152) hides the SharedArrayBuffer global from the sandbox content scripts run in, unless the page itself is allowed to use it (cross-origin isolated). Before this it was exposed unintentionally on every page. A content script that names the global directly (new SharedArrayBuffer, instanceof SharedArrayBuffer) now throws ReferenceError. Shared WebAssembly.Memory still works and its buffer is still a SharedArrayBuffer (bug comment 8).",
			"fix": "Feature-detect with typeof SharedArrayBuffer !== 'undefined', or reach the constructor through new WebAssembly.Memory({initial:1, maximum:1, shared:true}).buffer.constructor. Moving the code to an extension page does not help: unprivileged extension pages do not get SharedArrayBuffer either (bug 1673477 open).",
			"links": {
				"pulse": [],
				"sources": [
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2033743",
					"https://github.com/mozilla-firefox/firefox/commit/a1257d040ae4"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-152-web-pages-and-content-scripts-can-no",
			"revision": 1,
			"kind": "breaks",
			"title": "Web pages and content scripts can no longer start extension worker scripts",
			"browsers": {
				"firefox": {
					"version": "152",
					"date": "2026-06-16"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "new\\s+(?:Shared)?Worker\\s*\\(\\s*[\\w$.]*getURL\\s*\\("
					}
				],
				"confidence": "medium"
			},
			"why": "A web page or content script could create a Worker or SharedWorker from a web-accessible moz-extension: URL. From Firefox 152 such worker loads are treated as cross-origin unless the loader is the extension's own origin. Mozilla's tests assert that creating an extension worker from a content script is disallowed, and from an extension subframe it is allowed. The emergency rollback pref extensions.web_accessible_workers.deprecated_behavior (default false) is removed in Firefox 160 (bug 2074192).",
			"fix": "Create the worker in an extension page or in a moz-extension: iframe that the content script inserts, and talk to it with messaging. Alternatively build the worker from a Blob/data URL in the page context.",
			"links": {
				"pulse": [],
				"sources": [
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1767455",
					"https://github.com/mozilla-firefox/firefox/commit/bb1bf7421703",
					"https://github.com/mozilla-firefox/firefox/commit/9f6963ba0d91",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2074192",
					"https://github.com/mozilla-firefox/firefox/commit/ce6571f3bb4c"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"const w = new Worker(browser.runtime.getURL('worker.js'))",
					"new SharedWorker(chrome.runtime.getURL('shared.js'))"
				],
				"negative": [
					"const w = new Worker('worker.js')"
				]
			}
		},
		{
			"id": "firefox-153-container-colors-renamed-use-getsupportedcolors-and-getsupportedicons",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Container colors renamed; use getSupportedColors() and getSupportedIcons()",
			"browsers": {
				"firefox": {
					"version": "153",
					"date": "2026-07-21",
					"forecastEvent": "firefox-153-contextual-identities-api"
				}
			},
			"detect": {
				"manifest": {
					"permissionsAny": [
						"contextualIdentities"
					]
				},
				"code": [
					{
						"regex": "\\bcolor\\s*(?:[!=]==?|:)\\s*[\\'\"](?:turquoise|toolbar)[\\'\"]"
					}
				],
				"confidence": "medium"
			},
			"why": "contextualIdentities gets getSupportedColors() and getSupportedIcons(). The color names changed: 'turquoise' became 'cyan', 'toolbar' became 'gray', and 'violet' was added. The old names are still accepted as input but are normalised: create/update with 'turquoise' stores and returns 'cyan', and existing containers are migrated, so query()/get()/onUpdated now report 'cyan' and 'gray'. Code that compares color === 'turquoise' or 'toolbar', or maps colors from a hard-coded table, stops matching.",
			"fix": "Compare against both old and new names (or normalise turquoise->cyan, toolbar->gray), and fill color/icon pickers from the two new methods instead of hard-coded arrays.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w31/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w32/"
				],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/153#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/07/23/firefox-153-webextensions-api-updates/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2044712",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2044354"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"browser.contextualIdentities.create({ name: 'Work', color: 'turquoise', icon: 'briefcase' })",
					"if (identity.color === 'toolbar') useGray()"
				],
				"negative": [
					"const position = 'toolbar'",
					"const id = 'toolbarButton'",
					"create({ color: 'cyan' })"
				]
			}
		},
		{
			"id": "firefox-153-content-scripts-can-read-and-modify-adoptedstylesheets",
			"revision": 1,
			"kind": "opportunity",
			"title": "Content scripts can read and modify adoptedStyleSheets directly",
			"browsers": {
				"firefox": {
					"version": "153",
					"date": "2026-07-21",
					"forecastEvent": "firefox-153-new-apis"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "wrappedJSObject(?:\\s*\\.\\s*[A-Za-z_$][\\w$]*)*\\s*\\.\\s*adoptedStyleSheets"
					}
				],
				"confidence": "high"
			},
			"why": "Content scripts can now access document.adoptedStyleSheets and ShadowRoot.adoptedStyleSheets without .wrappedJSObject, which exposed the extension to interference from the page.",
			"fix": "Remove wrappedJSObject-based access to adoptedStyleSheets when your minimum is Firefox 153.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w30/"
				],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/153#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/07/23/firefox-153-webextensions-api-updates/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1751346"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"document.wrappedJSObject.adoptedStyleSheets = sheets",
					"host.shadowRoot.wrappedJSObject.adoptedStyleSheets.push(sheet)"
				],
				"negative": [
					"document.adoptedStyleSheets = [sheet]"
				]
			}
		},
		{
			"id": "firefox-153-documentid-gives-a-stable-document-identifier-across",
			"revision": 1,
			"kind": "opportunity",
			"title": "documentId gives a stable document identifier across extension APIs",
			"browsers": {
				"firefox": {
					"version": "153",
					"date": "2026-07-21",
					"forecastEvent": "firefox-153-new-apis"
				}
			},
			"why": "Firefox 153 adds documentId, including runtime.getDocumentId(), webNavigation events and methods, webRequest events, scripting injection targets and messaging APIs. If the frame's document changes, an operation that uses the ID fails instead of silently targeting the new document.",
			"fix": "Prefer documentId over tabId plus frameId for long-lived references and injections. See MDN 'Work with documentId'.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w30/"
				],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/153#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/07/23/firefox-153-webextensions-api-updates/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1891478"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-153-file-access-becomes-a-separate-opt-in",
			"revision": 1,
			"kind": "breaks",
			"title": "file:// access becomes a separate opt-in permission, off by default",
			"browsers": {
				"firefox": {
					"version": "153",
					"date": "2026-07-21",
					"forecastEvent": "firefox-153-file-access-optin"
				}
			},
			"detect": {
				"manifest": {
					"hostPermissionsAny": [
						"file:///*",
						"file://*/*"
					]
				},
				"confidence": "high"
			},
			"why": "Extensions can no longer read file:// URLs by default, including already installed ones. Users must enable the new 'Access local files on your computer' permission (desktop). extension.isAllowedFileSchemeAccess() now returns true when the user granted it (it always returned false before), and devtools.inspectedWindow.eval() on file:// URLs needs it too. Existing users lose local-file features after the upgrade until they flip the switch.",
			"fix": "Detect with extension.isAllowedFileSchemeAccess(), show instructions (about:addons, Permissions and data), and degrade gracefully. Do not assume <all_urls> covers file://.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w28/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w30/",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w32/"
				],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/153#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/07/23/firefox-153-webextensions-api-updates/",
					"https://blog.mozilla.org/addons/2026/04/23/webextensions-api-changes-firefox-149-152/",
					"https://www.mozilla.org/en-US/firefox/153.0/releasenotes/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2034168",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1598946"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"{\"manifest_version\": 3, \"host_permissions\": [\"file:///*\"]}",
					"{\"manifest_version\": 2, \"permissions\": [\"tabs\", \"file://*/*\"]}"
				],
				"negative": [
					"{\"manifest_version\": 3, \"host_permissions\": [\"https://*/*\"]}"
				]
			}
		},
		{
			"id": "firefox-153-local-network-access-prompts-are-on-for",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Local Network Access prompts are on for all users and reach content scripts",
			"browsers": {
				"firefox": {
					"version": "153",
					"date": "2026-07-21"
				}
			},
			"why": "Firefox asks before a public website connects to localhost or the LAN: Strict ETP users in 147, a rollout to all users from 151, default for everyone in 153. Mozilla engineers say content-script requests behave like the page's; extension pages with host permission for localhost are exempt. Content scripts that call a local helper app from public pages can get a permission prompt or fail.",
			"fix": "Make the localhost call from the background script or an extension page with host permission for it, and have content scripts message it. Handle fetch failures with a clear user message.",
			"links": {
				"pulse": [],
				"sources": [
					"https://www.mozilla.org/en-US/firefox/147.0/releasenotes/",
					"https://www.mozilla.org/en-US/firefox/151.0/releasenotes/",
					"https://www.mozilla.org/en-US/firefox/153.0/releasenotes/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2023758",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2032778"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-153-tabs-capturetab-capturevisibletab-return-blurred-images-of",
			"revision": 1,
			"kind": "breaks",
			"title": "tabs.captureTab/captureVisibleTab return blurred images of internal pages",
			"browsers": {
				"firefox": {
					"version": "153",
					"date": "2026-07-21"
				}
			},
			"why": "The capture is now blurred (downscaled to at most 300 px with JPEG artifacts) when the tab shows a system-principal page such as an about: page or another add-on's page; with a rect option it returns a 1px placeholder. Mozilla called it a breaking change to a public API and did not uplift to ESR 140. Tab-preview and screenshot extensions will show blur for those tabs.",
			"fix": "Treat blurred output as expected for internal pages; do not rely on pixel-accurate captures of about: pages. Skip those URLs in thumbnail logic.",
			"links": {
				"pulse": [],
				"sources": [
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1420800",
					"https://github.com/mozilla-firefox/firefox/commit/a5f3cdbf64fc"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-153-theme-additional-backgrounds-accept-css-gradients",
			"revision": 1,
			"kind": "opportunity",
			"title": "Theme additional_backgrounds accept CSS gradients",
			"browsers": {
				"firefox": {
					"version": "153",
					"date": "2026-07-21"
				}
			},
			"why": "The theme manifest key's images.additional_backgrounds now accepts CSS gradients as well as image URLs, and a new properties.additional_backgrounds_size sets the size of each item. Older Firefox versions ignore it.",
			"fix": "Use gradients instead of generated gradient images, and set strict_min_version only if the theme breaks without them.",
			"links": {
				"pulse": [],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/153#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/07/23/firefox-153-webextensions-api-updates/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2036647"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-153-userscripts-execute-for-one-off-user-script",
			"revision": 1,
			"kind": "opportunity",
			"title": "userScripts.execute() for one-off user script injection",
			"browsers": {
				"firefox": {
					"version": "153",
					"date": "2026-07-21",
					"forecastEvent": "firefox-153-new-apis"
				}
			},
			"why": "userScripts.execute() injects one or more user script sources into a tab or frame on demand, in a defined order, as opposed to the persistent URL-pattern userScripts.register(). Contributed by a community member.",
			"fix": "Replace register()/unregister() workarounds used for one-time injection with execute().",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w30/"
				],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/153#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/07/23/firefox-153-webextensions-api-updates/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1930776"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-153-webnavigation-oncommitted-fires-once-for-cross-origin",
			"revision": 1,
			"kind": "behavior-change",
			"title": "webNavigation.onCommitted fires once for cross-origin iframe loads",
			"browsers": {
				"firefox": {
					"version": "153",
					"date": "2026-07-21"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "webNavigation.onCommitted.addListener"
					}
				],
				"confidence": "medium"
			},
			"why": "Bug 1750196 (fixed 153) moves the webNavigation.onCommitted trigger for subframes from OnStateChange to OnLocationChange. As a result: the duplicate onCommitted for cross-origin iframe loads under Fission is gone; subframe onCommitted now fires when the document actually commits (later than before); and with redirects it reports the post-redirect URL instead of the pre-redirect one. Code that counted events, deduplicated by hand, or matched the pre-redirect URL in frames will see different events.",
			"fix": "Remove duplicate-event workarounds once your minimum is 153, keep handlers idempotent, and match frame URLs from onCommitted as the final (post-redirect) URL.",
			"links": {
				"pulse": [],
				"sources": [
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1750196"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"browser.webNavigation.onCommitted.addListener(onCommit)"
				],
				"negative": [
					"browser.webNavigation.onCompleted.addListener(onDone)"
				]
			}
		},
		{
			"id": "firefox-154-corrupted-storage-local-databases-are-auto-reset",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Corrupted storage.local databases are auto-reset on all channels",
			"browsers": {
				"firefox": {
					"version": "154",
					"date": "2026-08-18"
				}
			},
			"why": "Bug 1992973 enables, on all channels, an automatic reset of the IndexedDB behind storage.local when Firefox detects known corruption cases that left extensions stuck with 'An unexpected error occurred'. Extensions that were permanently broken for some profiles start working again, with the stored data lost.",
			"fix": "Treat empty storage.local after errors as possible; keep recoverable state in storage.sync or on a server, and re-initialise defaults.",
			"links": {
				"pulse": [],
				"sources": [
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1992973",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1885297",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1979997"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-154-manifest-sandbox-key-lets-extension-pages-run",
			"revision": 1,
			"kind": "opportunity",
			"title": "Manifest 'sandbox' key lets extension pages run eval in an opaque origin",
			"browsers": {
				"firefox": {
					"version": "154",
					"date": "2026-08-18",
					"forecastEvent": "firefox-154-sandbox-manifest-key"
				}
			},
			"why": "Firefox supports the sandbox manifest key: listed pages load with an opaque origin and without direct access to extension APIs, and may use eval() and similar constructs that the extension CSP blocks. Chrome already supports the same key, so cross-browser extensions can share one setup.",
			"fix": "Move eval or new Function work (templating, wasm, user code) into a sandbox page and talk to it with postMessage; delete Firefox-only workarounds when your minimum is 154.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w33/"
				],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/154#changes_for_add-on_developers",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=1685123",
					"https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/sandbox"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-155-alert-in-background-pages-no-longer-logs",
			"revision": 1,
			"kind": "behavior-change",
			"title": "alert() in background pages no longer logs to the console",
			"browsers": {
				"firefox": {
					"version": "155",
					"date": "2026-09-01"
				}
			},
			"why": "Firefox 155 removes the override that turned alert() in extension background pages into a console log (which also opened the Browser Console). MDN already documents that background pages do not support alert(), confirm() or prompt(). Code using alert() as a debug print loses that output.",
			"fix": "Use console.log, or show UI through notifications or an extension page.",
			"links": {
				"pulse": [],
				"sources": [
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2060892",
					"https://github.com/mozilla-firefox/firefox/commit/61baf1ee061e"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-156-theme-backgrounds-area-chooses-where-backgrounds-are",
			"revision": 1,
			"kind": "opportunity",
			"title": "Theme backgrounds_area chooses where backgrounds are drawn",
			"browsers": {
				"firefox": {
					"version": "156",
					"date": "2026-09-15"
				}
			},
			"why": "The theme manifest key gains backgrounds_area: 'window' draws images and gradients across the whole window, 'top_toolbars' restricts them to the top toolbars, and 'auto' (default) decides from additional_backgrounds_alignment. It is the main fix for image themes that look wrong next to the Nova sidebar.",
			"fix": "For image themes made for a wide short toolbar set 'top_toolbars' together with a frame color.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/156#changes_for_add-on_developers",
					"https://blog.mozilla.org/addons/2026/09/29/nova-is-here-what-changes-for-your-firefox-theme/",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2059526"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-157-themes-render-differently-and-some-theme-keys",
			"revision": 1,
			"kind": "behavior-change",
			"title": "Themes render differently and some theme keys have no effect",
			"browsers": {
				"firefox": {
					"version": "157",
					"date": "2026-09-29",
					"forecastEvent": "firefox-157-nova-ui-refresh"
				}
			},
			"detect": {
				"manifest": {
					"keysAny": [
						"theme.colors.ntp_text",
						"theme.colors.popup_text",
						"theme.colors.sidebar_highlight",
						"theme.colors.sidebar_highlight_text",
						"theme.colors.button_background_hover",
						"theme.colors.button_background_active"
					]
				},
				"confidence": "high"
			},
			"why": "Firefox 157 ships the Nova interface. The toolbar and sidebar sit flush with the window, the sidebar shares the theme background, the selected-tab shadow is gone and the accent color follows the OS. ntp_text no longer styles the new tab search box, popup_text no longer styles the address bar dropdown, sidebar_highlight and sidebar_highlight_text have no visible effect, and button_background_hover/active also apply to tab strip buttons.",
			"fix": "Check light/dark, vertical tabs and sidebar states. Set tab_selected, tab_line, sidebar and sidebar_text, and use semi-transparent button hover colors. Image themes should set backgrounds_area (Firefox 156) and a toolbar color.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://blog.mozilla.org/addons/2026/09/29/nova-is-here-what-changes-for-your-firefox-theme/",
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/156#changes_for_add-on_developers",
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2059526",
					"https://www.mozilla.org/en-US/firefox/157.0/releasenotes/"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"{\"manifest_version\": 2, \"theme\": {\"colors\": {\"frame\": \"#000\", \"ntp_text\": \"#fff\"}}}"
				],
				"negative": [
					"{\"manifest_version\": 2, \"theme\": {\"colors\": {\"frame\": \"#000\"}}}"
				]
			}
		},
		{
			"id": "firefox-160-runtime-getplatforminfo-arch-returns-arm64-instead-of",
			"revision": 1,
			"kind": "breaks",
			"title": "runtime.getPlatformInfo().arch returns 'arm64' instead of 'aarch64'",
			"browsers": {
				"firefox": {
					"version": "160",
					"date": "2026-11-10",
					"forecastEvent": "w3c-runtime-getplatforminfo-nacl-arch-deprecation"
				}
			},
			"detect": {
				"code": [
					{
						"stringLiteral": "aarch64",
						"exact": true
					},
					{
						"regex": "PlatformArch\\s*\\??\\.\\s*AARCH64\\b"
					}
				],
				"confidence": "medium"
			},
			"why": "On 64-bit ARM Firefox 160 returns arm64 for getPlatformInfo().arch and defines PlatformArch.ARM64 instead of AARCH64, matching Chrome. Code that checks for 'aarch64' (download selectors for native helpers, telemetry, architecture-specific wasm) stops matching on 64-bit ARM.",
			"fix": "Accept both 'arm64' and 'aarch64' when picking binaries.",
			"links": {
				"pulse": [],
				"sources": [
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2080310",
					"https://github.com/mozilla-firefox/firefox/commit/e775d5304b32",
					"https://github.com/w3c/webextensions/issues/1056",
					"https://whattrainisitnow.com/api/release/schedule/?version=160"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"if (info.arch === 'aarch64') useArmHelper()",
					"if (info.arch === browser.runtime.PlatformArch.AARCH64) useArmHelper()"
				],
				"negative": [
					"const target = 'aarch64-apple-darwin'",
					"if (info.arch === 'arm64') useArmHelper()"
				]
			}
		},
		{
			"id": "firefox-160-runtime-messagesender-gets-windowid-first-browser-to",
			"revision": 1,
			"kind": "opportunity",
			"title": "runtime.MessageSender gets windowId (first browser to implement it)",
			"browsers": {
				"firefox": {
					"version": "160",
					"date": "2026-11-10",
					"forecastEvent": "w3c-messagesender-windowid-supportive"
				}
			},
			"why": "Firefox adds windowId to runtime.MessageSender (WECG #1077), so popups, side panels and devtools panels can learn the sender's window without going through sender.tab. When no window can be determined Firefox leaves the property absent.",
			"fix": "Feature-detect with 'windowId' in sender; keep the sender.tab?.windowId fallback for other browsers.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w43/"
				],
				"sources": [
					"https://bugzilla.mozilla.org/show_bug.cgi?id=2072133",
					"https://github.com/mozilla-firefox/firefox/commit/f43deb1ab6b2",
					"https://github.com/w3c/webextensions/issues/1077",
					"https://whattrainisitnow.com/api/release/schedule/?version=160"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "firefox-na-new-extensions-must-declare-data-collection-in",
			"revision": 1,
			"kind": "behavior-change",
			"title": "New Firefox extensions must declare data collection in manifest.json",
			"browsers": {
				"firefox": {
					"date": "2025-11-03"
				}
			},
			"why": "From 2025-11-03 AMO requires every NEW Firefox extension to declare browser_specific_settings.gecko.data_collection_permissions (required: [\"none\"] when nothing is collected). Firefox shows the declared data types in the install prompt, on AMO and in about:addons. It does not change how already-published extensions run; Mozilla says all extensions will have to adopt it later, but I found no date.",
			"fix": "Before submitting a brand-new Firefox extension add the key and declare the data your code or third-party SDKs really send out. For existing add-ons add it with your next release so you are ready when it becomes mandatory.",
			"links": {
				"pulse": [],
				"sources": [
					"https://blog.mozilla.org/addons/2025/10/23/data-collection-consent-changes-for-new-firefox-extensions/",
					"https://extensionworkshop.com/documentation/develop/firefox-builtin-data-consent/"
				]
			},
			"verifiedAt": "2026-10-22"
		},
		{
			"id": "gaia-manage-accounts-header-hidden",
			"revision": 2,
			"kind": "breaks",
			"title": "X-Chrome-Manage-Accounts is hidden from extensions on Google account URLs",
			"browsers": {
				"chrome": {
					"version": "157",
					"date": "2026-11-03",
					"forecastEvent": "chrome-157-manage-accounts-header"
				}
			},
			"detect": {
				"code": [
					{
						"stringLiteral": "X-Chrome-Manage-Accounts",
						"caseInsensitive": true
					}
				],
				"confidence": "high"
			},
			"why": "From Chrome 157 the X-Chrome-Manage-Accounts response header on Gaia (accounts.google.com) URLs joins X-Chrome-ID-Consistency-Response in the hidden-header list. Extensions can no longer read, modify or inject it through webRequest or declarativeNetRequest, even with host permissions for accounts.google.com. Nothing errors: the header is simply not there.",
			"fix": "Stop reading, injecting or modifying X-Chrome-Manage-Accounts on Google account URLs: the header drives Chrome's own account-management UI, and Chrome offers no extension API that replaces it. Test any flow that depends on it on Chrome 157 beta before stable on November 3, 2026.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://chromium-review.googlesource.com/c/chromium/src/+/8501321"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"const v = details.responseHeaders.find((h) => h.name === 'X-Chrome-Manage-Accounts')",
					"{ \"header\": \"x-chrome-manage-accounts\", \"operation\": \"remove\" }"
				],
				"negative": [
					"// X-Chrome-Manage-Accounts is hidden from Chrome 157",
					"const v = h.name === 'X-Chrome-ID-Consistency-Response'"
				]
			}
		},
		{
			"id": "publicsuffix-invalid-host-errors",
			"revision": 1,
			"kind": "breaks",
			"title": "publicSuffix calls now error on invalid hostnames",
			"browsers": {
				"firefox": {
					"version": "158",
					"date": "2026-10-13",
					"forecastEvent": "firefox-158-publicsuffix-isknownsuffix-throws",
					"note": "isKnownSuffix() throws on any invalid hostname instead of returning false"
				},
				"chrome": {
					"version": "157",
					"date": "2026-11-03",
					"note": "hostnames with a leading dot (\".example.com\") are rejected"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "publicSuffix.*"
					}
				],
				"confidence": "medium"
			},
			"why": "Code that passes user- or URL-derived strings straight to publicSuffix now gets an error instead of a false or a best-effort answer. Firefox 158 throws on any invalid hostname (empty, leading dot, non-host strings); Chrome 157 rejects leading-dot hostnames such as cookie-domain values. An uncaught throw aborts the whole event handler it runs in.",
			"fix": "Normalize and validate before calling: strip leading dots and reject empty or non-host strings. Or wrap every publicSuffix call in try/catch. One fix covers both browsers. Test on Firefox 158 now and on Chrome 157 beta before November 3, 2026.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w43/#firefox-158-is-live--and-publicsuffix-now-throws",
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/158",
					"https://chromium-review.googlesource.com/c/chromium/src/+/8464103",
					"https://github.com/w3c/webextensions/pull/1097"
				]
			},
			"verifiedAt": "2026-10-21",
			"fixtures": {
				"positive": [
					"if (!browser.publicSuffix.isKnownSuffix(host)) return",
					"const known = await chrome.publicSuffix.isKnownSuffix(cookie.domain)",
					"const api = globalThis.browser ?? chrome\napi.publicSuffix?.isKnownSuffix?.(h)"
				],
				"negative": [
					"// browser.publicSuffix.isKnownSuffix(host) throws on Firefox 158",
					"const label = 'publicSuffix.isKnownSuffix(host)'",
					"const myPublicSuffix = { isKnownSuffix() {} }; myPublicSuffix.isKnownSuffix(x)"
				]
			}
		},
		{
			"id": "runtime-getversion-available",
			"revision": 1,
			"kind": "opportunity",
			"title": "runtime.getVersion() replaces reading your own manifest.json",
			"browsers": {
				"firefox": {
					"version": "158",
					"date": "2026-10-13",
					"forecastEvent": "firefox-158-runtime-getversion"
				},
				"chrome": {
					"note": "implemented before Firefox (WECG implemented label, October 2025)"
				},
				"safari": {
					"note": "implemented before Firefox (WECG implemented label, October 2025)"
				}
			},
			"detect": {
				"code": [
					{
						"regex": "fetch\\s*\\(\\s*[\\w$.]*getURL\\s*\\(\\s*['\"`]/?manifest\\.json['\"`]\\s*\\)"
					}
				],
				"confidence": "high"
			},
			"why": "With Firefox 158, runtime.getVersion() returns your extension's version in all three major browsers. Fetching your own manifest.json just to read the version is no longer needed for changelog popups, version-gated migrations or telemetry self-reporting.",
			"fix": "Use runtime.getVersion() where your minimum browser versions allow it, and keep the manifest fetch as a fallback for older releases.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w43/#two-firefox-firsts-windowid-lands-getversion-completes-parity"
				],
				"sources": [
					"https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Releases/158",
					"https://github.com/w3c/webextensions/issues/878"
				]
			},
			"verifiedAt": "2026-10-21",
			"fixtures": {
				"positive": [
					"const m = await (await fetch(chrome.runtime.getURL('manifest.json'))).json()",
					"fetch(browser.runtime.getURL(\"/manifest.json\")).then((r) => r.json())"
				],
				"negative": [
					"const v = chrome.runtime.getManifest().version",
					"// fetch(chrome.runtime.getURL('manifest.json'))",
					"fetch(chrome.runtime.getURL('rules.json'))"
				]
			}
		},
		{
			"id": "webrequest-user-restricted-initiators",
			"revision": 2,
			"kind": "behavior-change",
			"title": "webRequest and DNR miss navigations started from sites the user blocked extensions on",
			"browsers": {
				"chrome": {
					"version": "157",
					"date": "2026-11-03",
					"forecastEvent": "chrome-157-webrequest-initiator-user-restrictions"
				}
			},
			"detect": {
				"code": [
					{
						"apiCall": "webRequest.*.addListener"
					},
					{
						"apiCall": "declarativeNetRequest.updateDynamicRules"
					},
					{
						"apiCall": "declarativeNetRequest.updateSessionRules"
					},
					{
						"stringLiteral": "rule_resources"
					}
				],
				"confidence": "medium"
			},
			"why": "From Chrome 157, navigations initiated by a site the user put under \"block all extensions on this site\" (including link clicks from it to allowed sites) are invisible to webRequest listeners and no longer match declarativeNetRequest rules, even permission-less block rules. No error is raised: the events never arrive. It only matters if your extension promises to see or block every navigation (blockers, parental controls, DLP, annotators).",
			"fix": "Do not treat a missing event as a missing navigation. Test your webRequest and DNR flows on Chrome 157 beta against a user-restricted site, and adjust any product copy that promises full coverage before stable on November 3, 2026.",
			"links": {
				"pulse": [
					"https://blog.extenshi.io/posts/extension-pulse-2026-w42/"
				],
				"sources": [
					"https://chromium-review.googlesource.com/c/chromium/src/+/8497010"
				]
			},
			"verifiedAt": "2026-10-22",
			"fixtures": {
				"positive": [
					"chrome.webRequest.onBeforeRequest.addListener(onRequest, { urls: ['<all_urls>'] })",
					"browser.webRequest.onCompleted.addListener((d) => log(d), filter)",
					"await chrome.declarativeNetRequest.updateDynamicRules({ addRules: rules })",
					"{ \"declarative_net_request\": { \"rule_resources\": [{ \"id\": \"block\", \"enabled\": true, \"path\": \"rules.json\" }] } }"
				],
				"negative": [
					"chrome.webNavigation.onCommitted.addListener(onNav)",
					"// chrome.webRequest.onBeforeRequest.addListener(fn)",
					"chrome.webRequest.onBeforeRequest.removeListener(onRequest)",
					"const rules = await chrome.declarativeNetRequest.getDynamicRules()"
				]
			}
		}
	]
}
