Fast, offline, private grammar and spelling checking for the Joplin Markdown editor on desktop and mobile, powered by Harper. Per-kind coloured underlines, a suggestion card with one-click fixes, add-to-dictionary, ignore and disable-rule, plus American, British, Australian and Canadian dialects. The dictionary can live in a synced Joplin note so words follow you across devices.
| Links: | |
|---|---|
| Maintainers: | pmslava |
| Version: | 1.5.1 |
| Minimum app version: | 3.1 |
| Downloads: This version: | 194 |
| Last updated: | 2026-08-29T17:43:20Z |
Grammar and spell checking for Joplin's Markdown editor, on desktop and Android, powered by Harper — the fast, private grammar checker from Automattic.
Harper runs entirely on your machine. There is no cloud service, no account, and no network request: your notes never leave your computer. The whole checker (a compiled WebAssembly engine) ships inside the plugin, so it works fully offline.
As you type in the Markdown editor, Harper underlines spelling and grammar issues with a coloured squiggle, and clicking (or tapping) an underline opens a small card with the problem, one-click fixes, and follow-up actions.


Per-kind coloured underlines — a red spelling squiggle, an orange typo, a green word-choice suggestion:

By default, words you add via Add to dictionary are stored in the plugin's own private word list on that device. To share one dictionary everywhere, set up the sync note — it carries your words along with your rules and your dismissed findings.
On desktop you can also keep the same words in a plain file of your own:
External dictionary file (desktop only). Point External dictionary file at a plain-text dictionary file of your own — one word per line. When that path is set:
Deleting a word is the one case where the plugin writes more than a new line, so it does as little as it can: it drops only the lines for words you removed, appends genuinely new words at the end, and leaves every other line, comments included, exactly where it was. Surviving lines are never reordered or rewritten, and the file is left completely untouched when nothing changed. Writes go through a temporary file in the same directory and an atomic rename, and if the file changes while the plugin is working on it, the write is abandoned and retried on the next pass, so a sync client writing at the same moment is never clobbered.
This is deliberately just a flat text file with no special format. That makes it easy to keep the
file wherever you like and sync it between machines with your own tooling (a synced folder, a
version-controlled dotfile, rsync/rclone, and so on) — or to share the same word list with
another tool that reads a plain word-per-line dictionary, such as harper-ls in Zed, Neovim, or
VS Code.
Using both together is the full setup: with the sync note and the file both configured on desktop, a word added on your phone reaches this machine through Joplin sync and lands in the file — and from there in any external tool that reads it. Deletions travel the same way, in every direction.
The sync note carries everything: your rule choices, your dictionary, and the findings you dismissed.
Run the command Harper: Create sync note once, on any device. The plugin creates a note called "Harper Sync", seeds it with what that device already has, and remembers it in the Sync note setting. On your other devices, open the same note, copy its id, and paste it into their Sync note setting. From then on the plugin writes the note whenever you change something and reads it back on the other devices, through your normal Joplin sync.
The note holds machine-readable data, so do not edit it by hand. It says so at the top.
A few things are worth knowing:
harper-ls powers Harper in Zed, Neovim, Helix, and VS Code, and it reads its rules from that
editor's own settings file. Point External settings file at a path of your choosing — the full
path including the file name — and the plugin writes your current dialect and rule overrides there
as plain JSON, rewriting it whenever either changes:
{
"dialect": "American",
"linters": {
"SentenceCapitalization": false
}
}
Those are harper-ls's own key names, so whatever you feed the file to needs no translation. The
file is Harper's, written wholesale: point it somewhere of its own rather than at a config you edit
by hand. Leave the setting empty and nothing is written at all.
The linters map lists only the rules you actually changed, so it stays short and Harper's own
defaults keep applying to everything else; with nothing overridden the key is left out entirely.
Dismissed findings are not included: harper-ls computes its ignore hashes differently, so
exporting them would produce a file that silently ignores nothing.
Run the command Harper: Settings… for the full settings screen. On desktop it is in
Tools → Harper: Settings…, or type it into the command palette (Ctrl+P, then :). On mobile
it is in the note's … menu. It works the same on both.
It has four sections:
Open Tools → Options → Harper on desktop, or Configuration → Plugins → Harper on mobile.
Where the settings live is itself a setting. Out of the box the Harper window owns all of them, and this page holds a single switch:
| Setting | Default | What it does |
|---|---|---|
| Manage settings in the Harper window | On | Where the basic settings are edited. On: they live in the Harper window and this page stays minimal. Off: they appear on this page instead, and Harper: Settings… leaves the Tools menu (desktop) or the note toolbar (mobile). Either way the rule browser, the dictionary editor and the dismissed findings exist only in the Harper window — on desktop it stays reachable from the command palette. Takes effect after a Joplin restart, and applies per device (it does not sync). |
The settings below are the ones that switch moves. With it on they are edited in the Harper window's General tab; with it off they appear on this page. Their values and behaviour are identical either way, and the two surfaces stay in step — change something in either and the other reflects it.
| Setting | Default | What it does |
|---|---|---|
| Enable Harper grammar checking | On | Master switch. When off, no underlines are shown. |
| External settings file (desktop) | (empty) | Absolute path — file name included — to a JSON file where Harper keeps your dialect and rule overrides for other tools to read. Harper rewrites it when they change. See Share your rules with other tools. |
| Sync note | (empty) | The Joplin note that syncs your rules, your dictionary and your dismissed findings between devices. Set automatically by the Harper: Create sync note command. See Sync your settings. |
| English dialect | American | Which English variety Harper checks against: American, British, Australian, Canadian, or Indian. |
| Lint debounce (ms) | 500 |
How long the editor waits after you stop typing before re-checking, in milliseconds (0–10000). Changes apply immediately. |
| Underline style | Squiggly | How findings are underlined: Squiggly (default) for Harper's wavy underline, or Solid line for a straight 2 px line with a light tint. Either way the colour is the issue type's. Changes apply immediately. |
| Ignore non-English text | Off | Skip text that Harper detects as not English. Useful for multilingual notes. |
| External dictionary file | (empty) | Desktop only. Absolute path to a plain-text dictionary (one word per line). See Your dictionary. |
| Rule overrides (JSON) | (empty) | Advanced. A JSON object of {"RuleName": true | false} applied on top of the defaults, e.g. {"SpelledNumbers": false}. Invalid JSON is ignored. |
In Joplin desktop, go to Tools → Options → Plugins, search for Harper, and click Install. Restart Joplin when prompted.
Joplin mobile's plugin support is marked Beta and is off by default. In Configuration →
Plugins, enable plugin support first, then search for Harper and install — or use
Install from file with the .jpl from the releases page. Restart the app when prompted.
io.github.pmslava.harper.jpl from the
latest release..jpl..jpl roughly 21 MB. That is large for a plugin, but it is the whole checker, downloaded once.Harper runs locally and makes no network calls. Your note text is passed to the bundled WebAssembly engine inside Joplin and nowhere else — nothing is uploaded, and there is no telemetry. The external dictionary, if you use one, is a file on your own disk; the sync note is an ordinary Joplin note that travels only through your own Joplin sync, like every other note.
npm install
npm test # builds the plugin and runs the harness suite (incl. performance budgets)
npm run test:e2e # full end-to-end suite: launches a real Joplin desktop under Xvfb (Linux)
npm run dist # build only -> publish/io.github.pmslava.harper.jpl
test:e2e starts a real Joplin, so it first takes a machine-wide lock
(~/.cache/joplin-plugin-e2e.lock) shared with the author's sibling Joplin plugin repos: exactly one
E2E run exists at a time, and a run that finds the lock held waits its turn rather than piling a
second Joplin onto the machine (E2E_LOCK_WAIT_MS sets the budget, default 10 minutes; 0 fails
fast). The rest of the resource discipline — orphan sweep, RAM gate, signal teardown — lives in
e2e/guard.ts.
Architecture in one line: the grammar checker (harper.js LocalLinter, WASM) runs in the
plugin main process, and a CodeMirror 6 content script draws the underlines and the suggestion card,
talking to it over Joplin's postMessage bridge.
For the full design, the WASM loading approach, the UI spec, and the research behind the plugin, see
docs/ — in particular docs/SPEC.md.
The plugin is licensed under the MIT License.
It embeds and is powered by Harper (via the
harper.js package), which is developed by Automattic
and licensed under the Apache License 2.0. All credit for the grammar-checking engine goes to the
Harper project — this plugin only integrates it into Joplin.