Developing Plugins
A Motrix plugin is TypeScript or JavaScript bundled into a single ES2020 module. It runs inside an isolated sandbox: there are no Node.js APIs, no require, and no direct file or network access. Everything that reaches the outside world goes through capabilities you declare in the manifest, and Motrix shows the user exactly what you declared before it installs anything. Design for least privilege — every permission you add is a line on someone’s consent screen, and a broad host pattern makes your plugin look scarier than it is.
What a plugin can do
Plugins attach to the download lifecycle. Each hook receives a context object you inspect and, where allowed, mutate.
| Hook | When it runs | What you can do |
|---|---|---|
beforeCreate | Before a download is created | Inspect uris, headers, saveDir; ctx.update({ uris, filename, connections, headers, proxy }) |
beforeFinalize | Data is complete, file not yet finalized | ctx.update({ filePath }) to rename |
afterComplete | File is finalized on disk | Side effects: notify, post-process, hand off to another tool |
onError | A task failed | Read error.code / error.message, log or notify |
Beyond hooks, a plugin can:
- Contribute commands — callable entry points other plugins or the host can invoke, with JSON-Schema-validated arguments and results.
- Ship settings — declare a configuration schema and users edit the values in Motrix’s own UI, on the plugin’s Settings tab. Your code reads them through the
configcapability.
Every hook context carries an AbortSignal (ctx.signal) — honor it in long-running work — and a metadata store for passing values between your own hooks across one task’s lifetime.
Quick start
pnpm create motrix-plugin my-plugin # basic-resolver template
pnpm create motrix-plugin my-plugin post-action # template as second argument
cd my-plugin && pnpm install
| Template | Starts you with |
|---|---|
basic-resolver (default) | A beforeCreate hook that inspects and rewrites downloads (site-resolver category) |
post-action | An afterComplete hook that fires a desktop notification (post-action category) |
The scaffold:
my-plugin/
├── motrix-plugin.json # the manifest — identity, permissions, hooks
├── src/index.ts # your entry point
├── locales/ # en-US.json, zh-CN.json (UI strings)
├── esbuild.config.mjs # standalone build
├── package.json # scripts: build / pack / dev
└── tsconfig.json
Your first hook is already wired:
import { hooks, log } from 'motrix:plugin-api'
hooks.beforeCreate(async (ctx) => {
log.info('resolving', { uri: ctx.uris[0] })
// rewrite the download before it starts:
// ctx.update({ filename: 'nicer-name.zip' })
return ctx
})
The scaffolded plugin id starts as me.<name>. Set your real publisher prefix by editing id in motrix-plugin.json, or scaffold with the full CLI, which takes flags:
pnpm --package=@motrix/plugin-cli dlx motrix-plugin init my-plugin -t post-action -p acme
The dev loop
pnpm dev # = motrix-plugin dev
dev watch-builds src/index.ts with inline sourcemaps and launches your local Motrix with the plugin loaded straight from your working directory. Edit code and the bundle rebuilds; edit motrix-plugin.json or locales/ and the host reloads the plugin. Motrix is located automatically from the standard install paths — set MOTRIX_BIN=/path/to/motrix if you run a build from somewhere else.
While it runs, your log.* calls stream into the plugin’s Logs tab inside Motrix, filtered by Log level. Turning on Verbose mode there captures full URLs and headers for one hour — useful for debugging a resolver, but it puts real URLs in the log, so leave it off otherwise.
Before you ship:
pnpm exec motrix-plugin validate # manifest against the official schema
pnpm run pack # minified bundle + .moext archive
pnpm exec motrix-plugin lint # static checks on the packed bundle
Tip
Use pnpm run pack, not pnpm pack — the bare form invokes pnpm’s built-in tarball command instead of the scaffold’s script. lint inspects the packed output, so pack first.
The manifest
motrix-plugin.json is your contract with the host. A minimal resolver:
{
"manifestVersion": 1,
"id": "acme.example-resolver",
"name": "Example Resolver",
"version": "0.1.0",
"description": "Rewrites example.com download links",
"categories": ["site-resolver"],
"engines": { "motrix": ">=2.0.0 <3.0.0" },
"main": "dist/plugin.js",
"permissions": ["http"],
"hostPermissions": ["https://example.com/*"],
"activationEvents": ["onTaskType:http"],
"contributes": {
"hooks": { "beforeCreate": { "role": "resolve" } }
},
"l10n": "locales"
}
Field rules, all enforced by motrix-plugin validate:
| Field | Rules |
|---|---|
id | <publisher>.<name>, lowercase a-z0-9-. The publisher prefixes motrix, verified, official, and system are reserved. |
version | Semver (1.2.3; prerelease and build suffixes allowed). |
categories | 1–8 of site-resolver, post-action, theme, productivity, integration. Hook roles are category-gated. |
engines.motrix | Semver range of supported Motrix versions. engines.ffmpeg is optional. |
permissions | Capabilities you require. The auto-injected ones (log, i18n, config, lifecycle, commands, app, crypto) must not be listed. Max 32. |
optionalPermissions | Capabilities you can live without — install succeeds even when the user denies them; check .available at runtime. |
hostPermissions | URL match patterns gating what http may reach (https://*.example.com/*, <all_urls>) — requests outside them fail with plugin.http.host_not_permitted. They also gate URL-scoped activation and are shown verbatim on the consent screen. Max 64. |
activationEvents | When the host loads you (onTaskType:http, onStartup, …). Required. |
contributes.hooks | Which lifecycle hooks you implement, each with a role. |
contributes.commands | Commands you register, id <publisher>.<plugin>.<command>, max 64. public: true commands must declare argsSchema + resultSchema (bounded JSON-Schema subset: no $ref/oneOf, ≤ 8 KiB, depth ≤ 8). |
contributes.configuration | Your settings schema, same bounded subset. |
requestedHeapMB | Sandbox heap, 32 (default) to 64. |
l10n | Directory of locale JSON files. |
Roles order execution when several plugins implement the same hook: resolve → enrich → post-process → audit. Two are category-gated — resolve requires site-resolver, post-process requires post-action. Hooks registered in code without a declared role run as enrich. (pre-resolve is reserved for built-in plugins.)
UI strings live in locales/<lang>.json and are read with i18n.t('key'). pack validates locale coverage, so a key missing from a shipped locale fails your build instead of the user’s session.
Capabilities and permissions
Import everything from the motrix:plugin-api virtual module. Types come from the @motrix/plugin-api package; the implementation is injected by the host, so never bundle it — the scaffold’s esbuild config already marks it external.
These are always available and must not appear in permissions:
| Namespace | What it gives you |
|---|---|
log | Structured logging: trace/debug/info/warn/error/fatal(msg, fields?) |
i18n | t(key, params?), current language/dir, change events |
config | Your contributes.configuration values, plus an onChange subscription |
lifecycle | onActivate / onDeactivate for setup and teardown |
commands | register(id, handler) / execute(id, args) across plugins |
app | Host version, platform, arch, runtime (electron/server), locale |
crypto | hash, hmac, randomBytes, aes (cbc/gcm) |
These are permission-gated. Declare them, then check .available before use:
| Namespace | Permission | What it gives you |
|---|---|---|
http | http, http.cookies | request/get/post with typed responses (text/json/bytes), timeouts, ranges, opt-in cookie jar — http(s) only and confined to your declared hostPermissions: out-of-scope URLs, including redirect targets, throw plugin.http.host_not_permitted |
storage | storage | Versioned key-value store with compareAndSet for safe concurrent updates |
fs.task | fs.task.read, fs.task.write | Read, stat, hash, or rename the file of the task your hook is handling |
fs.storage | fs.storage | A private scratch directory for your plugin |
notify | notify | Desktop notifications |
ffmpeg | ffmpeg | probe, transcode, extractAudio, mergeStreams, generateThumbnail with progress streams — requires FFmpeg on the user’s system, so declare it optional |
Users never read your permission strings verbatim; Motrix renders a plain-language line per permission on the install screen:
| You declare | The user reads |
|---|---|
http | Read websites — “Opens pages this plugin supports.” |
http.cookies | Use website cookies — “Accesses sites where you are signed in.” |
fs.task.read | Read download files — “Sees files in a task.” |
fs.task.write | Change download files — “Can add or update task files.” |
fs.storage | Save plugin files — “Keeps files for this plugin.” |
storage | Save settings — “Remembers plugin options.” |
notify | Send notifications — “Shows completion alerts.” |
ffmpeg | Use FFmpeg — “Processes audio or video.” |
Warning
The patterns <all_urls>, *://*/*, http://*/*, and https://*/* count as broad host access. Any one of them adds a red Broad host access panel to the install dialog telling the user your plugin “can read and modify downloads from any URL”. Enumerate the hosts you actually need instead; motrix-plugin validate-host-permissions reviews your patterns for common mistakes.
Graceful degradation is the expected pattern for optional capabilities:
import { notify } from 'motrix:plugin-api'
if (notify.available) {
await notify.show({ title: 'Done', body: ctx.filePath })
}
Sandbox rules
- No top-level side effects. Register hooks and command handlers at the top level; do the work inside them.
linttreats top-level effectful calls as errors — a plugin that computes at import time slows every Motrix launch. - No Node.js. There is no
fs,net,process, orrequire. The capabilities above are the entire surface. - Budget your heap. You get 32 MB, up to 64 via
requestedHeapMB. Stream instead of buffering:fs.task.openReaderandhttprange requests exist for exactly this. - Honor
ctx.signal. Users cancel downloads; a hook that ignores the abort signal holds the pipeline hostage. - Target ES2020, single ESM file. If you bring your own bundler, keep
motrix:plugin-apiexternal.
The SDK packages
| Package | What it is for |
|---|---|
create-motrix-plugin | The pnpm create motrix-plugin scaffolder |
@motrix/plugin-cli | The motrix-plugin CLI: init, dev, validate, pack, lint, validate-host-permissions |
@motrix/plugin-api | Guest-facing types and the motrix:plugin-api virtual-module declaration (a devDependency in your plugin) |
@motrix/plugin-manifest-schema | The manifest’s Zod schema — the single source of truth that both the CLI and the Motrix host validate against |
Because the CLI and the host share that schema, a manifest that passes motrix-plugin validate is a manifest Motrix will accept.
Distributing your plugin
motrix-plugin pack produces dist/<id>-<version>.moext — a reproducible zip containing motrix-plugin.json, dist/plugin.js, your locale files, and icon.png / LICENSE / CHANGELOG.md when present. It enforces the distribution caps: bundle ≤ 1 MiB, archive ≤ 5 MiB.
Users install that file from Motrix’s Plugins page via Add plugin. One input accepts all three forms and detects which one you gave it:
| Form | What to paste |
|---|---|
| GitHub | owner/repo[@tag] — Motrix resolves the release asset |
| URL | A direct https link to a .moext |
| Local file | Pick or drag a .moext file into the input |
Motrix then shows the consent screen built from the manifest inside your package: your name and description, one row per permission (optional ones as switches the user can leave off), the Broad host access panel if applicable, and your host patterns under Advanced details. Install plugin commits it.
To appear in the in-app marketplace under Available plugins, submit an entry to the public plugin registry: one PR adding plugins/<your.plugin-id>.json to motrixapp/plugin-registry, pointing at an https release asset with its sha256 and size. Motrix verifies that hash before unpacking, and rejects a package whose manifest disagrees with the registry entry — so every new release needs a version-bump PR.
Important
Registry entries carry a preview of your permissions for the listing. The grants Motrix actually applies always come from the manifest inside the package. Installing straight from the registry works in the desktop app; in the web and server builds it is still coming soon, and users install from a URL or file instead.
Further reading
- Plugins — how users install, review, disable, and remove plugins, and where to find each plugin’s Settings and Logs tabs while you debug.
- Built-in plugins — the official plugins, useful as real-world manifest examples.
- motrixapp/plugin-sdk — the SDK repository and full README, including the complete CLI and capability reference.
- motrixapp/plugin-registry — registry schema, submission rules, and the data behind the marketplace.