The manifest
extension.json is the only required file in an extension. It declares
identity, what the extension contributes, what it is allowed to touch, and
when its code should run. This page covers the fields you write by hand and
the decisions behind them. For the exhaustive field-by-field table, including
the shape of every contribution type, see the
manifest reference.
The smallest manifest that works
Section titled “The smallest manifest that works”{ "name": "My Notes", "version": "1.0.0", "engines": { "wamp": "^12.0.0" }, "icon": "NotebookPen", "compat": { "pluginApi": "^1.0.0" }, "activationEvents": ["onStartupFinished"], "contributes": { "pages": [ { "id": "my-notes", "title": "Notes", "icon": "NotebookPen" } ] }}Only name and version are strictly required by the schema. Everything else
in that example is there because leaving it out costs you something later, and
each of those costs is explained below.
Note what is not there: there is no id field. An extension’s id is its
directory name, which must be lowercase kebab-case (^[a-z][a-z0-9-]*$).
name is the human label and can be anything.
Every top-level field
Section titled “Every top-level field”Twenty keys, and no others — the schema is the full list.
| Field | Required | Type |
|---|---|---|
name |
yes | string, non-empty — the human label |
version |
yes | strict MAJOR.MINOR.PATCH |
description |
no | string — one line, used in cards and search |
longDescription |
no | Markdown — the catalog detail page body |
engines |
no | { wamp: <semver range> } |
icon |
no | lucide-react icon name in PascalCase |
author |
no | string |
permissions |
no | array of permission strings and/or { "auth.outbound": [urls] } |
capabilities |
no | array of capability strings |
products |
no | array of product slugs (^[a-z0-9][a-z0-9-]*$) |
webRequestRewrite |
no | { stripCSP?, stripFrameOptions?, allowlist? } |
contributes |
no | the contribution block |
build |
no | { loaders: [{ match, loader }] } |
activationEvents |
no | array of activation-event strings |
dependencies |
no | array of extension ids |
requiredServices |
no | array of service ids |
main |
no | path to the built main bundle, e.g. "dist/main.js" |
server |
no | path to the built headless bundle, e.g. "dist/server.js" |
requiresElectron |
no | boolean |
compat |
no | { pluginApi: <semver range> } |
Identity and versioning
Section titled “Identity and versioning”version is strict
Section titled “version is strict”MAJOR.MINOR.PATCH, three numeric segments, nothing else. Pre-release tags
are rejected: 1.0.0-beta.1 and 2.1 both fail validation. If you version
with pre-release tags elsewhere, the manifest is the one place you cannot.
engines.wamp — optional to load, required to publish
Section titled “engines.wamp — optional to load, required to publish”This is the field most worth reading carefully, because its two behaviors differ.
The host treats engines.wamp as optional: an extension without it loads
normally, and the host does not compare the range against its own version at
install or activation time.
The marketplace treats it as required: an upload without engines.wamp is
rejected. The catalog reads the range unconditionally to populate every
listing’s compatibility field.
compat.pluginApi — the version check that does run
Section titled “compat.pluginApi — the version check that does run”This one is enforced, at registration. The host compares the declared range against its own plugin-API version and takes one of three paths:
| Declared | Result |
|---|---|
| Absent | Registers. No warning — the permissive default for manifests predating the field |
| Present and satisfied | Registers |
| Present and not satisfied | Refuses to register, faults with CompatMismatch, and surfaces an error naming both versions |
The current host plugin-API version is 1.2.0, so "^1.0.0" — what the
scaffold writes — is satisfied. A range with unrecognized syntax falls back to
exact string equality against the host version, so a typoed range does not
silently match everything; it silently matches nothing and blocks activation.
A lucide-react component name in PascalCase: NotebookPen, ListChecks,
Sparkles. A name that does not exist in the icon set produces no icon and no
error. Trash2, not Trash. Check the exact spelling against the icon set
before shipping.
Entry points
Section titled “Entry points”| Field | Set it when |
|---|---|
main |
The extension has code that runs outside the window — tools, cron jobs, HTTP routes, services |
server |
That code must also run on a core with no Electron in the process |
requiresElectron |
The main half genuinely needs Electron APIs |
main is also what drives the build: the main bundle is produced only when
main is set, and the renderer bundle only when contributes.pages or
contributes.views is non-empty. An extension with main in the manifest but
no main/activate.ts on disk builds nothing and registers as renderer-only —
no activation, dead commands, dead services.
When both main and server are present, exactly one activates per host, so
the same tool never registers twice. With requiresElectron: true and no
server, a headless core skips the extension at scan; the desktop host
ignores the flag entirely.
Activation
Section titled “Activation”{ "activationEvents": ["onCommand:notes.new", "onTool:notes_search"] }The narrower the events, the less the extension costs at startup. The
recognized forms are onStartupFinished, onCommand:<id>, onView:<id>,
onAgent:<id>, onTool:<name>, onMcpServer:<id>, onChatCommand:<command>,
and onProjectKind:<id>. * is rejected by the schema.
requiredServices is a second, independent gate: activation waits until every
listed service id is registered by some other extension. A missing service
parks the extension rather than faulting it, and it wakes when the service
arrives. dependencies names other extensions by id and is resolved
recursively at install.
permissions
Section titled “permissions”An array whose entries are either permission strings or the one structured form:
{ "permissions": [ "database", "cron", "notifications", { "auth.outbound": ["https://api.example.com"] } ]}The valid strings are database, network, cron, ai, terminal,
filesystem, http-routes, notifications, process, auth.identity, and
mcp. Which of them actually withhold a capability — and which are consent
signals the runtime does not yet enforce — is the whole subject of
Permissions. Read that page before you decide what to
declare; the two classes behave very differently when you get one wrong.
capabilities
Section titled “capabilities”A separate, narrower opt-in list, unrelated to permissions. Four values are
recognized:
| Capability | Unlocks |
|---|---|
webviews.navigate |
Navigation control on an embedded webview |
webviews.executeScript |
Script injection into an embedded webview |
webviews.interceptRequests |
Per-request inspection and rewriting |
webRequestRewrite |
Header rewriting declared via webRequestRewrite |
Declaring any of the three webviews.* capabilities also selects the
higher-powered backing implementation for ctx.api.webviews, so the choice is
not purely about permission — it changes what the webview is.
contributes
Section titled “contributes”Thirteen keys, all optional:
| Key | Declares |
|---|---|
pages |
Top-level surfaces with sidebar entries — see Pages and windows |
views |
Components mounted into a named slot other than the main workspace |
commands |
Command-palette entries, optionally with a keybinding |
agents |
Agent definitions — see Agents and skills |
skills |
true to load co-located skills/*.md, or explicit paths |
services |
Typed services other extensions can require |
settings |
User-editable settings with schema defaults |
toolMetadata |
Display and behavior hints for tools |
mcpServers |
MCP servers to register, over stdio, Streamable HTTP, or SSE |
agentRuntimes |
An external agent that can answer a chat turn |
cliHarness |
A hosted CLI coding harness |
projectTemplates |
One-shot new-project bootstrappers |
ipcNamespaces |
Namespace prefixes this extension claims for cross-process messaging |
The full shape of each is in the manifest reference. Three notes that catch people out:
A page’s context is optional. It takes 'global', 'project', or
'both', and omitting it means 'both' — the page appears everywhere. It is
not a required field.
ipcNamespaces collide loudly. Namespaces are claimed at registration,
before first use, so two extensions claiming the same prefix means the second
one faults with IpcNamespaceCollision and does not load. Prefix with your
extension id.
skills: true is the common case. It loads every *.md under the
extension’s skills/ directory. Pass a string or array of strings only when
you need to name files explicitly.
Validation is strict in some places and permissive in others
Section titled “Validation is strict in some places and permissive in others”This asymmetry is deliberate and it is worth knowing which way each object leans, because the two failure modes look nothing alike.
The manifest root is permissive. An unknown top-level key is accepted and
ignored. A typo like "activationEvent" (singular) validates cleanly and does
nothing, which reads exactly like a host bug.
These objects are strict — an unknown key fails the whole manifest with a
validation error naming the field: entries in contributes.commands,
contributes.pages, contributes.settings, contributes.toolMetadata,
contributes.cliHarness, contributes.agentRuntimes, the build block and
its loaders rules, and the structured auth.outbound permission.
So a typo inside a page contribution stops the extension from loading at all and tells you where; a typo one level up costs you an afternoon. When something you declared has no effect, check the spelling of the top-level key first.
build — loader overrides without a build script
Section titled “build — loader overrides without a build script”{ "build": { "loaders": [ { "match": "presets/**.svg", "loader": "text" } ] }}match is a glob relative to the extension root; loader is one of text,
json, base64, dataurl, binary. This exists so an extension with a
modest asset-handling need does not have to ship its own build script — which
matters because an extension the host builds itself stays editable inside WAMP,
and one with its own build script does not.
products
Section titled “products”{ "products": ["wamp", "forge"] }Which branded products list this extension in their catalog. Absent or empty means every catalog lists it. This is curation, not access control — the download route stays open regardless.
- Permissions — what each declaration actually unlocks.
- Manifest reference — every field, every contribution shape, exhaustively.