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.

HookWhen it runsWhat you can do
beforeCreateBefore a download is createdInspect uris, headers, saveDir; ctx.update({ uris, filename, connections, headers, proxy })
beforeFinalizeData is complete, file not yet finalizedctx.update({ filePath }) to rename
afterCompleteFile is finalized on diskSide effects: notify, post-process, hand off to another tool
onErrorA task failedRead 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 config capability.

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
TemplateStarts you with
basic-resolver (default)A beforeCreate hook that inspects and rewrites downloads (site-resolver category)
post-actionAn 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:

FieldRules
id<publisher>.<name>, lowercase a-z0-9-. The publisher prefixes motrix, verified, official, and system are reserved.
versionSemver (1.2.3; prerelease and build suffixes allowed).
categories1–8 of site-resolver, post-action, theme, productivity, integration. Hook roles are category-gated.
engines.motrixSemver range of supported Motrix versions. engines.ffmpeg is optional.
permissionsCapabilities you require. The auto-injected ones (log, i18n, config, lifecycle, commands, app, crypto) must not be listed. Max 32.
optionalPermissionsCapabilities you can live without — install succeeds even when the user denies them; check .available at runtime.
hostPermissionsURL 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.
activationEventsWhen the host loads you (onTaskType:http, onStartup, …). Required.
contributes.hooksWhich lifecycle hooks you implement, each with a role.
contributes.commandsCommands 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.configurationYour settings schema, same bounded subset.
requestedHeapMBSandbox heap, 32 (default) to 64.
l10nDirectory of locale JSON files.

Roles order execution when several plugins implement the same hook: resolveenrichpost-processaudit. 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:

NamespaceWhat it gives you
logStructured logging: trace/debug/info/warn/error/fatal(msg, fields?)
i18nt(key, params?), current language/dir, change events
configYour contributes.configuration values, plus an onChange subscription
lifecycleonActivate / onDeactivate for setup and teardown
commandsregister(id, handler) / execute(id, args) across plugins
appHost version, platform, arch, runtime (electron/server), locale
cryptohash, hmac, randomBytes, aes (cbc/gcm)

These are permission-gated. Declare them, then check .available before use:

NamespacePermissionWhat it gives you
httphttp, http.cookiesrequest/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
storagestorageVersioned key-value store with compareAndSet for safe concurrent updates
fs.taskfs.task.read, fs.task.writeRead, stat, hash, or rename the file of the task your hook is handling
fs.storagefs.storageA private scratch directory for your plugin
notifynotifyDesktop notifications
ffmpegffmpegprobe, 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 declareThe user reads
httpRead websites — “Opens pages this plugin supports.”
http.cookiesUse website cookies — “Accesses sites where you are signed in.”
fs.task.readRead download files — “Sees files in a task.”
fs.task.writeChange download files — “Can add or update task files.”
fs.storageSave plugin files — “Keeps files for this plugin.”
storageSave settings — “Remembers plugin options.”
notifySend notifications — “Shows completion alerts.”
ffmpegUse 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. lint treats 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, or require. 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.openReader and http range 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-api external.

The SDK packages

PackageWhat it is for
create-motrix-pluginThe pnpm create motrix-plugin scaffolder
@motrix/plugin-cliThe motrix-plugin CLI: init, dev, validate, pack, lint, validate-host-permissions
@motrix/plugin-apiGuest-facing types and the motrix:plugin-api virtual-module declaration (a devDependency in your plugin)
@motrix/plugin-manifest-schemaThe 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:

FormWhat to paste
GitHubowner/repo[@tag] — Motrix resolves the release asset
URLA direct https link to a .moext
Local filePick 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.