Skip to content

Permissions

A permission in WAMP does one concrete thing: it decides whether a namespace appears on your extension’s API object. There is no permission prompt, no runtime consent dialog, and no PermissionDeniedError. If you did not declare cron, then ctx.api.cron is undefined — that is the entire enforcement mechanism, and understanding it saves you from the failure mode it produces.

After this page you can tell which declarations actually withhold something, which are consent labels the runtime does not yet enforce, and how to write main-process code that fails loudly rather than quietly when a permission is missing.

{
"permissions": [
"database",
"cron",
"notifications",
"process",
{ "auth.outbound": ["https://api.example.com"] }
]
}

Eleven string values are valid — database, network, cron, ai, terminal, filesystem, http-routes, notifications, process, auth.identity, mcp — plus one structured entry, auth.outbound, which carries data. There is no other form, and the array rejects anything else.

Permissions are read once, at registration. Changing them means editing extension.json, which triggers a re-scan and a fresh context — the change takes effect on the next activation, not on the current one.

These withhold a capability. Declare them or the namespace is not there.

Permission Unlocks Enforced where
cron ctx.api.cron — scheduled jobs Main process
http-routes ctx.api.http — local HTTP routes for webhooks Main process
notifications ctx.api.notifications — system notifications Main process
process ctx.api.process, ctx.api.pty, ctx.api.git Main process
database ctx.api.cloud and pluginAPI.cloud Both halves
auth.identity ctx.api.auth — identity of the signed-in user Main process
{ "auth.outbound": [...] } The origins auth.fetch may target At every call
mcp pluginAPI.mcp — list and start/stop MCP servers Main process

Four of those deserve their own note.

process (child processes), pty (interactive terminal sessions), and git (worktrees and diffs) all sit behind the single process permission. Every operation in git is a git subprocess spawn, so it grants nothing process does not already imply, and a pty is a child process with a terminal attached. One permission, three namespaces, deliberately — the vocabulary stays shorter than the surface.

pty is the one to reach for when you are running a CLI: it negotiates size, reports itself as xterm-256color, and lets a full terminal UI run the way it does in a real terminal. Piping stdio through process instead gets you line-buffered text with no resize and no interactivity.

database gates the cloud store, not the local one

Section titled “database gates the cloud store, not the local one”

This is the one permission whose gate holds in the window as well as in the main process, and the one that does not gate what its name suggests.

  • ctx.api.data and pluginAPI.data — the local per-extension typed store — are always available. No permission gates them. They never leave the machine.
  • ctx.api.cloud and pluginAPI.cloud — records on the app’s own backend, shared across the user’s devices and, in shared scope, across users — require database.

The renderer path is re-checked in the main process on every call, so a modified renderer bundle cannot reach the backend without the declaration. The error is explicit:

Extension 'my-app' does not declare the 'database' permission,
which cloud storage requires. Add "database" to `permissions` in extension.json.

auth.identity and auth.outbound are two different things

Section titled “auth.identity and auth.outbound are two different things”

auth.identity unlocks ctx.api.auth in the main process: read the signed-in user, subscribe to session changes. Declaring it alone gets you read-only identity.

auth.outbound is the origin allowlist for auth.fetch, the call that reaches your own backend with a WAMP-signed, audience-bound token attached. Matching is exact origin — scheme, host, and port must all match, and there are no globs. Loopback origins are auto-allowed in a development build; a production build is strict.

{
"permissions": [
"auth.identity",
{ "auth.outbound": ["https://api.example.com", "https://events.example.com"] }
]
}

The origin check runs inside the auth service, so it applies to the call regardless of which half made it. Today, auth.identity itself gates only the main-process ctx.api.auth; the renderer’s pluginAPI.auth.getSession is not withheld when the permission is absent. Declare it anyway — the catalog shows what you declared, and relying on the gap is relying on a bug.

mcp unlocks pluginAPI.mcp, which can list MCP servers and start or stop them app-wide. What it exposes about each server is deliberately limited to name, display name, status, and tool count — never the server configuration, which may carry credentials. The check runs in the main process, not the renderer.

These four are consent signals. They appear in the catalog and describe what the extension intends to touch, and the runtime does not currently withhold the matching capability.

Permission Describes
ai Model calls — ctx.api.ai, pluginAPI.ai, agent delegation
filesystem File reads and writes — ctx.api.fs, pluginAPI.fs
network Outbound HTTP via pluginAPI.http.fetch
terminal Command execution via pluginAPI.terminal.exec

Everything else on ctx.api is present regardless of what you declared: workspace, ai, agents, fs, events, data, tools, secrets, dialog, shell, ipc, tokens, services, commands, disposables, storage, webviews, and settings.

Two of those are ungated for a reason worth knowing. dialog opens a native file picker, which cannot happen without the user choosing a file — the interaction is its own gate. tools.register adds a tool to the assistant’s surface, which the user then sees in every tool card; hiding it behind a permission would not make it more visible.

capabilities in the manifest is not a permission list and does not overlap with one. It carries four values — webviews.navigate, webviews.executeScript, webviews.interceptRequests, and webRequestRewrite — and it gates individual operations on ctx.api.webviews rather than the namespace as a whole. Declaring any of the three webviews.* values also selects the more capable webview backing, so it changes what the webview is, not only what you may ask of it. An unrecognized string is accepted and matches nothing. See the manifest.

Absence, not refusal — and how to write for it

Section titled “Absence, not refusal — and how to write for it”

Here is the trap, and it costs real time.

A missing permission does not produce an error naming the permission. The namespace is not on the object, so the first thing that happens is a TypeError about a property of undefined:

TypeError: Cannot read properties of undefined (reading 'schedule')

That message names schedule. It does not name cron, and it does not mention extension.json. In a 5-second activate() it reads like a host bug.

The SDK types are what save you: every gated namespace is declared optional on the API type — cron?, http?, notifications?, process?, pty?, git?, auth?, cloud?. So the compiler flags the access before you ever run it. That compiler error is the permission check. Two ways of silencing it are both worse than the error:

// Compiles. Throws at runtime with a message that names the wrong thing.
ctx.api.cron!.schedule('daily', '0 9 * * *', run);
// Compiles. Does nothing, forever, silently. The extension activates
// cleanly and the job never runs.
ctx.api.cron?.schedule('daily', '0 9 * * *', run);

Guard once, at the top of activate, and say what is missing:

import type { PluginContext } from '@wamp/extension-sdk';
export async function activate(ctx: PluginContext): Promise<void> {
const { cron, notifications } = ctx.api;
if (!cron || !notifications) {
ctx.log.warn(
'reminders: needs the "cron" and "notifications" permissions in extension.json',
);
return;
}
// Past this point both are narrowed to non-optional. No `!`, no `?.`.
ctx.api.disposables.add(
cron.schedule('reminders.daily', '0 9 * * *', async () => {
notifications.show({ title: 'Daily check', body: 'Nothing overdue.' });
}),
);
}

The guard costs three lines and turns an unexplained TypeError into a log line naming the fix.

Host-conditional namespaces are not permissions

Section titled “Host-conditional namespaces are not permissions”

Two namespaces are optional for a reason that has nothing to do with what you declared, and they look identical from inside your code:

Namespace Absent when
ctx.api.sessions The host has no user interface — nobody is looking at a session
ctx.api.clientAffordances The host does not answer reverse calls from a remote core

No permission makes either appear. Guard with ?. here — this is the one place where the optional-chaining shortcut is the correct answer, because absence is a legitimate deployment shape rather than a misconfiguration.

  • The plugin API — the surface each permission unlocks, with a working snippet per namespace.
  • The manifest — where permissions sit among the other twenty fields.