Build Your First Plugin
Create a Manifest v2 Zync worker and pane, validate the package, and test it in the desktop app.
Start with a small worker and one pane. Add permissions and integrations only after the basic package installs and works in the desktop app.
Requirements and compatibility
Section titled “Requirements and compatibility”- Node.js 20 or newer, npm, and Git for the commands below.
- An installed Zync build that satisfies the manifest’s
engines.zyncrange. - SDK
2.1.0-beta.4for the examples here. Its npm beta version is independent of the host’s Plugin API 2.1.0 and your plugin’s own version. - OpenSSL later, when generating a publisher key.
The starter uses package-relative pane assets, supported from Zync 2.33.8.
It requires API ^2.0.0 because it does not use API 2.1 additions. A plugin using
sshCommand.execute must require ^2.1.0 and a tested compatible desktop range.
Installing a newer SDK cannot add host APIs to an older installed app.
Start from the SDK template
Section titled “Start from the SDK template”Run these commands from a directory where plugin-sdk and my-zync-plugin do
not already exist. The clone supplies the starter; the plugin installs its own
pinned npm dependency.
git clone https://github.com/zync-sh/plugin-sdk.gitnode -e "require('node:fs').cpSync('plugin-sdk/templates/basic', 'my-zync-plugin', { recursive: true, errorOnExist: true, force: false })"cd my-zync-pluginnpm install --save-dev --save-exact @zync-sh/plugin-sdk@2.1.0-beta.4npm run validatenpx --no-install zync-sdk validate dist --zync-version 2.34.0Keep package-lock.json in your plugin repository and use npm ci in CI. The
template is also shipped in the installed SDK under templates/basic. Record the
SDK source commit used for the template if you copy it from Git.
my-zync-plugin/ manifest.mjs package.json package-lock.json scripts/build.mjs scripts/preview.mjs src/worker.js src/ui/index.html src/ui/pane.js src/ui/pane.css dist/ generated packageThe starter build copies the worker and UI assets and writes dist/manifest.json.
It does not compile TypeScript or bundle npm imports automatically. If you add
TypeScript, React, or another dependency, add a build step that produces
browser-compatible worker and pane bundles. Bundle the worker as one script;
there is no Node.js module loader in the plugin runtime.
Set your identity and manifest
Section titled “Set your identity and manifest”Edit manifest.mjs before distribution. Replace dev.example with your publisher
namespace and choose a plugin ID beneath it, such as dev.example.hello.
Contribution IDs must match between the manifest and worker calls.
This is a complete minimal pane manifest, shown as the generated JSON:
{ "manifestVersion": 2, "id": "dev.example.hello", "name": "Hello Zync", "version": "1.0.0", "publisher": "dev.example", "description": "A small example pane.", "license": "MIT", "engines": { "zync": ">=2.33.8", "pluginApi": "^2.0.0" }, "runtime": { "entry": "worker.js" }, "contributes": { "paneKinds": [ { "id": "hello.main", "title": "Hello Zync", "entry": "ui/index.html", "allowMultiple": true } ] }, "permissions": { "required": [ { "id": "ui.pane.register", "reason": "Open the Hello Zync workspace pane." } ] }}defineManifest helps with authoring; validation and installation enforce the
rules. Add your source, support, and privacy URLs as appropriate. Never reuse
com.zync or another publisher’s namespace. Publisher approval is a separate
registry review.
Worker and pane communication
Section titled “Worker and pane communication”The worker registers the declared pane and handles messages. Validate message
types in the worker and reply only to the originating paneInstanceId.
const zync = globalThis.zync;
zync.on('ready', async () => { await zync.panel.register('hello.main');});
zync.panel.onMessage(({ paneInstanceId, message }) => { if (!message || typeof message !== 'object' || message.type !== 'ping') return; void zync.panel.postMessage(paneInstanceId, { type: 'pong', text: 'Hello from the worker', }).catch(error => console.error('Could not reply to pane', error));});<!doctype html><html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Hello Zync</title> <link rel="stylesheet" href="./pane.css"> <script src="./pane.js" defer></script> </head> <body> <button id="ping" type="button">Ask the worker</button> <p id="reply" role="status"></p> </body></html>const button = document.getElementById('ping');const reply = document.getElementById('reply');button.addEventListener('click', () => { window.zync.pane.postMessage({ type: 'ping' });});const unsubscribe = window.zync.pane.onMessage(message => { if (message?.type === 'pong' && typeof message.text === 'string') { reply.textContent = message.text; }});window.addEventListener('pagehide', unsubscribe, { once: true });Keep the starter’s pane.css or replace it with your styles. The pane uses
window.zync.pane; calling worker-only APIs on it will fail. Render untrusted
output with textContent or framework text bindings.
Assets and framework builds
Section titled “Assets and framework builds”Use package-relative asset URLs such as ./pane.js and ./pane.css. For Vite,
set base: './'. Bundle dependencies and fonts into the package; CDN scripts,
remote iframes, and direct pane network requests are not a supported integration.
Avoid absolute /assets/... URLs and links that escape the package.
Current pane assets support CSS, JS/MJS, PNG, JPEG, GIF, WebP, SVG, WOFF/WOFF2, TTF, and OTF. The SDK preflight limits individual pane assets to 2 MiB; the native package file ceiling is 20 MiB. Keep pane HTML below its 512 KiB host limit. Large editor builds use their separate editor asset path; do not assume that an editor’s bundle layout works unchanged inside an ordinary pane.
The optional @zync-sh/plugin-ui@0.1.0-beta.1 provides themed controls. Bundle
its JavaScript and CSS into your own assets. See UI best practices.
Test inside Zync
Section titled “Test inside Zync”- Run
npm run validateand inspectdist/for unexpected files. - Open Settings > Plugins > Developer and enable Developer Mode.
- Install the built folder
dist/, not the source project ornode_modules. - Review the requested permissions and approve only what you intend to test.
- Open the contributed pane from the workspace’s panel choices. Click Ask the worker.
- Open two pane instances and check that each receives only its own replies.
- Rebuild and reinstall after changes. Folder installation copies the package; it is not a live link to your source directory. Follow any restart notice.
- Disable the plugin, remove optional permissions where applicable, and test the resulting unavailable states. Turn Developer Mode off when finished.
npm run preview serves a visual preview on localhost. It does not run the worker,
grant permissions, connect to SSH, or prove native installation works. Browser
mock tests are useful but do not replace an installed-app test.
Debugging
Section titled “Debugging”| Symptom | Check |
|---|---|
| Package will not install | Manifest at root, engine ranges, permission names, missing assets, and validation output |
| Pane never appears | Exact contribution ID, ui.pane.register, and worker ready handler |
| Pane is blank | Packaged paths, browser-compatible bundles, CSP errors, and asset size limits |
| Button sends no result | Worker message validation, originating pane ID, rejected promise, and declined permission |
| Local plugin stops after restart | Developer Mode, enable state, and plugin recovery/quarantine state |
| Preview works, installed build fails | Host bridge availability, platform WebView behavior, and installed package contents |
Keep diagnostic output small and exclude credentials, command output containing secrets, and private file contents. Use a development build’s WebView console when available, and inspect Zync’s plugin/runtime error messages. Do not depend on production installers exposing developer tools.
Continue with API and permissions and signing and publishing.