# The manifest What every field in extension.json means, which ones you set by hand, and the four places where getting one wrong fails quietly. Source: https://docs.vampikez.fun/build/manifest/ `extension.json` is the only required file in an extension. It declares identity, contributions, host API permissions and disclosures, and when its code should run. It does not sandbox the package: installing an extension means trusting its Node and UI code. 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](/reference/manifest/). ## The smallest manifest that works ```json { "name": "My Notes", "version": "1.0.0", "engines": { "wamp": "^12.0.0" }, "icon": "NotebookPen", "compat": { "pluginApi": "^4.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 These are the recognized keys. Unknown root keys are discarded; they do not extend the contract. | 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: }` | | `icon` | no | `lucide-react` icon name in PascalCase | | `author` | no | string | | `permissions` | no | array of permission strings and/or `{ "auth.outbound": [urls], "auth.resource": [{ audience, baseUrl }] }` | | `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 | | `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: }` | ## Identity and versioning ### `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 in the catalog 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. Catalog publication treats it as **required**: the upload pipeline rejects a package without `engines.wamp`. The catalog reads the range unconditionally to populate every listing's compatibility field. Because the host never asks for `engines.wamp`, an extension missing it runs, hot-reloads, and passes every local check. It is not eligible for catalog publication, whose upload pipeline rejects it. The current scaffold writes `"engines": { "wamp": "^12.0.0" }`. ### `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 `3.0.0`, so `"^4.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. ### `icon` A `lucide-react` component name in PascalCase: `NotebookPen`, `ListChecks`, `Sparkles`. Use an exact export name; an unknown name can leave the surface without its intended icon. ## Entry points | Field | Set it when | | --- | --- | | `main` | The extension has code that runs outside the window — tools, 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 supported source entry fails the SDK build with a missing-entry diagnostic. The SDK also builds a declared `server` from `server/activate.ts`, `src/server/activate.ts`, or `src/server/index.ts`, and rejects Electron imports in that bundle. When both `main` and `server` are present, exactly one activates per host, so the same tool never registers twice. An Electron-free `main` can run headless without a separate `server`. With `requiresElectron: true` and no `server`, a headless core skips the extension at scan. The Cloud browser hides its UI whenever `requiresElectron` is true, even when the server half is present. ## Activation ```json { "activationEvents": ["onCommand:notes.new", "onTool:notes_search"] } ``` The narrower the events, the less the extension costs at startup. The recognized forms are `onStartupFinished`, `onCommand:`, `onAgent:`, `onTool:`, `onMcpServer:`, and `onChatCommand:`. `*`, `onView:`, and `onProjectKind:` are rejected by the schema. An absent or empty `activationEvents` array is replaced with `["onStartupFinished"]`. The extension activates at boot. If you want lazy activation you have to name the event that should cause it. `dependencies` names other extensions by id and is resolved recursively at install. ## `permissions` An array whose entries are either permission strings or the one structured form: ```json { "permissions": [ "notifications", { "auth.outbound": ["https://api.example.com"] } ] } ``` The valid strings are `network`, `ai`, `terminal`, `filesystem`, `http-routes`, `notifications`, `process`, and `auth.identity`. Which strings actually withhold a capability — and which are disclosure labels the runtime does not enforce — is the whole subject of [Permissions](/build/permissions/). Read that page before you decide what to declare; the two classes behave very differently when you get one wrong, and neither class confines the extension's own JavaScript. ## `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*. The known-capability list is informational. An unrecognized string is accepted by the schema and never matches anything the host gates on — so `"webview.navigate"` (singular) validates fine and leaves you with a webview that cannot navigate, with no error to trace. ## `contributes` All contribution keys are optional: | Key | Declares | | --- | --- | | `pages` | Top-level surfaces with sidebar entries — see [Pages and windows](/build/pages-and-windows/) | | `views` | Components mounted into a named slot other than the main workspace | | `commands` | Command-palette entries, optionally with a keybinding | | `skills` | `true` to scan co-located `skills/` for directory packages, or explicit directories | | `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 | | `ipcNamespaces` | Namespace prefixes this extension claims for cross-process messaging | The full shape of each is in the [manifest reference](/reference/manifest/). Three notes that catch people out: **A page's `context` is optional.** It takes `'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 scans the extension's `skills/` directory for portable `/SKILL.md` packages, including their resources. A loose `.md` file in `skills/` is not a skill package and produces a load diagnostic. Pass a string or array of strings only when you need to name skill directories explicitly. ## 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.agentRuntimes`, the `build` block and its `loaders` rules, and the structured `auth.outbound` / `auth.resource` permission with its `auth.resource` entries. 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` — public builder loader overrides ```json { "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 can stay on the standard SDK builder without custom esbuild plumbing. It applies in `wamp ext dev` and `wamp ext pack`. ## `products` ```json { "products": ["wamp", "ledger"] } ``` 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. ## Next - [Permissions](/build/permissions/) — what each declaration actually unlocks. - [Manifest reference](/reference/manifest/) — every field, every contribution shape, exhaustively.