Back to plugin gallery Developer guide

Build a Marknote plugin

Plugins ship as a folder with a manifest.json and a JS entry-point. Marknote loads them into a sandboxed WebView2 at startup; from there a plugin can register palette commands and preview transforms. The API surface is small on purpose — what's here is what 1.4.0 ships. New surface lands in subsequent releases.

Where plugins live

On Windows, drop your plugin folder into:

%LocalAppData%\Marknote\plugins\<your-plugin-id>\
  manifest.json
  main.js

Open Settings → Plugins in Marknote to see what was discovered. Failed manifests show up with a status line explaining exactly which field is wrong. After dropping new files into the folder, click Reload to re-run discovery without restarting the app — though the runtime only activates plugins on startup, so executing a freshly-dropped plugin still requires a relaunch.

manifest.json

One required object per plugin. The schema below is the full set of fields Marknote 1.4 understands.

{
  "id": "uk.marknote.demo",
  "name": "Marknote Demo",
  "version": "1.0.0",
  "author": "Gregory Hulsmann",
  "description": "Reference plugin demonstrating commands and previewTransforms.",
  "main": "main.js",
  "homepage": "https://marknote.md/plugins",
  "contributes": ["commands", "previewTransforms"],
  "minMarknoteVersion": "1.4.0"
}
Field Required Notes
id Yes Reverse-DNS-style. Becomes the folder name AND the identity used to attribute every registration. Renaming after release breaks every install.
name Yes Display name shown in Settings → Plugins.
version Yes Semver, no leading 'v'. Bumped on each release of your plugin.
main Yes Path to your entry-point JS file, relative to the plugin folder. Must be a single segment with a .js extension.
author Surfaced in Settings → Plugins as an inline byline.
description One-line summary shown under the plugin name.
homepage Clickable in Settings → Plugins. Point at docs or your issue tracker.
contributes Declared capabilities. Listing one you don't use is harmless; calling an API for a capability you didn't declare throws at runtime. Known values: "commands", "previewTransforms".
minMarknoteVersion Plugin is skipped with a clear error if the running app is older than this.

The marknote API

Your plugin's main.js is evaluated inside an IIFE that receives a marknote object scoped to your plugin id. Three surfaces are exposed in 1.4.0:

marknote.commands.register({ id, title, run })

Adds a command to the Marknote command palette (Ctrl+Shift+P). Commands appear at the bottom of the palette prefixed with Plugin · . When the user picks one, your run callback fires.

marknote.commands.register({
  id: 'show-timestamp',
  title: 'Show timestamp',
  run() {
    marknote.notify('Plugin clock: ' + new Date().toLocaleString());
  },
});

marknote.preview.onTransform(fn)

Registers a markdown → markdown transform. The function runs on the markdown source before markdig sees it, on every render. Pure sync return works; returning a Promise<string> also works. A 4-second timeout per transform is enforced — exceed it and Marknote falls back to the pre-transform content rather than freezing the preview.

marknote.preview.onTransform(function (markdown) {
  const today = new Date().toISOString().slice(0, 10);
  return markdown.split('{{date}}').join(today);
});

marknote.notify(text)

Surfaces a transient message in Marknote's status bar. Useful for command callbacks that need to confirm an action without opening a dialog. String only.

Reference plugin

The public wearemarknote/plugins repo ships a working example at marknote-demo/ — a manifest plus a 20-line main.js demonstrating both contributes. Copy the folder into %LocalAppData%\Marknote\plugins\ and restart the app to try it. Source: github.com/wearemarknote/plugins.

Sandbox model

Each plugin runs in a single shared WebView2 with no DOM affordances (no fetch, no XHR via CSP, no developer tools, no context menu). Plugin code is wrapped in an IIFE so var / let declarations don't leak across plugins. The bridge to .NET is the only side-effect channel — every registration, transform result, and notification rides through a tracked WebMessage round-trip.

This isn't an adversarial sandbox yet — a malicious plugin can still infinite-loop or hog CPU. Don't install plugins from sources you don't trust. A harder sandbox (origin isolation, per-plugin iframes, time slicing) is on the roadmap.

What's coming next

Two areas of active work for subsequent releases:

  • Marketplace — in-app browsing and one-click install of community plugins, with a moderated catalogue.
  • Wider API surface — editor access (read cursor, insert at selection), workspace queries (list notes, read a file), exporter registration. Driven by what plugin authors actually need; open an issue if you're blocked.

Built something?

Open a PR or an issue on github.com/wearemarknote/issues with a link to your repo and a one-line description. Once it's approved, it joins the plugin gallery.

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.