Cookie Stores in Incognito and Firefox Containers

Target the right cookie store from an MV3 extension: storeId for incognito, spanning versus split mode, Firefox container stores, and mapping a tab to its store with getAllCookieStores.

Published October 2, 2026 Updated October 2, 2026 8 min read
Table of Contents

The user is signed in to the site in an incognito window, your popup says “signed out”, and the cookie your extension writes never shows up in that window. Or, in Firefox, the user keeps a “Work” container and a “Personal” container signed in to different accounts, and your extension reads one account’s session while the user is looking at the other. In both cases the API is answering from the wrong cookie store, because every cookie call has an implicit target and the default is rarely the one the user is looking at. This guide is part of cookies and webRequest observation.

Why there is more than one store

Browsers keep separate cookie jars for contexts that must not share identity. Chrome has two: the default store (id "0") and the incognito store (id "1"), which exists only while an incognito window is open. Firefox has "firefox-default", "firefox-private", and one "firefox-container-N" store per container. Every chrome.cookies call accepts a storeId; when you omit it, the call targets the store associated with the calling context. For a service worker in "spanning" incognito mode — the default — that is always the default store, no matter which window the user is in. The extension is not confused; it is asking the wrong jar.

Cookie stores an extension can addressThe default store, the incognito or private store, and Firefox container stores, each with its store id and when it exists.Default storeChrome "0" · Firefox "firefox-default"always presentIncognito / privateChrome "1" · Firefox "firefox-private"only while openFirefox containers"firefox-container-1" …one per containerPartitions inside eachpartitionKeyorthogonal to stores
Omit storeId and you get the top band — whichever window the user is actually in.

Step-by-step: always target the user’s store

1. Decide your incognito mode deliberately

1{
2  "incognito": "spanning"   // one worker; sees all stores via storeId (default)
3  // "incognito": "split"   // a second worker instance runs inside incognito
4  // "incognito": "not_allowed"  // never runs in incognito
5}

Execution context: the manifest. With "spanning" a single service worker handles events from both normal and incognito windows and must pass storeId explicitly. With "split" Chrome starts a separate worker instance for incognito whose default store is the incognito store, at the cost of two copies of in-memory state. Firefox does not support "split" and treats it as "spanning". The user must still allow the extension in incognito for any of this to apply. The trade-offs are covered in extensions in incognito: split versus spanning.

2. Map tabs to stores with getAllCookieStores

Each store lists the tab ids that use it, which is the bridge from “the tab the user is looking at” to “the storeId to pass”.

1export async function storeIdForTab(tabId) {
2  const stores = await chrome.cookies.getAllCookieStores();
3  const store = stores.find((s) => s.tabIds.includes(tabId));
4  return store?.id ?? "0";
5}

Execution context: the service worker. The incognito store appears only if the extension is allowed in incognito and an incognito window is currently open. Firefox’s tab.cookieStoreId property gives the same answer directly from browser.tabs.get, without a lookup; Chrome’s tab object has no such field.

3. Pass the storeId on every call

 1export async function sessionForTab(tabId) {
 2  const storeId = await storeIdForTab(tabId);
 3  return chrome.cookies.get({
 4    url: "https://app.example.com/",
 5    name: "session_id",
 6    storeId,
 7  });
 8}
 9
10// From the popup:
11const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
12const session = await chrome.runtime.sendMessage({ type: "session", tabId: tab.id });

Execution context: the service worker for the cookie call; the popup for the active-tab query. The popup belongs to a specific window, so currentWindow: true resolves to the right tab even when several windows are open. Writes and removals need the same storeId — writing without it from a spanning worker plants the cookie in the default store, invisible to the incognito tab.

Reading the session for the tab the user is onThe popup finds the active tab and asks the worker; the worker maps the tab id to a cookie store id with getAllCookieStores and reads the cookie from that store.PopupService workerCookie APItabs.query(active){type:'session', tabId}getAllCookieStores()[{id:'1', tabIds:[…]}]get({…, storeId:'1'})cookiesigned-in (incognito)
Two lookups — tab to store, then store to cookie — and the answer matches what the user sees.

4. Attribute onChanged events to a store

Every change event carries the cookie’s storeId. A badge that reflects sign-in state must update only the tabs that use that store.

1chrome.cookies.onChanged.addListener(async ({ cookie, removed }) => {
2  if (cookie.name !== "session_id") return;
3  const stores = await chrome.cookies.getAllCookieStores();
4  const tabIds = stores.find((s) => s.id === cookie.storeId)?.tabIds ?? [];
5  for (const tabId of tabIds) {
6    chrome.action.setBadgeText({ tabId, text: removed ? "" : "✓" });
7  }
8});

Execution context: the service worker, top level. Per-tab badge text is cleared when the tab navigates, so pair this with a refresh on tabs.onUpdated. Events from the incognito store are only delivered if the extension is allowed in incognito.

5. Handle Firefox containers explicitly

1// Firefox only: list containers and their stores
2if (globalThis.browser?.contextualIdentities) {
3  const identities = await browser.contextualIdentities.query({});
4  for (const { name, cookieStoreId } of identities) {
5    const jar = await browser.cookies.getAll({ domain: "example.com", storeId: cookieStoreId });
6    console.log(name, jar.length);
7  }
8}

Execution context: the Firefox background context. contextualIdentities needs its own permission ("contextualIdentities") and exists only when containers are enabled. Chrome and Safari have no equivalent; feature-detect rather than branching on the user agent.

6. Keep per-store state apart in your own storage

The cookie API is only half the problem. Any state your extension derives from cookies — a cached profile, a sync cursor, an unread count — must be keyed by store too, or the incognito session’s data leaks into the normal window’s popup.

1const keyFor = (storeId, name) => `${name}:${storeId}`;
2
3export async function cacheProfile(storeId, profile) {
4  const area = storeId === "0" || storeId === "firefox-default"
5    ? chrome.storage.local          // survives restarts
6    : chrome.storage.session;       // incognito and containers: memory only
7  await area.set({ [keyFor(storeId, "profile")]: profile });
8}

Execution context: the service worker. Writing incognito-derived data to chrome.storage.local persists it to disk after the private window closes, which contradicts what the user expects from incognito and is a review risk. chrome.storage.session is cleared on browser exit and never touches disk in Chrome. Firefox containers are persistent identities, so you may choose local for them deliberately — but make it a choice, not an accident.

When the last incognito window closes, Chrome deletes store "1" and every cookie in it. Listen for chrome.windows.onRemoved and, if getAllCookieStores no longer lists the incognito store, drop every cached entry keyed to it so stale data cannot reappear the next time a private window opens.

Cross-browser variation

  • Chrome / Edge: store ids "0" and "1"; "split" mode gives incognito its own worker. getAllCookieStores omits the incognito store when no incognito window is open.
  • Firefox: string ids, containers, and tab.cookieStoreId on every tab. "split" is not supported. Private browsing access must be granted by the user per extension.
  • Safari: private browsing uses a separate store but extensions rarely see it; getAllCookieStores typically returns only the default store, and private windows run extensions only when the user enables them.
Store handling across enginesComparison of store ids, split incognito mode, container support and tab-to-store mapping in Chrome, Firefox and Safari.CapabilityChromeFirefoxSafariIncognito store id"1""firefox-private"Rarely exposedsplit modeYesTreated as spanningNoContainer storesNoYesNoTab → storegetAllCookieStorestab.cookieStoreIdgetAllCookieStores
Firefox gives you the store on the tab object; Chrome makes you look it up.

Verification

  1. Allow the extension in incognito from chrome://extensions → Details.
  2. Sign in to the site in a normal window and sign out in an incognito window (or the reverse).
  3. Open the popup in each window. Each must report the state of its own window.
  4. In the service worker console, run await chrome.cookies.getAllCookieStores() and confirm two stores, each listing the expected tab ids.
  5. In Firefox, repeat with two containers and confirm each container tab reports its own session.

FAQ

Why does getAllCookieStores only return one store?

Either no incognito window is open, or the extension is not allowed in incognito. The incognito store does not exist until a private window does, and it disappears when the last one closes — along with every cookie in it.

Should I use split mode to avoid passing storeId?

Only if your extension’s in-memory state must also be isolated between normal and incognito. Split mode doubles worker instances and makes shared state harder; spanning plus an explicit storeId is simpler for most cookie features.

Technically yes — read it from "0" and write it to "1". Doing so defeats the user’s reason for opening incognito, and store reviewers treat it as a privacy violation. Do not.

Other Core APIs & Cross-Browser Data Management Resources