reference guide

The Complete Manifest V3 Reference for Chrome Extensions

Every Manifest V3 field documented with examples, valid values, common mistakes, and gotchas. The reference Google's docs should be but aren't.

By Udaya PrakashUpdated June 8, 202624 min read

The manifest.json file is the contract between your Chrome extension and Chrome itself. Chrome reads it before doing anything else, and every behavior of your extension descends from what's declared here. The format is straightforward JSON, but the details are exacting: a missing field can prevent loading, a typo in a permission name silently disables functionality, and Chrome's own documentation is scattered across three different sites.

This reference documents every field in Manifest V3 with the actual format, valid values, common mistakes, and the context for when you'd actually use it. The fields are grouped by purpose, not alphabetical order — required fields first, then execution declarations, then UI, then everything else.

For the broader context of how Chrome extensions work, see the complete guide. For documentation of the chrome.* APIs your extension calls at runtime, see the APIs reference.

The minimum valid manifest

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0"
}

That's it. Three fields. Chrome will load this — it does nothing because no scripts or UI are declared, but it loads. Every other field below is added when your extension needs the functionality the field unlocks.

Required fields

manifest_version

Type: Integer Required: Yes Valid values: 3

The version of the manifest format. Must be 3. Chrome stopped accepting V2 extensions in the Web Store in 2024, and the Chrome browser itself stopped running V2 extensions in 2025 (depending on enterprise policy).

"manifest_version": 3

Do not use "3" (string) — must be the number 3.

name

Type: String Required: Yes Max length: 75 characters Localizable: Yes

The human-readable name of the extension. Shown in Chrome's extension list, in the Web Store, and in install prompts. This is the user-facing name and matters significantly for discoverability.

"name": "Save This"

For localized extensions, use the __MSG_extensionName__ pattern and define translations in _locales/[lang]/messages.json:

"name": "__MSG_extensionName__"

Naming guidelines: keep it short, avoid generic terms ("AI Assistant" is a Web Store policy violation), don't include version numbers or platform names ("for Chrome" is implied).

version

Type: String Required: Yes Format: Up to four dot-separated integers (e.g., 1.0.0 or 1.0.0.1)

The version of your extension. Chrome rejects same-or-lower version numbers when updating, so this must always increase. Use semver: MAJOR.MINOR.PATCH.

"version": "1.2.3"

Common mistakes:

  • Using a string like "1.0-beta" — only digits and dots are allowed
  • Using a leading zero like "01.0.0" — Chrome parses as 1.0.0 and may reject as same version
  • Skipping the patch number — Chrome may parse "1.0" ambiguously

version_name

Type: String Required: No Max length: 75 characters

An optional human-readable version string shown in the extensions UI alongside version. Useful for marketing-friendly version names like "Spring 2026 Update" while keeping version numeric.

"version": "1.2.3",
"version_name": "1.2.3 — Spring 2026 Update"

Description and identity

description

Type: String Required: For Web Store submission, no for development Max length: 132 characters Localizable: Yes

A short description of what the extension does. Shown in the Chrome extensions page, the Web Store listing summary, and install prompts. The first 132 characters of your Web Store description are derived from this.

"description": "Save any webpage to a quick-access list. Works on every site, no account required."

Writing tips:

  • Lead with what the extension does, not who it's for
  • Avoid superlatives ("best", "fastest") — Web Store reviewers sometimes flag these
  • No emojis (rejected by some Web Store reviewers as unprofessional)

author

Type: String Required: No

The author of the extension. Mostly informational and not surfaced in most UI.

"author": "PlugThis Team"

homepage_url

Type: String (URL) Required: No

A link to your homepage. Appears in Chrome's extension UI as a "Visit website" link when users view extension details.

"homepage_url": "https://plugthis.ai"

icons

Type: Object mapping size strings to image paths Required: Required for Web Store submission Recommended sizes: 16, 32, 48, 128

Icons used throughout Chrome's UI: the extensions page, the install prompt, the Web Store listing, and the "manage extensions" view. Each size has a specific use:

  • 16x16 — favicons, extension menu items
  • 32x32 — Windows install confirmation
  • 48x48 — extensions page
  • 128x128 — Web Store listing, install dialog
"icons": {
  "16": "icons/icon-16.png",
  "32": "icons/icon-32.png",
  "48": "icons/icon-48.png",
  "128": "icons/icon-128.png"
}

Format requirements: PNG with alpha transparency. SVG is not supported (despite Chrome's docs hinting otherwise — it doesn't work in production).

A common production mistake is providing only one icon size. Chrome will scale it for other sizes, but the result looks blurry and unprofessional. Always provide all four.

Permissions and access

permissions

Type: Array of strings Required: No (but most extensions need at least one)

API permissions your extension requires. Each permission corresponds to a chrome.* API. Asking for permissions you don't use is a Web Store policy violation and a red flag for reviewers.

"permissions": [
  "storage",
  "activeTab",
  "scripting",
  "notifications"
]

The full set of permissions is large (50+). The most common:

  • storage — chrome.storage APIs for persistent data
  • activeTab — temporary access to the current tab when user clicks the extension
  • tabs — full tabs API (read all tabs, switch, create, close)
  • scripting — chrome.scripting for programmatic content injection
  • notifications — desktop notifications
  • contextMenus — add items to the right-click menu
  • cookies — read/write browser cookies (for declared host_permissions)
  • bookmarks — read/modify bookmarks
  • history — read/modify browsing history
  • downloads — manage downloads
  • clipboardRead — read from clipboard
  • clipboardWrite — write to clipboard
  • alarms — schedule periodic tasks
  • webNavigation — observe page navigation events
  • webRequest — observe network requests (NOT block them — see declarativeNetRequest)
  • declarativeNetRequest — rules-based request blocking/modification
  • identity — OAuth flow for Google accounts
  • offscreen — offscreen document API for DOM access in background contexts
  • unlimitedStorage — bypass the default chrome.storage.local quota

Each permission has implications for the Web Store install prompt. Users see human-readable warnings like "Read your browsing history" (for tabs) or "Modify data you copy and paste" (for clipboardWrite). Higher-risk permissions reduce install rates.

optional_permissions

Type: Array of strings Required: No

Permissions your extension can request at runtime but doesn't require at install time. The extension asks for these via chrome.permissions.request() only when needed.

"optional_permissions": [
  "history",
  "bookmarks"
]

Best practice: use optional permissions for features that are nice-to-have but not core. The extension installs cleanly with the minimum permission set, and users grant additional permissions only if they actually use the relevant features. This significantly improves install rates.

host_permissions

Type: Array of URL match patterns Required: No (required for content_scripts and cross-origin fetches in most cases)

URL patterns where your extension can run content scripts, make cross-origin network requests, and intercept network traffic.

"host_permissions": [
  "https://*.example.com/*",
  "https://api.openai.com/*"
]

URL match pattern syntax:

  • <all_urls> — every URL (heaviest review burden)
  • *://*/* — every URL on http or https
  • https://*/* — every HTTPS URL
  • https://example.com/* — specific domain, any path
  • https://*.example.com/* — domain and all subdomains
  • https://example.com/specific-path/* — specific domain and path prefix

Common mistakes:

  • Using * alone instead of <all_urls> (rejected as invalid)
  • Forgetting the trailing /* (https://example.com won't match — needs to be https://example.com/*)
  • Using http:// when you only need HTTPS (broadens permission unnecessarily)

A note on Web Store review: <all_urls> triggers the strictest review level. If your extension only needs to work on a few specific sites, list them explicitly. Reviewers consistently push back on broad host permissions where narrower ones would work.

optional_host_permissions

Type: Array of URL match patterns Required: No

Host permissions your extension can request at runtime. Same syntax as host_permissions, but the extension installs without them and prompts the user when needed.

"optional_host_permissions": [
  "https://*.example.com/*"
]

Useful for extensions that work on a core set of sites by default but optionally expand. Users who only need the core experience get a cleaner install prompt.

externally_connectable

Type: Object Required: No

Declares which other extensions or websites can send messages to your extension via chrome.runtime.sendMessage. Default: only other parts of your own extension.

"externally_connectable": {
  "matches": ["https://plugthis.ai/*"],
  "ids": ["abcdef0123456789abcdef0123456789"]
}

matches allows websites at those URLs to message your extension. ids allows specific other extensions (by their Chrome Web Store ID) to message yours. Use this only when you need a deliberate cross-extension or web-to-extension integration.

Execution declarations

background

Type: Object Required: No

Declares the background service worker. In Manifest V3, there's no persistent background page — only a service worker that Chrome can terminate and restart as needed.

"background": {
  "service_worker": "background.js",
  "type": "module"
}

service_worker is the path to your background script, relative to the extension root. The path must point to an existing file.

type is optional. Set to "module" if your background script uses ES modules (import/export). If omitted, the script runs as a classic script and import won't work.

Common confusion: there's no persistent flag anymore. In V2 you could declare a persistent background page that never died. V3 service workers always have a lifecycle controlled by Chrome — they wake on events, run, and idle out. You cannot make them persistent. Adapt your code to be event-driven.

content_scripts

Type: Array of content script declarations Required: No

Scripts and stylesheets to inject into webpages matching specified URL patterns.

"content_scripts": [
  {
    "matches": ["https://*.example.com/*"],
    "exclude_matches": ["https://example.com/admin/*"],
    "js": ["content.js"],
    "css": ["content.css"],
    "run_at": "document_idle",
    "all_frames": false,
    "match_about_blank": false,
    "world": "ISOLATED"
  }
]

Each entry's fields:

  • matches — required. URL patterns where the script injects. Same syntax as host_permissions.
  • exclude_matches — optional. URL patterns to exclude from matches.
  • js — optional. Array of script paths. Scripts inject in order.
  • css — optional. Array of stylesheet paths.
  • run_at — optional. When to inject. Values: document_start (before any DOM), document_end (after DOM is parsed, before resources load), document_idle (default — after page is mostly idle).
  • all_frames — optional, default false. If true, injects into all iframes too.
  • match_about_blank — optional, default false. If true, injects into about:blank pages opened from matched origins.
  • world — optional. Either ISOLATED (default — sandboxed from page scripts) or MAIN (runs in the page's own JavaScript context, can interact with page variables).

Multiple content script entries are allowed. Common pattern: one entry for the main extension behavior, another for specific sites with custom logic.

Gotchas:

  • Content scripts have limited Chrome API access. They can use chrome.storage, chrome.runtime (for messaging), and a few others. Most APIs (like chrome.tabs) are only available in the background worker.
  • The world: "MAIN" mode is powerful but rarely needed. Use it only when you need to call functions defined by the page's own scripts.
  • Content scripts run on a per-page basis. If your script keeps state in a global variable, that state is per-page, not shared across tabs.

action

Type: Object Required: Recommended for any extension with a toolbar presence

Declares the extension's toolbar icon and what happens when the user clicks it.

"action": {
  "default_title": "Save This",
  "default_popup": "popup.html",
  "default_icon": {
    "16": "icons/icon-16.png",
    "32": "icons/icon-32.png",
    "48": "icons/icon-48.png",
    "128": "icons/icon-128.png"
  }
}

default_title — the tooltip shown when hovering the icon default_popup — path to an HTML file that opens when the user clicks the icon. If omitted, the click fires chrome.action.onClicked in the background worker instead. default_icon — the toolbar icon, in multiple sizes

If you don't declare default_popup, the toolbar icon click sends an event to the background worker, where you can handle it programmatically (open a new tab, run a script, send a notification, etc.).

A common pattern: declare a popup for most users, but enable a "quick action" mode where the icon directly triggers behavior. To switch modes at runtime, use chrome.action.setPopup({ popup: '' }) to disable the popup and use chrome.action.onClicked instead.

options_ui

Type: Object Required: No

Declares an options/settings page accessible from the extensions list.

"options_ui": {
  "page": "options.html",
  "open_in_tab": true
}

page — path to the HTML file open_in_tab — if true, opens in a full tab. If false (default), opens in a modal-style embedded panel that's smaller and more constrained.

Strong recommendation: always use open_in_tab: true unless your settings are extremely minimal. The embedded panel is too small for serious settings UI.

chrome_url_overrides

Type: Object Required: No

Lets your extension replace one of Chrome's built-in pages.

"chrome_url_overrides": {
  "newtab": "newtab.html"
}

Available overrides:

  • newtab — replaces the new-tab page
  • bookmarks — replaces the bookmarks manager
  • history — replaces the browsing history page

Only one extension can override each page at a time. If the user has multiple extensions trying to override the same page, Chrome picks one (usually the most recently installed) and shows a warning.

This is a high-friction permission with users — overriding newtab is the most common and is what every "productivity new tab" extension does. Be sure the override adds clear value or users will uninstall quickly.

commands

Type: Object mapping command names to definitions Required: No

Declares keyboard shortcuts the user can configure.

"commands": {
  "save-page": {
    "suggested_key": {
      "default": "Ctrl+Shift+S",
      "mac": "Command+Shift+S"
    },
    "description": "Save the current page",
    "global": false
  },
  "_execute_action": {
    "suggested_key": {
      "default": "Ctrl+Shift+E"
    }
  }
}

suggested_key — default keybinding (users can change it in chrome://extensions/shortcuts) description — shown in Chrome's shortcuts UI global — if true, the shortcut works even when Chrome isn't focused

The special command _execute_action triggers the extension's action (opens the popup or fires onClicked). Other named commands fire chrome.commands.onCommand in the background worker.

Limit: Chrome allows at most 4 suggested keys per extension (Chrome enforces this at install — extensions with more get the extras unconfigured by default). Users can manually add more shortcuts in chrome://extensions/shortcuts.

omnibox

Type: Object Required: No

Registers a custom keyword for Chrome's address bar. When the user types the keyword followed by a space, the rest of what they type is sent to your extension as a search query.

"omnibox": {
  "keyword": "save"
}

After this, typing save my-query in the address bar fires chrome.omnibox.onInputChanged and chrome.omnibox.onInputEntered in your background worker. Useful for power-user extensions that act as search interfaces or command palettes.

Web-accessible resources

web_accessible_resources

Type: Array of resource declarations Required: No (required to expose any extension files to webpages or other extensions)

Declares which files in your extension can be loaded by webpages or other extensions. By default, all extension files are private — webpages cannot embed them as images, scripts, or via fetch. Each entry whitelists specific files for specific origins.

"web_accessible_resources": [
  {
    "resources": ["images/overlay.png", "fonts/*.woff2"],
    "matches": ["https://*.example.com/*"]
  },
  {
    "resources": ["injected-script.js"],
    "matches": ["<all_urls>"],
    "use_dynamic_url": true
  }
]

Each entry has:

  • resources — array of file paths (supports wildcards)
  • matches — URL patterns of pages that can load these resources
  • extension_ids — optional, array of other extension IDs that can load these resources
  • use_dynamic_url — optional. If true, the resource URL includes a per-session random token, making it harder for sites to fingerprint your extension's installation

Common use case: a content script that needs to inject an image or stylesheet from your extension into the page. The page can only load extension resources that are listed here.

Less common but important: injecting a script into the page's own world: "MAIN" context using chrome.scripting.executeScript from a content script requires the injected file to be web-accessible.

Networking and data

content_security_policy

Type: Object Required: No (Chrome has a strict default)

Customizes the Content Security Policy for different parts of your extension. Manifest V3 has a strict default CSP that blocks remote code execution; you can sometimes loosen it for specific resources but never to the point of allowing eval or remote scripts.

"content_security_policy": {
  "extension_pages": "script-src 'self'; object-src 'self'",
  "sandbox": "sandbox allow-scripts; script-src 'self' https://example.com"
}

extension_pages — CSP for your popup, options, and other extension HTML pages sandbox — CSP for sandboxed pages (declared separately via sandbox field)

Almost all extensions can leave this field unset. Custom CSP is only needed for unusual cases like loading remote configuration JSON or running third-party libraries that need specific allowances.

Important: in V3 you cannot use 'unsafe-eval', cannot load remote scripts, and cannot use 'unsafe-inline' for scripts. These are hard restrictions, not defaults you can override. If your extension needs to run remote code, redesign it.

declarative_net_request

Type: Object Required: No

Declares static rules for blocking, redirecting, or modifying network requests. This is the V3 replacement for blocking webRequest.

"declarative_net_request": {
  "rule_resources": [
    {
      "id": "block_ads",
      "enabled": true,
      "path": "rules/ad-rules.json"
    }
  ]
}

Each rule resource references a JSON file containing the actual rules. Rules can:

  • Block matching requests
  • Redirect matching requests
  • Modify request or response headers
  • Upgrade insecure requests

Rule limits: up to 30,000 static rules per ruleset, 100 rulesets total per extension, 5,000 dynamic rules at runtime. These limits are why content blockers complained about V3.

Most extensions don't need this. It's primarily for ad blockers, tracker blockers, privacy tools, and certain security extensions.

sandbox

Type: Object Required: No

Declares HTML pages that run in a sandboxed origin without Chrome extension privileges. Used for safely running third-party libraries or untrusted code (like user-provided JavaScript expressions).

"sandbox": {
  "pages": ["sandboxed-eval.html"]
}

Sandboxed pages cannot access chrome.* APIs and run in a unique origin. They communicate with the rest of your extension via postMessage. Useful for extensions that evaluate user-defined expressions (calculators, formula parsers, query builders).

offscreen

The offscreen API isn't declared in the manifest — it's used at runtime via chrome.offscreen.createDocument(). But it requires the offscreen permission.

Useful when your background worker needs DOM access (parsing HTML, working with audio/video, certain crypto operations). The worker creates an offscreen document, communicates with it via messaging, and tears it down when done.

Extension identity

key

Type: String (base64-encoded public key) Required: No (auto-assigned by Chrome on Web Store publish)

Sets a fixed extension ID for development. Without this, your extension gets a randomly-generated ID each time you load it as unpacked, which breaks anything that depends on the ID (like externally_connectable allowlists or OAuth client IDs registered to a specific ID).

"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."

How to generate one: pack your extension once via chrome://extensions/ > "Pack extension". This creates a .crx and a .pem file. Run a small Node script (or use online tools) to convert the .pem to a manifest key. The resulting key locks your unpacked extension to a specific ID.

Most extensions don't need this — but if you're integrating with services that whitelist your extension ID (OAuth, externally_connectable), you'll need it.

minimum_chrome_version

Type: String (Chrome version like "100.0") Required: No

The minimum Chrome version required to run your extension. Useful if you depend on a recently-added API.

"minimum_chrome_version": "108.0"

Users on older versions see a clear "your browser is too old" message instead of cryptic errors.

update_url

Type: String (URL) Required: No (auto-set by Chrome Web Store)

The URL Chrome polls to check for extension updates. Set automatically by the Chrome Web Store when you publish there. Custom update URLs are used for self-hosted extensions (mostly enterprise).

"update_url": "https://your-update-server.com/updates.xml"

Self-hosting updates is rarely worth the complexity. Use the Chrome Web Store unless you have a specific enterprise reason not to.

Internationalization

default_locale

Type: String (locale code) Required: Required if using internationalization

The default locale for __MSG_*__ placeholders. Used when the user's preferred language isn't available.

"default_locale": "en"

Then create _locales/en/messages.json:

{
  "extensionName": {
    "message": "Save This",
    "description": "The name of the extension."
  },
  "extensionDescription": {
    "message": "Save any webpage to a quick-access list."
  }
}

And use the placeholders in manifest fields:

"name": "__MSG_extensionName__",
"description": "__MSG_extensionDescription__"

For each additional language, add _locales/[code]/messages.json. Common codes: en, es, fr, de, ja, zh_CN, pt_BR.

A translation strategy that works: ship in English only initially. Once you have users in other markets (visible in Chrome Web Store analytics), translate. Premature internationalization adds maintenance burden for minimal install lift.

URL match pattern syntax

URL match patterns appear in content_scripts.matches, host_permissions, externally_connectable.matches, and several other places. They have specific syntax that's worth understanding fully.

Pattern structure

A URL pattern has three parts: scheme, host, and path.

<scheme>://<host><path>
  • Schemehttp, https, file, ftp, or * (which matches http or https)
  • Host — exact hostname, or * (anything), or *.example.com (subdomain wildcard)
  • Path — exact path, or path with wildcards

Examples

PatternMatchesDoesn't match
https://example.com/*https://example.com/anythinghttp://example.com/anything (wrong scheme)
*://example.com/*http://example.com/x, https://example.com/yftp://example.com/x
https://*.example.com/*https://a.example.com/x, https://b.example.com/xhttps://example.com/x (no subdomain)
https://*/*every HTTPS URLhttp URLs
<all_urls>every URL of every schemenone

Special tokens

  • <all_urls> — equivalent to *://*/* plus file:// and others. Most permissive.
  • * in the host — matches any subdomain (and the root domain if combined with *.)
  • * in the path — matches any path
  • * in the scheme — matches http or https only

Common mistakes

Missing trailing /* in path. https://example.com is not a valid pattern — paths must include something. Use https://example.com/* to match all paths.

Using * to mean a different scheme. *://example.com/* matches http and https only, not other schemes like file or ftp.

Trying to match query strings. Patterns don't match query strings or fragments. https://example.com/?foo=bar is matched by https://example.com/* regardless of the query.

Wildcards in the middle of paths. https://example.com/users/*/profile works as you'd expect. Wildcards in arbitrary parts of a path are fine.

Subdomain wildcard at the root. https://*.com/* is rejected — too broad. *.com is not a valid wildcard target. Use https://*/* for "every HTTPS site."

Complete example — a real-world manifest

Here's a manifest for a hypothetical "Page Annotator" extension that highlights selected text and saves notes:

{
  "manifest_version": 3,
  "name": "Page Annotator",
  "version": "1.4.2",
  "version_name": "1.4.2 — Improved sync",
  "description": "Highlight and annotate any webpage. Notes sync across devices.",
  "author": "Acme Annotation Co.",
  "homepage_url": "https://annotator.acme.com",
  "default_locale": "en",

  "icons": {
    "16": "icons/icon-16.png",
    "32": "icons/icon-32.png",
    "48": "icons/icon-48.png",
    "128": "icons/icon-128.png"
  },

  "permissions": [
    "storage",
    "activeTab",
    "scripting",
    "contextMenus",
    "notifications"
  ],
  "optional_permissions": [
    "history"
  ],
  "host_permissions": [
    "https://*/*"
  ],

  "background": {
    "service_worker": "background.js",
    "type": "module"
  },

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "exclude_matches": ["https://*.bank-of-example.com/*"],
      "js": ["content/main.js"],
      "css": ["content/highlight.css"],
      "run_at": "document_idle"
    }
  ],

  "action": {
    "default_title": "Page Annotator",
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon-16.png",
      "48": "icons/icon-48.png",
      "128": "icons/icon-128.png"
    }
  },

  "options_ui": {
    "page": "options/options.html",
    "open_in_tab": true
  },

  "commands": {
    "toggle-highlight": {
      "suggested_key": {
        "default": "Ctrl+Shift+H",
        "mac": "Command+Shift+H"
      },
      "description": "Toggle highlight on current selection"
    },
    "_execute_action": {
      "suggested_key": {
        "default": "Ctrl+Shift+A",
        "mac": "Command+Shift+A"
      }
    }
  },

  "web_accessible_resources": [
    {
      "resources": ["fonts/*.woff2", "icons/cursor.svg"],
      "matches": ["<all_urls>"]
    }
  ],

  "minimum_chrome_version": "108.0"
}

This is a realistic manifest for a real extension. Note the patterns: minimum necessary permissions, narrow host permissions where possible, content scripts excluded from bank URLs, options page opens in a tab, two keyboard shortcuts, web-accessible fonts for the highlight UI.

Field deprecation notes

Several fields from Manifest V2 are gone in V3. If you're migrating from V2 docs or examples, watch for:

  • background.scripts — replaced by background.service_worker. Persistent background pages no longer exist.
  • background.persistent — removed. Background is always a service worker now.
  • browser_action and page_action — both replaced by a single action field.
  • web_accessible_resources as an array of strings — must now be an array of objects with resources and matches.
  • content_security_policy as a string — must now be an object with extension_pages and/or sandbox.
  • permissions with host patterns — host patterns now go in host_permissions, not permissions. The old V2 style is silently ignored in V3.
  • webRequest blocking — still allowed for observation but not for blocking. Use declarativeNetRequest to block.

If you find old extension tutorials online, check the date or the manifest version they show. V2 examples will not run as-is in V3.

Testing your manifest

Two free tools that catch most manifest issues:

Chrome's own loader. Load your extension as unpacked. Chrome shows specific error messages for invalid manifest fields. The error text is usually the fastest path to diagnosis.

JSONLint or a similar JSON validator. If your manifest won't parse at all, it's likely a JSON syntax issue (trailing comma, missing quote). JSONLint highlights the exact line.

For more advanced validation, the Chrome team maintains a JSON Schema for Manifest V3 at github.com/GoogleChrome/chrome-extensions-samples. Some IDEs can validate against it for live error checking as you type.

A useful debugging habit: keep a known-good minimal manifest in a separate file. When something breaks, compare your manifest to the minimum and add fields back one at a time until you find what introduced the issue.

What this reference doesn't cover

Two intentional omissions:

Native messaging. Chrome extensions can communicate with native applications via Native Messaging hosts. The manifest fields involved (nativeMessaging permission) are documented in Chrome's official docs. This is a niche feature used by password managers, IDEs, and a few security tools.

Chrome Apps fields. Chrome Apps (a separate extension category) was deprecated in 2018 and removed in 2022. Any manifest fields specific to Chrome Apps (app.background, kiosk_enabled, etc.) no longer work. If you see them in old documentation, ignore them.

Next steps

If you're writing extension code that calls the APIs declared in your manifest, see the Chrome extension APIs reference — every chrome.* API documented with examples.

If you're building your first Chrome extension and want the whole picture from idea to Web Store launch, the complete guide is the structured walkthrough.

And if writing the manifest by hand feels tedious, PlugThis generates the full manifest for you based on a plain-English description of what you want the extension to do. The output is exactly the kind of clean V3 manifest documented here.

Continue reading

Build Chrome extensions without writing this code yourself

Describe what you want. PlugThis generates the manifest, the scripts, and the packaged ZIP — ready to load.

Open the builder