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.

Right now, everything is reviewed Automatic listing is switched off until there is a maintainer rotation to back it up. Every submission queues for a person, however clean it scans. That is slower, and it is the honest posture while the queue is one human.

1Before you submit

Three things save the most time, in order:

  1. 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.
  2. 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.
  3. 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.json at the root of what you upload.
  • Every file named in entryPoints is in the bundle.
  • category set to one of the seven.
  • description is a real sentence, 10–200 characters, and describes behaviour.
  • id contains your publisher handle as a segment, is lowercase and dotted, has no underscores, and is at most 80 characters.
  • version is 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 remote import(), wasm, or packed payload strings.
  • Under 2 MB total, at most 200 files, no single file over 500 KB.
  • A README.md and a LICENSE.
  • Screenshots, if you have them: up to 4 https URLs in screenshots, 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.

  1. From the site. Browse pluginsPublish a plugin. Sign in, claim your handle, choose your plugin folder. The findings come back in the browser.
  2. From the app. ⌘PPublish 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

  1. Structure. Paths, size, entry points, manifest validity, id namespace, version collision. A failure here is immediate and tells you exactly what to fix.
  2. 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.
  3. A verdict. rejected, pending, or approved. Today everything clean lands on pending.
  4. 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:

FindingWhy
evalBuilds code at runtime, so nothing can see what the plugin does.
new Functioneval by another name.
.constructor.constructorReaching Function that way is a sandbox escape, not a style.
importScriptsLoads 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/instantiateOpaque bytes; a reviewer cannot read them.
globalThis[…]How stripped capabilities get rebuilt by name.
String setTimeouteval with extra steps.
Minified sourceA line over 2,000 characters, or a mean line length over 220.
Packed payloadA long, high-entropy string literal.
Decode-then-executeatob or fromCharCode feeding an executor.
The scanner reads past strings and comments A variable called 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:

ShapeWhy
terminalA 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:commandsReading text messages, or running commands from a phone.
vault:write (whole vault)Can change every note. Declare the folder you write to.
Any first versionAlso the moment to notice a name that passes for something else.
Any newly added permissionSee §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.

LayerStopsTrust needed
The sandboxAnything undeclarednone
The publish gateUnreadable code, sandbox escapesthe server
ReviewDangerous permission shapesmaintainers
Integrity & consentSubstituted bytes, silent escalationnone
SuspensionEverything the first four missedmaintainers

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.

What a verified badge means, and what it does not It means a maintainer confirmed who the publisher is. It does not mean the plugin is good, safe, or maintained. That is the sandbox's job and the consent prompt's job, and blurring the two would make the badge worse than useless. Verified publishers go through the identical gate.

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.