Manifest Keys Reference
A practical reference to MV3 manifest.json keys: required fields, background, action, permissions, content scripts, web_accessible_resources, CSP, key, minimum versions, icons and browser_specific_settings.
manifest.json is the one file every browser reads before running a line of your code, and it decides more than most developers expect: the extension’s id in development, which pages may load your resources, which browsers will install the package, which permissions trigger warnings, and how the extension appears in the toolbar at every display density. Many of its keys are set once and forgotten — until a native host rejects a development build because its id changed, a fingerprinting script detects the extension through an exposed image, or Firefox refuses an upload for a missing add-on id. This topic sits inside Manifest V3 Architecture & Extension Lifecycle and works through the keys that cause real problems, beginning with pinning a stable extension id with the key field.
The reference below is organised by what each key controls rather than alphabetically. Every key has a section in the annotated manifest; the ones with non-obvious behaviour have a section of their own further down and, where they need it, a dedicated guide.
Prerequisites checklist
- A single source of truth for the manifest — a template or a build-time generator — rather than hand-edited copies per browser.
- A decision about which browsers and minimum versions you support.
- Icons at 16, 32, 48 and 128 pixels as PNG files.
- A Firefox add-on id if you ship to Firefox.
- A public key (
key) if anything outside the browser — a native host, an OAuth client, a server allow-list — depends on the extension id during development. - A list of every resource that web pages must load from the extension, and the pages that need them.
Annotated manifest
1{
2 // ── Identity ─────────────────────────────────────────────
3 "manifest_version": 3, // required; 2 is no longer accepted by Chrome
4 "name": "__MSG_appName__", // localised via _locales/<lang>/messages.json
5 "short_name": "Readable", // used where space is tight
6 "version": "4.2.0", // up to four dot-separated integers, each 0–65535
7 "version_name": "4.2 beta", // display-only; Chrome shows it, stores ignore it
8 "description": "__MSG_appDesc__", // 132 characters max for the Chrome Web Store
9 "default_locale": "en", // required when _locales exists
10 "icons": { "16": "icons/16.png", "32": "icons/32.png", "48": "icons/48.png", "128": "icons/128.png" },
11 "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA…", // pins the id of unpacked builds
12
13 // ── Runtime ──────────────────────────────────────────────
14 "background": { "service_worker": "sw.js", "type": "module" },
15 "action": { "default_popup": "popup.html", "default_title": "Readable", "default_icon": { "16": "icons/16.png", "32": "icons/32.png" } },
16 "options_ui": { "page": "options.html", "open_in_tab": true },
17 "side_panel": { "default_path": "panel.html" },
18 "content_scripts": [{
19 "matches": ["https://*.example.com/*"],
20 "js": ["content.js"],
21 "css": ["content.css"],
22 "run_at": "document_idle", // document_start | document_end | document_idle
23 "all_frames": false,
24 "world": "ISOLATED" // or "MAIN" (Chrome 111+)
25 }],
26 "commands": { "_execute_action": { "suggested_key": { "default": "Alt+Shift+R" } } },
27 "omnibox": { "keyword": "rd" },
28
29 // ── Access ───────────────────────────────────────────────
30 "permissions": ["storage", "alarms", "scripting", "activeTab", "contextMenus", "sidePanel"],
31 "optional_permissions": ["notifications"],
32 "host_permissions": ["https://api.readable.example/*"],
33 "optional_host_permissions": ["https://*/*"],
34
35 // ── Exposure ─────────────────────────────────────────────
36 "web_accessible_resources": [{
37 "resources": ["fonts/*.woff2", "overlay.css"],
38 "matches": ["https://*.example.com/*"],
39 "use_dynamic_url": true // URL changes per session: harder to fingerprint
40 }],
41 "externally_connectable": { "matches": ["https://app.readable.example/*"] },
42 "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" },
43
44 // ── Compatibility ────────────────────────────────────────
45 "minimum_chrome_version": "116",
46 "browser_specific_settings": {
47 "gecko": { "id": "readable@acme.example", "strict_min_version": "115.0" },
48 "safari": { "strict_min_version": "16.4" }
49 },
50 "incognito": "spanning"
51}
Execution context: parsed by the browser at install, on update, and on every reload of an unpacked extension. Chrome treats unknown keys as warnings, not errors, which is why one manifest can carry Firefox’s browser_specific_settings safely; Firefox ignores Chrome-only keys like side_panel and minimum_chrome_version with a warning in about:debugging. Comments are not valid JSON — strip them in the build, or keep this annotated version as documentation and generate the real file.
1. Identity: version and key
version must strictly increase with every store upload; the store compares it numerically segment by segment, so 4.10.0 is newer than 4.9.0. Use version_name for human-friendly labels like “beta”. The key field is unusual: it contains the public key from your store item, and its only effect is to make an unpacked build compute the same extension id as the published one. That matters whenever something outside the browser recognises the extension by id — native messaging hosts, OAuth redirect URIs, server-side origin allow-lists.
1// Confirm the id at runtime
2console.log(chrome.runtime.id); // identical in unpacked and store builds when key is set
Execution context: any extension context. The store strips key from uploaded packages, or rejects them, depending on the store — remove it from the packaged build.
Version numbers deserve a scheme, not just increments. The store only needs each upload to be numerically larger than the last, but your support team needs to tell at a glance which build a user is on, and your rollback plan needs room to publish an “older” build under a newer number. A common convention keeps the first three segments as a semantic version and reserves the fourth for rebuilds: 4.2.0.0 for the release, 4.2.0.1 for a rebuild of the same source with a packaging fix, and 4.2.1.0 for a rollback that republishes 4.1.x code. Read the running version with chrome.runtime.getManifest().version and include it in every error report, so crash data can be grouped by release — the approach described in versioning and changelogs for extension releases. Details in pinning a stable extension id with the key field.
2. Runtime: background, action and content scripts
The background key accepts exactly one service worker file in Chrome, optionally as an ES module with "type": "module". Firefox’s MV3 prefers "scripts": [...] (an event page) and accepts service_worker only in recent versions; Safari accepts either. The action key replaces MV2’s two action types. Content scripts declared here inject on every matching page load for the extension’s whole life, which makes them the right choice for always-on features and the wrong one for features triggered by the user — those belong in chrome.scripting calls.
1// Firefox-flavoured background, generated alongside the Chrome one
2"background": { "scripts": ["sw.js"], "type": "module" }
Execution context: the Firefox manifest. Emitting both background forms from one template is covered in generating a manifest per browser target. Content script matching syntax is in writing match patterns and globs.
3. Exposure: what the outside world can reach
Two keys open the extension to the web. web_accessible_resources lets web pages load specific files from your package — images, fonts, CSS, an iframe page — and every exposed file is also a way for any matching page to detect that your extension is installed. externally_connectable lets specific web origins (and other extensions) send messages to your service worker with chrome.runtime.sendMessage(extensionId, …). Both should be as narrow as possible.
1"web_accessible_resources": [{
2 "resources": ["overlay.html", "overlay.css"],
3 "matches": ["https://*.example.com/*"], // not <all_urls> unless truly needed
4 "use_dynamic_url": true
5}]
Execution context: the manifest. With use_dynamic_url, the resource’s URL includes a per-session token instead of the stable extension id, which defeats simple probing. Resources must be fetched with chrome.runtime.getURL() to get the right URL. See declaring web accessible resources correctly and externally connectable matches and ids.
4. Compatibility: who can install it
minimum_chrome_version prevents installation on Chrome versions older than the one you name and stops updates reaching them, so users on older browsers stay on your last compatible release. Set it to the oldest version that supports every API you call — not to the version you happen to test on. browser_specific_settings carries Firefox’s add-on id and minimum version under gecko, and Safari’s minimum version under safari. Firefox requires the id for signing in MV3.
1// Runtime guard for an API newer than your minimum
2if (chrome.sidePanel?.open) {
3 await chrome.sidePanel.open({ windowId });
4} else {
5 await chrome.tabs.create({ url: chrome.runtime.getURL("panel.html") });
6}
Execution context: any extension context. Even with a correct minimum version, optional features introduced later should be feature-detected so the extension degrades instead of throwing. See setting minimum browser versions and browser-specific settings for Firefox and Safari.
5. Icons and presentation
The icons key supplies the extension’s identity in the extensions page, the store and permission dialogs; action.default_icon supplies the toolbar button. They are separate because they serve different sizes. Provide PNGs at 16, 32, 48 and 128 pixels for icons and at 16 and 32 for the action — the browser picks the size matching the display’s pixel density, and a missing size is scaled from the nearest one, which blurs.
1// Changing the action icon at runtime uses the same size map
2chrome.action.setIcon({ path: { 16: "icons/off-16.png", 32: "icons/off-32.png" } });
Execution context: the service worker or any extension page. SVG is not accepted for manifest icons in Chrome. Details and a build script are in icon sizes for the manifest and action.
6. Validating the manifest in CI
Because so many manifest mistakes fail silently at runtime, a build-time check pays for itself. A short script can verify the rules that the browser will not enforce until a user hits them: no key in the packaged build, no broad hosts outside an allow-list, every file referenced by the manifest present in the package, a version greater than the last published one, and browser_specific_settings present in Firefox builds.
1// scripts/check-manifest.mjs
2import fs from "node:fs";
3const m = JSON.parse(fs.readFileSync("dist/manifest.json", "utf8"));
4const errors = [];
5if (m.key) errors.push("remove `key` from the packaged manifest");
6if ((m.host_permissions ?? []).some((h) => /<all_urls>|\*:\/\/\*\/\*/.test(h))) errors.push("broad host permission");
7for (const f of Object.values(m.icons ?? {})) if (!fs.existsSync(`dist/${f}`)) errors.push(`missing icon ${f}`);
8if (errors.length) { console.error(errors.join("\n")); process.exit(1); }
Execution context: Node in CI, run against the built output for each browser target. Add a check per lesson learned; the script grows into a record of every manifest bug your team has shipped once and will not ship again. Snapshotting the generated manifest, covered in snapshot testing the generated manifest, catches unintended changes in review.
7. Localised fields
name, short_name, description and action.default_title accept __MSG_key__ placeholders that the browser resolves from _locales/<locale>/messages.json at install, using the browser’s UI language with default_locale as the fallback. Localising these fields matters more than localising the UI for discovery: the store listing, the extensions page and the install dialog all show the manifest strings, and users searching in their own language find extensions whose names match.
1{
2 "appName": { "message": "Readable — reader mode", "description": "Extension name; 45 characters max in the store" },
3 "appDesc": { "message": "Clean, accessible reading view for any article.", "description": "132 characters max" }
4}
Execution context: _locales/en/messages.json, read by the browser when it parses the manifest. A missing key in a non-default locale falls back to default_locale; a missing default_locale with a _locales directory present makes the manifest fail to load. The description field inside each message is for translators and is never shown to users. The full workflow is in translating manifest fields and store listings.
8. Keys to stop using
Some keys survive in tutorials and templates long after they stopped meaning anything. They rarely break the build — Chrome warns and moves on — but they confuse readers of the manifest and occasionally reviewers.
The usual suspects are background.persistent and background.page (no MV3 meaning in Chrome), browser_action and page_action (renamed to action), a string-valued content_security_policy (must be an object), options_page alongside options_ui (pick one; options_ui is preferred), and offline_enabled, minimum_opera_version or update_url in store-distributed packages — update_url in particular is set by the store and must not be in packages you upload, though self-hosted enterprise builds still use it.
Cross-cutting concerns: permissions, CSP and review
The manifest is what store reviewers read first. Every permission must map to a feature described in the listing, every host pattern should be the narrowest that works, and the CSP must not attempt to loosen MV3’s minimum. Keys that expose the extension — web_accessible_resources, externally_connectable — are increasingly examined for over-broad matches, because they enable fingerprinting and cross-site attacks against the extension. A manifest that reads as deliberate and minimal shortens review.
Reading a manifest you inherited
Teams often inherit an extension whose manifest grew by accretion. Reading it top to bottom with three questions in mind finds most problems in minutes: is every permission used by code that still exists, is every exposed resource or origin still needed by a live feature, and does every compatibility value reflect a deliberate decision rather than a copied template? Write the answers into the comments of an annotated copy like the one above, and the next person to touch the file inherits the reasoning instead of the mystery.
MV3 constraints box
- One background file.
background.service_workertakes a single file in Chrome; import the rest as modules. - CSP can only tighten.
extension_pagesmust include at leastscript-src 'self'; only'wasm-unsafe-eval'may be added. web_accessible_resourcesneedsmatchesorextension_ids. The MV2 string-array form is rejected.- Hosts live in
host_permissions. Host patterns inpermissionsare rejected in MV3. - Version format is strict. One to four dot-separated integers; no letters, no leading zeros beyond a single
0. keyis for development. Do not ship it in store packages.
Cross-browser notes
| Key | Chrome / Edge | Firefox | Safari |
|---|---|---|---|
background.service_worker | Required form | Supported in recent versions; scripts preferred | Supported |
side_panel | Yes | Ignored (sidebar_action instead) | Ignored |
minimum_chrome_version | Enforced | Ignored | Ignored |
browser_specific_settings.gecko.id | Ignored | Required for signing | Ignored |
use_dynamic_url | Yes | Ignored (Firefox ids are already random per install) | Ignored |
key | Pins unpacked id | Ignored (use gecko.id) | Ignored |
What this section covers
The guides take one troublesome key each: pinning a stable extension id with the key field, declaring web accessible resources correctly, setting minimum browser versions, browser-specific settings for Firefox and Safari, icon sizes for the manifest and action, and externally connectable matches and ids.
Keys with their own topics are covered there: permissions in host permissions and site access, CSP in extension security and CSP hardening, commands in keyboard shortcuts and commands, and the omnibox in omnibox and address bar integration.
Related
- Manifest V2 to V3 migration — keys that changed between versions.
- Build tooling and bundlers — generating manifests per browser.
- Host permissions and site access — the access keys in depth.
- Store submission and permissions compliance — how reviewers read the manifest.
- Manifest V3 Architecture & Extension Lifecycle — the parent section.