Skip to content

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.

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

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

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.

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

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[]

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.

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

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.

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.

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.

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.

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

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

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.

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.

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

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.

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.

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.

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