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.