Skip to content
ZyncDocsGitHub Download Zync

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.

  • Node.js 20 or newer, npm, and Git for the commands below.
  • An installed Zync build that satisfies the manifest’s engines.zync range.
  • SDK 2.1.0-beta.4 for 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.

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.

Terminal window
git clone https://github.com/zync-sh/plugin-sdk.git
node -e "require('node:fs').cpSync('plugin-sdk/templates/basic', 'my-zync-plugin', { recursive: true, errorOnExist: true, force: false })"
cd my-zync-plugin
npm install --save-dev --save-exact @zync-sh/plugin-sdk@2.1.0-beta.4
npm run validate
npx --no-install zync-sdk validate dist --zync-version 2.34.0

Keep 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 package

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

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:

dist/manifest.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.

The worker registers the declared pane and handles messages. Validate message types in the worker and reply only to the originating paneInstanceId.

src/worker.js
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));
});
src/ui/index.html
<!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>
src/ui/pane.js
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.

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.

  1. Run npm run validate and inspect dist/ for unexpected files.
  2. Open Settings > Plugins > Developer and enable Developer Mode.
  3. Install the built folder dist/, not the source project or node_modules.
  4. Review the requested permissions and approve only what you intend to test.
  5. Open the contributed pane from the workspace’s panel choices. Click Ask the worker.
  6. Open two pane instances and check that each receives only its own replies.
  7. Rebuild and reinstall after changes. Folder installation copies the package; it is not a live link to your source directory. Follow any restart notice.
  8. 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.

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.