Appearance
Are you an LLM? You can read better optimized documentation at /reference/permissions.md for this page in Markdown format
Permissions reference
Every IPC call a plugin makes is checked against the capabilities listed in its manifest permissions array. Undeclared = hard reject — there is no implicit trust and no runtime workaround. Getting this array right is where most first-attempt plugins fail, so this page maps each SDK feature to the exact string it needs.
Two distinct concepts share the word "permission":
- Capabilities (this page) — manifest strings that gate the plugin's access to runtime services. Enforced by the runtime on every IPC call.
- User permissions — role/permission checks your plugin runs on its users via
plugin.permissions(e.g. "can this user post?"). Registered withplugin.permissions.register(). These are application logic, not manifest declarations.
Grammar
resource.action[:scope]Validated against:
js
/^[a-z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9_]*(?::[a-z0-9*][a-z0-9.*:-]*)?$/resource and action are dotted identifiers; scope is optional and may contain dots, colons, and * wildcards. Scope conventions:
:self— the plugin's own resource (its own DB, its own files).:<plugin>.<table>— a specific cross-plugin target.:<namespace>.*— a prefix wildcard (events).:<hostname>— a network target (fetch). Hostname only, no port.
Capabilities by feature
| Capability | Unlocks (SDK) | Scope rules |
|---|---|---|
data.sql:self | plugin.db — own SQLite | :self only |
data.kv:self | plugin.kv — own key/value store | :self only |
data.read:<plugin>.<table> | plugin.data.read — read another plugin's published table | no wildcards — name an exact plugin.table |
events.publish:<ns>.* | plugin.events.publish | prefix wildcard or exact topic; bare * rejected. By convention your own namespace — see the note below |
events.subscribe:<pattern> | plugin.events.subscribe | prefix wildcard (x.*) or exact topic; bare * rejected |
broadcast.clients | plugin.broadcast + scoped plugin.presence | no scope |
notifications.push | plugin.notifications.send — durable notifications to up to 100 targeted users | no scope |
notifications.push_all | plugin.notifications.sendAll — durable notification to every member | no scope |
storage.file:self | plugin.files + the /upload endpoint | :self only |
http.fetch:<host> | plugin.fetch — outbound HTTP to that host | exact hostname, no port |
runtime.schedule | plugin.schedule — recurring tasks | no scope |
voice.tokens:self | plugin.voice.createJoinToken | :self |
voice.rooms:self | plugin.voice.createRoomToken for plugin-owned LiveKit rooms; also declare managed_services: ["livekit"] | :self |
voice.moderation:self | plugin.voice.removeParticipant | :self |
proxy.http:self | reverse-proxy HTTP forwarding for a proxy_mount | :self |
proxy.websocket:self | reverse-proxy WebSocket forwarding (not implied by HTTP) | :self |
resources.read:<plugin> | cross-plugin plugin.resources.check | exact owner-plugin slug |
net.public_ports | plugin.net — Transport-backed public TCP/UDP ports | no scope; requires active Transport |
companions.request:<companion> | brokered plugin.companions.request | exact companion id; experimental native companion lane |
Capabilities that need no declaration: reading your own
settings, thecoreuser/category cache, presence connect/disconnect hooks, and registering your own user permissions. These are always available.
The companion broker and manifest declaration are experimental. The native supervisor provisions an approved companion only when the machine's operator has enabled the preview lane; otherwise installation reports that the workload did not start.
Two notes on scoping, because both cost people an afternoon:
http.fetchscopes on hostname only — never a port.plugin.fetchderives the capability from the URL's hostname, so a declaredhttp.fetch:api.example.com:8080matches nothing and every fetch is denied. Declarehttp.fetch:api.example.comand put the port in the URL.events.publish:<ns>.*is not currently enforced against your own slug. The runtime reserves theruntime.*namespace, but nothing today stops a plugin from declaring and publishing to another plugin's namespace. Treat publishing only to your own namespace as the convention it is — other plugins' subscribers will not thank you — and don't rely on the platform to stop someone else doing it to you.
Accepted but inert
runtime.log and auth.currentUser are valid permission strings — they parse, they survive manifest validation, and they show up on the install consent screen — but no IPC action is gated by either one today, so declaring them grants nothing.
- Logging needs no capability at all: the runtime captures your backend's plain
stdout/stderrinto the server log (lines prefixedIPC:are the transport, so keep those out of your own output). - Current-user data arrives as the
userargument on every handler, and richer lookups go throughplugin.core, which is also undeclared.
Leave both out of your manifest: the smallest honest permission set is the one reviewers and owners can actually read.
runtime_capabilities (separate array)
Voice features are opted into via the manifest's runtime_capabilities array, not permissions:
| Value | Gates |
|---|---|
voice.media | LiveKit-mediated audio (the voice-channels plugin). |
voice.screen_share | the plugin's ability to grant screen-share publish. |
voice.moderation | admin "Stop their share" via LiveKit RemoveParticipant. |
Per-user authorization (e.g. voice.screen_share.publish) is a separate user permission your plugin registers and checks — see above.
Wildcard rules at a glance
| Pattern | data.read | events.publish | events.subscribe |
|---|---|---|---|
exact (x.y) | ✅ | ✅ | ✅ |
prefix (x.*) | ❌ | ✅ | ✅ |
bare * | ❌ | ❌ | ❌ |
Worked example
The text-channels plugin declares:
json
"permissions": [
"data.sql:self",
"events.publish:text-channels.*",
"events.subscribe:runtime.cascade.*",
"events.subscribe:runtime.presence.*",
"events.subscribe:text-channels.*",
"events.subscribe:core.category.*",
"broadcast.clients",
"notifications.push",
"storage.file:self",
"runtime.schedule"
]Reading top to bottom: it owns a database, publishes its own events, listens for user-deletion cascades, presence, its own events, and category deletions, pushes real-time updates to clients, sends durable mention notifications, stores uploaded files, and runs a scheduled orphan-GC sweep. Every SDK call it makes traces back to one of these lines.
The capability checker and its test suite are the executable spec: runtime/src/capabilities/checker.ts.