Skip to content

Plugin Development

Zach Hubbard edited this page Oct 13, 2026 · 1 revision

Plugin Development

The full plugin author's guide is PLUGIN_DEVELOPMENT.md in the repository. It is the source of truth and is kept in step with the plugin API. This page is a map of it plus the two sections you should read first. A starter plugin lives in the repository's plugin-dev-kit/ folder.

Sections of the guide

New plugin checklist

Before writing any code, decide these up front — each is cheap to get right at the start and annoying to change later:

  • Isolated or not, and why? Any real capability beyond a single ipcMain handler — spawning a process, network access, filesystem writes outside your own dataDir — means "isolated": true (see "Trust model" below). Every first-party plugin with real capabilities uses it now; same-process mode is for genuinely light plugins only.
  • Which category? "channel" (registers a real channel number), "theme" (a registerThemePack entry), "onscreen" (an overlay/effect, not tuned to), or "system" (background service, remote control, integration, no on-screen presence). This is only a grouping hint for the PLUGINS tab's category tabs, but pick one deliberately — "OTHER" is what you get for skipping it.
  • Does it need a dataDir? Only if you keep real data (a cache, guest uploads, anything beyond your own plugin folder) at the top level of userData/. Declaring it lets the app's own userData migration carry it along; if you don't need one, don't declare one.
  • Renderer entry point or not? renderer.js/settings.js share the same document and DOM as core and every other plugin — no isolation at all, unlike main.js. If your UI doesn't need direct DOM integration, prefer a sandboxed iframe or Worker instead of a renderer entry point (see "Trust model" below).
  • Does settings UI belong in its own settings.js? Every first-party plugin now splits its PLUGINS-tab settings panel out of renderer.js into its own settings.js (declared via manifest "settings") so channel- render logic and settings-panel logic don't share a file. Do this from the start rather than retrofitting it later.
  • Does it need a cross-plugin service? If another plugin (or core) needs to call into yours, publish it via ctx.services / claimServiceKey rather than having one plugin import another directly — plugins can't import each other's files across origins anyway (see "Cross-plugin services" below).
  • Read a similar existing plugin first. plugins/ has a first-party example for nearly every shape (channel, theme pack, system/isolated, media source) — copying the closest match's manifest/folder layout beats re-deriving these conventions from scratch.

Trust model

By default, a plugin's main.js runs via require() in the Electron main process — full Node/Electron access (ipcMain, child_process, fs, all of it), the same privilege level as the app's own code. There is no sandbox between plugins, or between a plugin and the app, in this mode.

Set "isolated": true in manifest.json and your main.js instead runs in its own real OS process (a forked Node process, no direct Electron access at all — see "Isolated plugins" below), with Node's own --experimental-permission flags actually enforcing (not just checking) that it can only read/write the paths and spawn the processes it declared. This is a genuine process boundary, not a same-process convention — a plugin that bypasses ctx.fs/ ctx.spawn entirely and calls require('fs')/require('child_process') directly still gets denied. New plugins with any real capabilities (spawn, network, filesystem writes beyond their own dataDir) should default to "isolated": true — every first-party plugin that needs one now does. Same-process mode still exists for plugins with lighter needs (a single ipcMain handler, nothing else) where the extra process overhead isn't worth it, and capabilities are still checked there too — just not by the OS, only by the same JS-level wrapper as before.

Only install plugins you trust the source of, isolated or not. A ZIP install records a SHA-256 digest outside the plugin folder. Manually copied plugins require an explicit one-time trust decision, and any later byte change blocks all of that plugin's main, renderer, settings, and CSS entry points until the exact new digest is reviewed. This detects replacement or local tampering; it is not an author signature and does not prove who wrote the original ZIP.

Isolation scopes what a plugin's main.js can touch on disk and which binaries it can spawn; it does nothing to stop a plugin from, say, exfiltrating data over the network it already declared a capability to use. It does not isolate renderer.js, settings.js, or style.css: those intentionally join the same renderer document as core and can inspect or modify the whole UI and call the exposed window.api surface. If third-party UI does not require direct DOM integration, prefer a sandboxed iframe or Worker communicating through narrow messages instead of a renderer entry point. If you're distributing a plugin, say clearly what it does and what it talks to (network requests, spawned processes, file access) so users can make that call.

See also Plugins, Security and Architecture.

Clone this wiki locally