Manifest reference
This page is the exhaustive field list for extension.json, derived from the
schema the host validates against. Use it to look up a type, a default, or the
full value set of an enum. For the reasoning behind the choices — which fields
you actually need and which ones fail quietly — read
The manifest first.
Where it lives, and where the id comes from
Section titled “Where it lives, and where the id comes from”extension.json sits in the extension’s root directory, beside main/, ui/,
skills/, and dist/. It is the only required file: a directory with UI or
main code but no manifest is reported as a failed scan.
The extension’s id is the directory name, not a manifest field. There is no
id key. The directory name must be lowercase kebab-case (^[a-z][a-z0-9-]*$)
because every id-shaped field elsewhere — dependencies, catalog slugs — is
validated against that pattern.
Top-level fields
Section titled “Top-level fields”Twenty keys. The root object is permissive: an unknown top-level key is accepted and ignored rather than failing validation.
| Field | Type | Required | Default | Constraint |
|---|---|---|---|---|
name |
string | yes | — | non-empty |
version |
string | yes | — | ^\d+\.\d+\.\d+$ — no pre-release tags |
description |
string | no | — | — |
longDescription |
string | no | — | Markdown |
engines |
{ wamp: string } |
no | — | wamp non-empty; a semver range |
icon |
string | no | — | a lucide-react export name |
author |
string | no | — | — |
permissions |
array | no | [] |
see Permissions |
capabilities |
string[] | no | [] |
unknown strings accepted |
products |
string[] | no | — | each ^[a-z0-9][a-z0-9-]*$ |
webRequestRewrite |
object | no | — | see webRequestRewrite |
contributes |
object | no | — | see contributes |
build |
object | no | — | strict; see build |
activationEvents |
string[] | no | ["onStartupFinished"] |
see Activation events |
dependencies |
string[] | no | — | each ^[a-z][a-z0-9-]*$ |
requiredServices |
string[] | no | — | each non-empty |
main |
string | no | — | path relative to the extension root |
server |
string | no | — | path relative to the extension root |
requiresElectron |
boolean | no | false |
— |
compat |
{ pluginApi: string } |
no | — | pluginApi non-empty; a semver range |
What each one controls:
| Field | Controls |
|---|---|
name |
The human label in the sidebar, the catalog card, and search |
version |
Update comparison at install; the version reported to the registry |
description |
One-line summary on catalog cards, in search results, and in the detail hero |
longDescription |
The Markdown body of the catalog detail page |
engines.wamp |
Host-version compatibility as displayed in the catalog; required for marketplace upload, not checked at load |
icon |
The sidebar and catalog icon |
author |
Attribution shown in the catalog |
permissions |
Which ctx.api namespaces exist, and what the install consent screen lists |
capabilities |
Call-time gates on ctx.api.webviews, and which webview backing is used |
products |
Which branded catalogs list the extension. Absent or empty means all |
webRequestRewrite |
Default header rewriting for webviews this extension creates |
contributes |
Everything declarative: pages, views, commands, agents, skills, settings, servers |
build |
Per-path loader overrides for the host build |
activationEvents |
When the main module is loaded and activate() runs |
dependencies |
Other extensions installed recursively alongside this one |
requiredServices |
Service ids that must be registered before activation proceeds |
main |
Entry point of the host/Electron-side module |
server |
Entry point of the headless (electron-free) module |
requiresElectron |
A headless core skips this extension at scan unless server is also declared |
compat.pluginApi |
Plugin-API range checked against the host at registration; a mismatch refuses activation |
The two version fields behave differently
Section titled “The two version fields behave differently”This is the asymmetry worth memorizing.
engines.wamp |
compat.pluginApi |
|
|---|---|---|
| Checked at load | no | yes |
| Absent | loads | registers, no warning |
| Present and satisfied | loads | registers |
| Present and unsatisfied | loads anyway | refuses to activate |
| Required to publish | yes | no |
| Where the value shows up | the catalog listing’s compatibility field | nowhere user-facing |
The host’s current plugin-API version is 1.2.0. A range with syntax the host
cannot parse falls back to exact string comparison against that version, so a
typo blocks activation rather than matching everything.
A legacy top-level key is still accepted
Section titled “A legacy top-level key is still accepted”A manifest carrying ui.toolbar or ui.page — the shape older scaffolds wrote
— is rewritten before validation into a single contributes.pages[] entry whose
id is the extension id, titled from ui.toolbar.label (falling back to name,
then the id) and iconed from ui.toolbar.icon (falling back to the top-level
icon). The load logs a deprecation warning. Write contributes.pages[]
directly; nothing else about ui is read.
Permissions
Section titled “Permissions”permissions is an array whose entries are either a string from the closed set
below, or the one structured form.
| Permission | What declaring it does |
|---|---|
database |
Adds ctx.api.cloud — the app’s own document store. Re-checked in the host for renderer calls |
cron |
Adds ctx.api.cron |
http-routes |
Adds ctx.api.http |
notifications |
Adds ctx.api.notifications |
process |
Adds ctx.api.process, ctx.api.pty, and ctx.api.git |
auth.identity |
Adds ctx.api.auth (getSession, fetch, onChange) |
mcp |
Allows pluginAPI.mcp.listServers / setEnabled, enforced in the host |
ai |
Consent signal only — ctx.api.ai is present either way |
network |
Consent signal only |
terminal |
Consent signal only |
filesystem |
Consent signal only — ctx.api.fs is present either way |
The structured form:
{ "permissions": [{ "auth.outbound": ["https://api.example.com"] }] }auth.outbound takes an array of absolute URLs — each entry must parse as a
URL or the manifest fails validation. It declares the origins
ctx.api.auth.fetch may target; other origins are refused at the host proxy.
The object is strict: any key other than auth.outbound fails validation.
Note that database gates cloud, not data. The local typed store
(ctx.api.data) is present with no permission at all. See
Permissions for the enforcement model and
Plugin API reference for the per-namespace table.
Capabilities
Section titled “Capabilities”capabilities is a separate list, unrelated to permissions. Four strings are
recognized:
| Capability | Gates |
|---|---|
webviews.navigate |
Navigation control on a webview handle |
webviews.executeScript |
Script injection into a webview |
webviews.interceptRequests |
Per-request inspection and rewriting |
webRequestRewrite |
Header rewriting declared via the webRequestRewrite field |
Declaring any of the three webviews.* values also selects the
WebContentsView backing for ctx.api.webviews; without one of them the
webview uses the sandboxed-iframe backing.
The allowlist is informational. An unrecognized string validates and then matches nothing the host gates on, so a misspelling produces a capability that is silently never granted.
Activation events
Section titled “Activation events”Every entry must match one of eight forms. The literal * is rejected.
| Form | Fires when |
|---|---|
onStartupFinished |
The renderer has settled after launch |
onCommand:<id> |
A contributed command with that id is invoked |
onView:<id> |
A view with that id is about to mount |
onAgent:<id> |
An agent with that id is about to run |
onTool:<name> |
A tool with that name is about to execute |
onMcpServer:<id> |
An MCP server with that id is starting |
onChatCommand:<command> |
That chat command is typed |
onProjectKind:<id> |
A project of that kind is opened |
The id segment accepts [\w.:-]+. An absent or empty array is replaced with
["onStartupFinished"] for any extension that declares main, so omitting the
field means eager activation, not lazy.
webRequestRewrite
Section titled “webRequestRewrite”Default header rewriting applied to webviews this extension creates. All three fields are optional.
| Field | Type | Effect |
|---|---|---|
stripCSP |
boolean | Removes Content-Security-Policy response headers |
stripFrameOptions |
boolean | Removes X-Frame-Options response headers |
allowlist |
string[] | Restricts the rewrite to these hosts |
Using it requires the webRequestRewrite capability.
Per-path loader overrides for the host build, so an extension with a modest
asset need stays host-built — and therefore editable inside WAMP — instead of
shipping its own build script. The build object and each rule are strict.
| Field | Type | Required | Constraint |
|---|---|---|---|
loaders |
array | no | — |
loaders[].match |
string | yes | non-empty glob, relative to the extension root |
loaders[].loader |
enum | yes | text | json | base64 | dataurl | binary |
{ "build": { "loaders": [{ "match": "presets/**.svg", "loader": "text" }] } }contributes
Section titled “contributes”Thirteen keys, every one optional. The contributes object itself is
permissive; strictness varies per contribution type and is tabulated in
Strict and permissive objects.
| Key | Type |
|---|---|
pages |
array |
views |
array |
commands |
array |
agents |
array |
skills |
boolean, string, or string[] |
services |
array |
settings |
array |
toolMetadata |
array |
mcpServers |
array |
projectTemplates |
array |
cliHarness |
array |
agentRuntimes |
array |
ipcNamespaces |
string[] |
contributes.pages
Section titled “contributes.pages”A page is a top-level workspace surface plus a sidebar entry. Strict — an unknown key fails the manifest.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id |
string | yes | — | non-empty; the bundle’s views[<id>] export is auto-registered into workspace.main with type: <id> |
title |
string | yes | — | non-empty; the sidebar label |
icon |
string | no | — | lucide-react export name |
context |
enum | no | both |
global | project | both |
presentation |
enum | no | docked |
docked | app |
placement |
enum | no | top |
docked pages only: top | footer |
context is normalized to both at parse time, so every consumer reads a
concrete value. presentation: 'app' hides the WAMP chrome and lets the page
render its own via AppShell; placement: 'footer' puts the sidebar row in the
fixed bottom group instead of the reorderable list. See
Pages and windows.
Do not also declare a views entry for the page’s own surface — the page id
already registers one.
contributes.views
Section titled “contributes.views”A component mounted into a named slot. Use this only for slots other than the
page’s own workspace.main surface.
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | non-empty; must match a key of the bundle’s views export |
slot |
enum | yes | one of the seven slots below |
type |
string | no | discriminator within workspace.main |
title |
string | no | label where the slot shows one |
icon |
string | no | lucide-react export name |
Every ViewSlot value, the props its component receives, and whether a host
renders it today:
| Slot | Props | Renders today |
|---|---|---|
workspace.main |
{ paneId: string; type: string } |
yes — the main workspace pane |
session.dock |
SessionDockContext (below) |
yes — the session dock strip |
statusbar.left |
{ compact: boolean } |
yes, but inside a popover |
statusbar.right |
{ compact: boolean } |
yes, but inside the same popover |
chatInput.attachments |
{ draftText: string; onInsert(text): void; onAttachmentRemoved(id): void } |
yes — below the chat composer |
sidebar.primary |
{ collapsed: boolean; onRequestExpand(): void } |
no host — the value validates and nothing mounts |
overlay.global |
{ viewport: { width: number; height: number } } |
no host — the value validates and nothing mounts |
The two statusbar slots are collected into one overflow popover behind a single sidebar trigger, in left-then-right order. The left/right distinction has no visual effect today.
SessionDockContext, the props a session.dock tool receives:
| Field | Type | Notes |
|---|---|---|
sessionId |
string | null |
null on the new-session surface, before a record exists |
context |
SessionContext | undefined |
undefined until a project is chosen |
hydrated |
boolean | false while the workspace is still loading |
activeTabId |
string | null |
which of your dock tabs is selected; null for a single-tab tool |
requestFocus |
(tabId?: string) => void |
brings your tool to the front of the dock strip |
SessionContext is { kind: 'dir'; path: string; coreUrl?: string; worktree?: string }.
contributes.commands
Section titled “contributes.commands”Palette entries. Strict.
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | non-empty |
title |
string | yes | non-empty; the palette label |
keybinding |
string | no | e.g. "mod+shift+k" |
category |
string | no | palette grouping |
scope |
string | no | non-empty. focused-view | focused-slot | global, or a custom string |
Declaring a command does not give it behavior — ctx.api.commands.register
supplies the handler. A declared command with no registered handler is a dead
palette row.
contributes.agents
Section titled “contributes.agents”Agent definitions with no prompt body. Permissive.
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | non-empty; non-[a-z0-9-] characters are replaced with - |
name |
string | yes | falls back to id when empty |
description |
string | no | defaults to an empty string |
tools |
string[] | no | tool names or category names, passed through verbatim |
Registered with scope: 'extension', the extension’s version, the host’s
resolved agent model, and a 25-turn cap. The file and terminal categories
are always prepended to tools. For an agent with a prompt body, ship
agents/<name>.md instead — see Agents and skills.
contributes.skills
Section titled “contributes.skills”| Value | Meaning |
|---|---|
true |
Scan <extension root>/skills |
false |
Register nothing |
"some/dir" |
Scan that directory, relative to the extension root; absolute paths are also accepted |
["a", "b"] |
Scan each directory; results accumulate under one extension id |
The string forms name directories, not individual files. Each is scanned by the
same loader that handles the conventional skills/ folder, which accepts a flat
<name>.md or a <name>/SKILL.md directory with co-located resources.
contributes.services
Section titled “contributes.services”Declares service ids this extension intends to register. Permissive.
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | non-empty; the id other extensions pass to services.require |
interface |
string | no | documentation only |
The declaration is bookkeeping; ctx.api.services.register does the actual
registering. Consumers gate on it through requiredServices.
contributes.settings
Section titled “contributes.settings”User-editable settings surfaced in the host’s settings UI. Strict.
| Field | Type | Required | Notes |
|---|---|---|---|
key |
string | yes | ^[a-zA-Z][a-zA-Z0-9._-]*$ |
title |
string | yes | non-empty |
description |
string | no | helper text |
kind |
enum | yes | boolean | string | number | enum |
default |
any | no | returned by ctx.api.settings.get until the user stores an override |
enum |
string[] | no | the choice list for kind: 'enum' |
Reads and writes go through ctx.api.settings, persisted in the extension’s
storage under the key prefix settings:.
contributes.toolMetadata
Section titled “contributes.toolMetadata”Display and behavior hints for tools, keyed by tool name. Strict.
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | non-empty; the tool name |
icon |
string | no | lucide-react export name |
label |
string | no | display label on the tool card |
category |
string | no | grouping |
persistenceQuota |
number | no | >= 0 |
compactable |
boolean | no | the result may be dropped during compaction |
longRunning |
boolean | no | relaxes execution timeouts |
readOnly |
boolean | no | the tool makes no changes |
contributes.mcpServers
Section titled “contributes.mcpServers”Each entry registers a runnable MCP server under <extensionId>_<serverId>.
Permissive, but with one cross-field rule: transport: 'http' or 'sse'
requires url; anything else requires command. Violating it fails the
manifest with the message “stdio MCP servers require command; http/sse MCP
servers require url.”
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | non-empty |
runtime |
enum | no | node | python | binary |
transport |
enum | no | stdio | http | sse. Absent means stdio |
command |
string | no | required for stdio; non-empty |
args |
string[] | no | stdio only |
envVars |
array | no | surfaced in Settings so users can edit credentials |
envVars[].key |
string | yes | non-empty |
envVars[].description |
string | no | — |
envVars[].required |
boolean | no | — |
envVars[].default |
string | no | — |
url |
string | no | required for http/sse; must parse as a URL |
headers |
Record<string,string> |
no | http/sse only |
oauth.authServerMetadataUrl |
string | no | must parse as a URL |
oauth.clientId |
string | no | non-empty; skips dynamic registration |
Prefer http (Streamable HTTP) over sse; sse exists because several hosted
servers still ship SSE-only endpoints. OAuth flows are run by the host — the
extension never sees tokens.
contributes.projectTemplates
Section titled “contributes.projectTemplates”A one-shot bootstrapper for the New Project dialog, which copies files/ to the
user’s destination with {{VAR}} substitution. Permissive.
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | non-empty |
displayName |
string | yes | non-empty |
description |
string | yes | may be empty, but the key must be present |
icon |
string | no | lucide-react export name |
techStack |
string[] | no | shown as tags |
files |
string | yes | non-empty; directory to copy, relative to the extension root |
variables[].key |
string | yes | non-empty; the {{KEY}} token |
variables[].label |
string | yes | non-empty; the form label |
variables[].description |
string | no | — |
variables[].default |
string | no | — |
variables[].required |
boolean | no | — |
Picking a template is a separate action from installing the extension that carries it; a template is not itself an installed extension.
contributes.cliHarness
Section titled “contributes.cliHarness”Declarative identity of a hosted CLI coding harness. Strict. The runtime spec lives in the extension’s page code; this block is what the host reads to build the unified credential vault and route cross-harness attaches.
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | non-empty; equals the session agent field |
label |
string | yes | non-empty |
icon |
string | yes | non-empty; brand-icon registry key |
pageId |
string | yes | non-empty; the page this harness contributes |
namespace |
string | yes | non-empty; IPC namespace prefix |
hasUsage |
boolean | no | the harness can report subscription usage |
contributes.agentRuntimes
Section titled “contributes.agentRuntimes”An external process that can answer a chat turn — the agent analogue of an MCP
server. Strict at every level, including the nested home, usage, limits,
and identity objects.
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | ^[a-z][a-z0-9-]*$; stored on the chat |
label |
string | yes | non-empty |
transport |
literal | yes | "acp" — the only accepted value |
command |
string | yes | non-empty; executable speaking the protocol on stdio |
args |
string[] | no | — |
env |
Record<string,string> |
no | layered over the inherited environment |
icon |
string | no | brand-icon registry key |
requires |
string | no | the underlying CLI, probed to report “not installed” |
tools |
string[] | no | WAMP tools lent over MCP. Omitted means every tool except the file and terminal categories; [] lends nothing |
authMethods |
string[] | no | min 1 entries; which of the agent’s sign-in methods to offer, in order. Omitted means all |
home.env |
string | yes within home |
non-empty; env var that repoints the agent at a WAMP-owned directory |
home.credentials |
string[] | yes within home |
min 1; files inside that directory carrying the login |
usage.url |
string | yes within usage |
must parse as a URL |
usage.credential |
string | yes within usage |
non-empty; file in the home holding the token |
usage.tokenPath |
string | yes within usage |
non-empty; dotted path to the token in that file |
usage.headers |
Record<string,string> |
no | — |
usage.windows |
array | yes within usage |
min 1 of { label, path }, both non-empty; dotted paths to { utilization, resets_at } |
limits.patterns |
string[] | yes within limits |
min 1; case-insensitive patterns matched against a failed turn’s error text |
identity.credential |
string | yes within identity |
non-empty |
identity.path |
string | no | dotted path to the identity inside credential |
identity.claim |
string | no | JWT claim name, when the value at path is a JWT |
identity.url |
string | no | must parse as a URL — the endpoint mode |
identity.tokenPath |
string | no | dotted path to the bearer, for endpoint mode |
identity.headers |
Record<string,string> |
no | — |
identity.paths |
string[] | no | min 1; response paths tried in order |
usage is only meaningful alongside home — without a WAMP-managed credential
there is no token to ask with.
contributes.ipcNamespaces
Section titled “contributes.ipcNamespaces”Namespace prefixes this extension claims for its own IPC channels.
| Constraint | Value |
|---|---|
| Type | string[] |
| Each entry | ^[a-z][a-z0-9._-]*$ |
Claims happen at registration, before first use. A channel whose prefix is not
declared is refused by ctx.api.ipc.broadcast, synchronously. Two extensions
claiming the same prefix is a collision: the second faults and does not load.
Prefix with your extension id.
Strict and permissive objects
Section titled “Strict and permissive objects”Two failure modes, and which one you get depends on where the typo is. A strict object rejects unknown keys and names the offending field; a permissive one accepts and silently ignores them.
| Object | Unknown keys |
|---|---|
| Manifest root | ignored |
contributes |
ignored |
contributes.pages[] |
rejected |
contributes.commands[] |
rejected |
contributes.settings[] |
rejected |
contributes.toolMetadata[] |
rejected |
contributes.cliHarness[] |
rejected |
contributes.agentRuntimes[] and its nested objects |
rejected |
build and build.loaders[] |
rejected |
{ "auth.outbound": [...] } |
rejected |
contributes.agents[] |
ignored |
contributes.services[] |
ignored |
contributes.views[] |
ignored |
contributes.mcpServers[] |
ignored |
contributes.projectTemplates[] |
ignored |
A rejected manifest is reported as a failed scan with the field path, and the extension does not appear. An ignored key costs you a debugging session, so when something you declared has no effect, check the spelling of the key one level up first.
A complete manifest
Section titled “A complete manifest”Every optional field is left out except the ones this extension needs. It has a
page, a palette command, a cron job, a tool contributed at runtime from main,
and a settings entry.
{ "name": "Release Watch", "description": "Watches release feeds and files a task when something ships.", "version": "1.4.0", "engines": { "wamp": "^12.0.0" }, "compat": { "pluginApi": "^1.2.0" }, "icon": "Radar", "author": "Example Corp", "main": "dist/main.js", "permissions": ["cron", "notifications"], "activationEvents": ["onStartupFinished"], "contributes": { "pages": [ { "id": "release-watch", "title": "Releases", "icon": "Radar", "context": "both" } ], "commands": [ { "id": "release-watch.refresh", "title": "Releases: Refresh now", "keybinding": "mod+shift+r" } ], "settings": [ { "key": "pollMinutes", "title": "Poll interval (minutes)", "kind": "number", "default": 30 } ], "toolMetadata": [ { "id": "release_watch_check", "label": "Check releases", "readOnly": true } ], "ipcNamespaces": ["release-watch"] }}Related
Section titled “Related”- The manifest — which fields to set and why.
- Permissions — what each declaration actually unlocks.
- Plugin API reference — the namespaces each permission adds.
- Troubleshooting — manifest mistakes that fail quietly.