# How Motrix's QuickJS Plugin Sandbox Works: Architecture, Security, and APIs

> Discover how Motrix's QuickJS plugin sandbox uses a Node.js worker thread and WebAssembly for secure isolation. Explore its architecture, APIs, and lifecycle management.

- Repository: [Dr_rOot/Motrix](https://github.com/agalwood/Motrix)
- Tags: architecture
- Published: 2026-08-19

---

**Motrix isolates plugins inside a QuickJS WebAssembly VM running in a Node.js worker thread, exposing a sealed `__motrix_plugin_api__` object with registration-only and effectful capabilities that are gated by lifecycle phases and enforced by a bridge protocol.**

Motrix, the open-source download manager, implements a secure plugin system using QuickJS to execute untrusted code safely. The sandbox runs each plugin in an isolated virtual machine without access to Node.js APIs, exposing functionality through a carefully controlled bridge. This architecture ensures that third-party extensions can extend Motrix's capabilities without compromising system security.

## Architecture Overview

### Worker Thread Isolation

Each Motrix plugin executes inside a dedicated Node.js `Worker` thread defined in [`src/core/plugin/host/quick-js-worker.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/plugin/host/quick-js-worker.ts). The worker loads the QuickJS WebAssembly runtime (`quickjs-emscripten`) and creates a fresh VM context for every plugin instance. This isolation prevents plugins from accessing the main process memory or Node.js native modules directly.

The worker entry point spawns at lines 27-34, then waits for an initialization message containing the plugin bundle, manifest metadata, and locale settings before booting the VM.

### VM Initialization and Boot Sequence

The `boot()` function (lines 104-125) orchestrates VM startup:

1. Loads the QuickJS WASM runtime via `getQuickJS()`
2. Creates a new `QuickJSContext`
3. Installs timer globals with quota enforcement via `setupGlobals()`
4. Injects the plugin API through `injectPluginApi()`
5. Evaluates the transformed plugin bundle using `vm.evalCode()`

Upon successful evaluation, the worker posts `{type:'ready'}` to the host and enters the idle phase, signaling that the plugin is ready to receive hooks and commands.

## Plugin Lifecycle Phases

The Motrix QuickJS plugin sandbox enforces a strict lifecycle that controls when specific operations are permitted.

- **Spawn**: The main process creates the worker thread running [`quick-js-worker.ts`](https://github.com/agalwood/Motrix/blob/main/quick-js-worker.ts)
- **Init**: The host sends a `BridgeInitMessage` containing the bundle source and manifest; the worker resolves its `initPromise` (lines 56-66)
- **Boot**: The VM initializes, installs globals, and evaluates the plugin code
- **Ready**: The worker confirms successful activation and transitions to idle
- **Hook Execution**: When the orchestrator fires an event, the host sends `hookEnter`, the worker retrieves the registered handler from `registeredHooks`, builds a `ctx` object, executes the handler via `handleHookEnter` (lines 197-260), and posts `hookExit`
- **Command Execution**: `executeCommand` events route to local handlers or forward across the bridge via `handleExecuteCommand`
- **Deactivate/Shutdown**: `handleDeactivate` runs lifecycle cleanup handlers before termination, while `shutdown` forces immediate process exit

## Security Model and Capability Classification

### Phase-Gated Access Control

Motrix classifies all plugin APIs as either **registration-only** or **effectful** using definitions in [`src/core/plugin/capabilities/classification.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/plugin/capabilities/classification.ts). During the **activation** phase, only registration-only methods (like `hooks.register` or `commands.register`) are permitted. Any attempt to invoke an effectful method triggers `assertEffectfulAllowed` (lines 82-95), which raises a fatal error with code `plugin.lifecycle.activation_capability_violation` and terminates the plugin.

Effectful methods—those that perform I/O, network requests, or state changes—use the `makeEffectfulNs()` wrapper. This utility marshals arguments via `jsValueToVmHandle`, sends a `call` message across the bridge, and returns a QuickJS Promise that resolves with the host's response.

### Resource Limits and Quotas

The sandbox enforces strict resource constraints through `setupGlobals()`:

- **Timer limits**: A maximum of `MAX_ACTIVE = 100` concurrent timers (`setTimeout`/`setInterval`)
- **Delay ceiling**: Maximum delay of `MAX_DELAY = 30` seconds
- **Fire-and-forget logging**: `log.info()`, `log.debug()`, and related methods bypass phase gates and run immediately without bridge round-trips

### Sandboxed File System

All filesystem operations route through `fs.storage` and `fs.task` capabilities injected into the VM. These methods enforce the plugin-root sandbox implemented in [`src/core/plugin/capabilities/fs-sandbox.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/plugin/capabilities/fs-sandbox.ts), preventing directory traversal outside the plugin's dedicated storage directory. Read, write, delete, and rename operations all require bridge confirmation and path validation before execution.

## Available Plugin APIs

### Core API Structure

The `injectPluginApi()` function (lines 85-115) constructs a sealed `globalThis.__motrix_plugin_api__` object containing namespaces for all capabilities. Effectful namespaces use `makeEffectfulNs()` to wrap bridge calls, while read-only namespaces (like `app` and `i18n`) return snapshots without bridge overhead.

### Registration-Only APIs

These methods are safe to call during activation and simply store callbacks or metadata:

- **`hooks.beforeCreate()`**, **`hooks.afterComplete()`**: Register lifecycle interceptors for download tasks
- **`commands.register()`**: Exposes local commands callable via the command palette
- **`lifecycle.onDeactivate()`**: Registers cleanup handlers that run before plugin shutdown

### Effectful APIs

Available only after successful activation, these methods perform privileged operations:

- **`http.request()`**, **`http.get()`**, **`http.post()`**: Make network requests with configurable headers and payloads
- **`storage.set()`**, **`metadata.set()`**: Persist plugin data to Motrix's key-value store
- **`notify.show()`**: Display UI toast notifications to the user
- **`crypto.hash()`**, **`crypto.hmac()`**, **`crypto.randomBytes()`**, **`crypto.aes()`**: Cryptographic operations including hashing and encryption
- **`config.get()`**: Read-only access to Motrix configuration values

### File System Operations

The **`fs.storage`** namespace provides sandboxed file access:

- `read(path)`: Returns `Uint8Array`
- `write(path, data)`: Accepts `Uint8Array` or strings
- `delete(path)`, `rename(oldPath, newPath)`: Mutation operations
- `exists(path)`, `stat(path)`: Metadata queries

All paths are resolved relative to the plugin's root directory and validated against escape attempts.

### FFmpeg Integration

The **`ffmpeg`** namespace exposes video processing capabilities:

- `transcode({input, output, videoCodec})`: Returns a handle object
- `extractAudio({input, output})`: Audio extraction

Each method returns a handle containing:
- `result`: A Promise resolving when the operation completes
- `pollProgress()`: Returns current progress percentage
- `abort()`: Terminates the FFmpeg process

## Bridge Protocol and Data Marshaling

### Host-Worker Communication

The bridge protocol defined in [`src/core/plugin/host/bridge-protocol.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/plugin/host/bridge-protocol.ts) standardizes all cross-boundary communication using typed messages:

**Host to Worker:**
- `init`: Carries locale data and bundle source
- `call`: Invokes effectful capabilities with `{id, capability, method, args}`
- `event`: Lifecycle events like `hookEnter`, `executeCommand`, `localeChange`

**Worker to Host:**
- `ready`: VM boot successful
- `fatal`: Unrecoverable error with stack trace
- `response`: Result of effectful call with `{id, ok, result|error}`
- `event`: Plugin-initiated events

### Data Serialization

Argument marshaling uses `jsValueToVmHandle()` (lines 75-95) to convert JavaScript primitives, arrays, and plain objects into QuickJS handles. Typed arrays are serialized as numeric arrays. Return values are extracted via `vm.dump(handle)` and transmitted back to the host. Errors include standardized codes via `vmErrorCode` and messages via `vmErrorMessage` for programmatic error handling.

## Implementation Examples

### Basic Plugin with Logging and Hooks

```typescript
// plugin.ts – bundled and transformed by Motrix's build step
export default async function (motrix) {
  const { log, hooks } = motrix

  log.info('Plugin loaded!')

  hooks.beforeCreate((ctx) => {
    log.debug('Before create hook', { taskId: ctx.taskId })
  })
}

```

### Making HTTP Requests

```typescript
export default async function (motrix) {
  const { http, log } = motrix

  // Effectful call – allowed only after activation
  const resp = await http.request({
    url: 'https://api.github.com/repos/agalwood/Motrix',
    method: 'GET',
  })

  const data = await resp.json()
  log.info('Repo data received', { stars: data.stargazers_count })
}

```

### Sandboxed File Storage

```typescript
export default async function (motrix) {
  const { fs, log } = motrix

  await fs.storage.write('my.txt', new Uint8Array([72, 105])) // "Hi"
  const buf = await fs.storage.read('my.txt')
  log.info('Read back', { text: new TextDecoder().decode(buf) })
}

```

### FFmpeg Transcoding

```typescript
export default async function (motrix) {
  const { ffmpeg, log } = motrix

  const handle = await ffmpeg.transcode({
    input: '/path/in.mp4',
    output: '/path/out.mp4',
    videoCodec: 'libx264',
  })

  const result = await handle.result   // resolves when transcode finishes
  log.info('Transcode finished', { result })
}

```

## Summary

- **Isolated Execution**: Motrix runs plugins in QuickJS VMs inside Node.js worker threads, preventing access to native APIs.
- **Phase-Gated Security**: The [`classification.ts`](https://github.com/agalwood/Motrix/blob/main/classification.ts) system enforces registration-only calls during activation and effectful calls afterward, aborting violations immediately.
- **Comprehensive API**: Plugins access HTTP, sandboxed FS, FFmpeg, crypto, and UI notifications through `globalThis.__motrix_plugin_api__`.
- **Bridge Architecture**: All privileged operations serialize through [`bridge-protocol.ts`](https://github.com/agalwood/Motrix/blob/main/bridge-protocol.ts) messages with `jsValueToVmHandle` marshaling.
- **Resource Limits**: Built-in quotas restrict active timers (max 100) and delays (max 30s) to prevent resource exhaustion.

## Frequently Asked Questions

### What JavaScript engine powers Motrix's plugin sandbox?

Motrix uses **QuickJS** compiled to WebAssembly via the `quickjs-emscripten` package. The engine runs inside a Node.js worker thread defined in [`src/core/plugin/host/quick-js-worker.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/plugin/host/quick-js-worker.ts), creating a fresh VM context for each plugin without access to Node.js native modules or the filesystem.

### How does Motrix prevent plugins from accessing the filesystem outside their directory?

All filesystem operations route through `fs.storage` methods that enforce the plugin-root sandbox implemented in [`src/core/plugin/capabilities/fs-sandbox.ts`](https://github.com/agalwood/Motrix/blob/main/src/core/plugin/capabilities/fs-sandbox.ts). The host resolves all paths relative to the plugin's dedicated storage directory and rejects path traversal attempts (like `../`) before executing any read or write operations.

### What happens if a plugin tries to make an HTTP request during the activation phase?

The sandbox calls `assertEffectfulAllowed` (lines 82-95) before executing any effectful operation. If the plugin is still in the activation phase, this check fails and triggers a fatal error with code `plugin.lifecycle.activation_capability_violation`. The worker immediately terminates the plugin process and reports the violation to the host.

### Can plugins execute system commands or access native binaries like FFmpeg?

Plugins cannot execute arbitrary system commands, but they can access sandboxed FFmpeg functionality through the **`ffmpeg`** API namespace. Methods like `ffmpeg.transcode()` return a handle that communicates with the host's FFmpeg binary through the bridge protocol, allowing video processing without exposing shell access or binary paths to the plugin code.