# Write a plugin

A plugin extends Speedwave with a containerized MCP worker Speedwave can call, or with a bundle of skills, commands, agents, and hooks. This page covers what a plugin is made of and how to get from source to a package Speedwave loads. Plugins are written, developed, and shipped exclusively by Speednet: a plugin built from this guide reaches users only after Speednet signs and releases it. If your plugin ships a worker, the [Write an MCP server](/docs/contributing/writing-an-mcp-server/) guide covers its boot contract and tool conventions.
**Only Speednet signs plugins:** Speedwave only loads plugins signed with Speednet's private key. You author and test the source, but send the finished result to Speednet for review and signing.

## What it does

Writing a plugin means producing source, not a finished package. You write a manifest and, for an MCP plugin, worker code and a container definition. Speednet turns that source into a signed ZIP, and Speedwave builds the image locally from verified contents.

## What it consists of

A `plugin.json` manifest with a `service_id`, a `Containerfile`, and the worker source. It may also ship a `claude-resources` directory.

A `plugin.json` manifest with neither `service_id` nor `Containerfile`, plus `claude-resources`: skills, commands, agents, and hooks. There is no worker container.

The manifest needs `name`, `slug`, `version`, and `description`. The `slug` is the identifier everything else derives from: install directory, config key, compose service name, worker environment variable, and token directory. It must match `^[a-z][a-z0-9-]{0,63}$` and cannot collide with a built-in service, whether by ID (`slack`, `github`, `office`), by derived compose name (`mcp-<slug>`), or bare (a slug of `claude` is rejected). For an MCP service plugin, `service_id` must equal `slug`.

Optional fields cover credentials (`auth_fields`), a settings form (`settings_schema`), setup notes (`instructions`), an OAuth flow (`oauth`), a desktop companion link (`host_bridge`), and resource caps (`mem_limit`, `cpu_limit`). See [Plugin mechanism](/docs/under-the-hood/plugin-mechanism/) for size limits and validation rules behind each field.

## How it works

For an MCP plugin, your `Containerfile` builds a worker that inherits [Speedwave's container hardening: dropped capabilities, no new privileges, a read-only root filesystem, and a noexec tmpfs at `/tmp`](/docs/security/isolation/). None of this is configurable, so design the worker to run unprivileged with nothing written to the root filesystem, within a cap of 16 GiB memory and 4 CPUs.

Two mounts cross the container boundary: `/tokens`, always read-only, where the worker reads credentials from `/tokens/<key>` and can never write; and `/workspace`, read-write, the only surface for state the worker needs to persist.

If the plugin authenticates through OAuth, declare an `oauth` block: [Speedwave's own `oauth` worker drives authorization and refresh, so long-lived secrets stay host-side and only a short-lived bearer token reaches the container](/docs/security/credentials/). Speedwave supports three grant types, each tied to a required manifest field: `authorization_code` needs `oauth.authorize_url`, `device_code` needs `oauth.device_authorization_url`, and `client_credentials` needs `oauth.client_secret_field`. Any endpoint URL you provide is validated before Speedwave accepts the manifest.

## Set it up

1. Write `plugin.json`, the `Containerfile` (for an MCP plugin), the worker source, and any `claude-resources`.

2. Send the source to Speednet for review and signing. Speednet builds the ZIP, signs it with the Ed25519 private key, and adds a `SIGNATURE` file.

3. Install the signed package in Speedwave, which verifies the signature against its embedded Speednet public key.

4. Speedwave runs install-time validation, then builds a local image from the verified source.

5. Enable the plugin for a project and fill in any credentials the manifest asks for.

Before you send the source off, test it locally: build the `Containerfile`, mount `/tokens` read-only and `/workspace` read-write, and confirm the worker starts unprivileged against a read-only root. That is the shape Speedwave enforces after install, so a mismatch caught now saves a signing round trip.

## Security boundaries

A plugin can reach its own `/workspace` and read its own `/tokens`. It cannot write credentials, escalate privileges, or reach another plugin's token directory. Speedwave re-verifies the signature every time it renders compose, builds the image, or lists the plugin in Desktop, so an altered package stops loading and nothing in `plugin.json` lets you self-sign or relax the hardening.

For how signature verification, the manifest schema, and storage work underneath, see [Plugin mechanism](/docs/under-the-hood/plugin-mechanism/).