Plugins
Publishing a plugin
What is required, what is checked, what gets rejected, and what a person looks at. If you have not written the plugin yet, start with Writing a plugin.
1Before you submit
Three things save the most time, in order:
- Install it from a URL first. Serve your bundle over HTTPS and use Install a plugin from a URL… in the app. That is the same code path the marketplace uses — if it installs that way, the packaging is right.
- Narrow your permissions. Every scope you can tighten moves you out of the always-reviewed pile and makes the consent prompt something people say yes to.
- Publish the source you wrote. Not a bundle, not minified output. A reviewer reads what you upload.
2The checklist
This maps one-to-one onto what the gate actually checks. Nothing here is a matter of taste.
manifest.jsonat the root of what you upload.- Every file named in
entryPointsis in the bundle. categoryset to one of the seven.descriptionis a real sentence, 10–200 characters, and describes behaviour.idcontains your publisher handle as a segment, is lowercase and dotted, has no underscores, and is at most 80 characters.versionis semver and has gone up — published versions are immutable, so re-uploading the same version is refused.- Permissions are as narrow as they can be, and every one is used by the code.
- Source is readable: not minified, not bundled, no generated single-line files.
- No
eval,Function, dynamic or remoteimport(), wasm, or packed payload strings. - Under 2 MB total, at most 200 files, no single file over 500 KB.
- A
README.mdand aLICENSE. - Screenshots, if you have them: up to 4
httpsURLs inscreenshots, showing the plugin doing its job rather than its empty state. The first is your thumbnail.
3Your publisher handle
A handle is your namespace, claimed once: 3–32 lowercase letters, digits
or hyphens. Every plugin id you publish must contain it as a segment, so
alice can publish alice.kanban or
io.github.alice.kanban, and nobody else can publish
anything with alice in it.
That is the whole anti-impersonation story, and it is enforced by the
database rather than by a policy: a plugin whose id does not carry its
publisher's handle cannot be inserted at all. Reserved handles
(slate, admin, security, and
similar) are refused, and the slate.* namespace belongs to
the built-ins.
You sign in with the same account you use for sync in Slate. There is no separate developer account.
4How to submit
Two doors, the same gate behind both.
- From the site. Browse plugins → Publish a plugin. Sign in, claim your handle, choose your plugin folder. The findings come back in the browser.
- From the app.
⌘P→ Publish a plugin…, and pick the plugin in your vault. You are already signed in.
Either way your files are uploaded as text, hashed on the server, and the manifest is parsed there — the permissions recorded against your version come from the manifest in the bundle, never from anything the request claims. A listing that under-declared its permissions would be a consent prompt that lies.
5What happens next
- Structure. Paths, size, entry points, manifest validity, id namespace, version collision. A failure here is immediate and tells you exactly what to fix.
- The analyzer. Static analysis for code that cannot be reviewed and for sandbox escapes, plus an assessment of what your permissions make possible. Anything blocking is a rejection with the file and line, and nothing is stored.
- A verdict.
rejected,pending, orapproved. Today everything clean lands onpending. - Review. A maintainer reads the source and the findings, and lists it or writes back.
You get the verdict, a risk score, and every finding — including the advisory ones, like a permission you declared but never used.
6What gets rejected
These are automatic and not negotiable, because each one is a way to run code a reviewer never saw:
| Finding | Why |
|---|---|
eval | Builds code at runtime, so nothing can see what the plugin does. |
new Function | eval by another name. |
.constructor.constructor | Reaching Function that way is a sandbox escape, not a style. |
importScripts | Loads code the review never saw. Also stripped from the sandbox. |
Computed import() | Loads a module chosen at runtime. |
Remote import() | Defeats both the review and the published checksum. |
WebAssembly.compile/instantiate | Opaque bytes; a reviewer cannot read them. |
globalThis[…] | How stripped capabilities get rebuilt by name. |
String setTimeout | eval with extra steps. |
| Minified source | A line over 2,000 characters, or a mean line length over 220. |
| Packed payload | A long, high-entropy string literal. |
| Decode-then-execute | atob or fromCharCode feeding an executor. |
retrieval does not trip the
eval rule, and neither does a comment about it. A checker
that cries wolf is one authors learn to ignore, so it is built not to.
7What a person looks at
Some permission shapes are always read by a human, whatever the code scans like. Not because they are forbidden — several built-ins use them — but because the sandbox cannot decide whether they are appropriate:
| Shape | Why |
|---|---|
terminal | A shell as the user. Nothing the sandbox does constrains what happens next. |
Whole-vault read + any network: | The exfiltration shape: read everything, send it somewhere. |
secret: + any network: | How a stolen credential leaves the machine. |
device:sms, device:commands | Reading text messages, or running commands from a phone. |
vault:write (whole vault) | Can change every note. Declare the folder you write to. |
| Any first version | Also the moment to notice a name that passes for something else. |
| Any newly added permission | See §8. |
The reviewer is checking three things: that the declared permissions
match what the code actually does, that the description matches the
behaviour, and that a network: grant points somewhere the
description names.
8Updating a plugin
Bump version and publish again. Two rules govern what
happens:
- Published versions are immutable. The manifest, permissions, checksum and bytes of a version can never change. New bytes are always a new version. This is what makes the checksum the app verifies mean anything.
- A version that adds a permission is never listed automatically. However clean it scans. Users are shown exactly which permissions are newly asked for, and asked again.
Nothing is ever installed for anyone automatically. Slate reports that an update exists; a person chooses. An automatic update is the delivery mechanism for the one attack this design most needs to survive — a good plugin whose next version is not.
9Withdrawal and suspension
Ask a maintainer to withdraw a plugin and the listing stays visible, marked withdrawn with a reason. It stops being downloadable immediately, and copies already installed find out the next time they are enabled — because pulling something from the catalogue protects the people who have not installed it yet, which is the group that needs protecting least.
Anyone signed in can report a plugin as malicious, a privacy problem, broken, or impersonating something. A publisher can be blocked, which hides their plugins and stops updates. A publisher can never lift their own suspension, set their own verified badge, or change their handle.
10What stops a malicious plugin
Five layers, weakest trust first. The two that need no trust at all sit on the outside.
| Layer | Stops | Trust needed |
|---|---|---|
| The sandbox | Anything undeclared | none |
| The publish gate | Unreadable code, sandbox escapes | the server |
| Review | Dangerous permission shapes | maintainers |
| Integrity & consent | Substituted bytes, silent escalation | none |
| Suspension | Everything the first four missed | maintainers |
The app verifies everything itself before writing a single file: it hashes what arrived and compares it against the published checksum, refuses a bundle whose manifest is a different plugin or version than the catalogue named, rejects unsafe paths, revalidates the manifest, and runs the whole analyzer again locally. A compromised server gets as far as a refused install.
Being straight about the limits: a competent author who writes readable code and asks for narrow permissions can be listed after review. The sandbox bounds the damage to what those permissions allow; nothing bounds it further. That is why the permission list is shown in plain words before anything runs, and why narrowing your scopes is the single most useful thing you can do for the people installing your work.
11Rules of the road
- Do not impersonate. Names or ids that pass for someone else's plugin, or for Slate itself, are refused.
- Say what you do. A description that does not match behaviour is grounds for suspension, and it is the most common thing reports are about.
- Ask for less. Every permission you drop is one fewer reason for someone to close the prompt.
- Handle being offline. Your plugin may run on a plane.
- Money and data leaving the machine go in the description. Not in a README nobody opens — in the sentence people read before they install.