Skip to content

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.

{
"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.

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> }

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.

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.

{ "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.

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.

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.

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": ["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.