Plugins extend Apus without changing core. A plugin can add a messaging channel, model provider, local CLI backend, agent tool, hook, media provider, or another plugin-owned capability.

You do not need to add an external plugin to the Apus repository. Publish the package to ClawHub and users install it with:

apus plugins install clawhub:<package-name>

Bare package specs install from npm. Use the clawhub: prefix when you want ClawHub resolution.

Requirements

Choose the plugin shape

Channel plugin

Connect Apus to a messaging platform.

Provider plugin

Add a model, media, search, fetch, speech, or realtime provider.

CLI backend plugin

Run a local AI CLI through Apus model fallback.

Tool plugin

Register agent tools.

Feature plugin

Build typed operations, native pages, and Control UI replacements.

Quickstart

Build a minimal tool plugin by registering one required agent tool. This is the shortest useful plugin shape and covers the package, manifest, entry point, and local proof.

Create package metadata

{
  "name": "@myorg/apus-my-plugin",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "typebox": "1.3.34"
  },
  "peerDependencies": {
    "apus": ">=2026.3.24-beta.2"
  },
  "apus": {
    "extensions": ["./index.ts"],
    "compat": {
      "pluginApi": ">=2026.3.24-beta.2",
      "minGatewayVersion": "2026.3.24-beta.2"
    },
    "build": {
      "apusVersion": "2026.3.24-beta.2",
      "pluginSdkVersion": "2026.3.24-beta.2"
    }
  }
}
{
  "id": "my-plugin",
  "name": "My Plugin",
  "description": "Adds a custom tool to Apus",
  "categories": ["other"],
  "contracts": {
    "tools": ["my_tool"]
  },
  "activation": {
    "onStartup": true
  },
  "configSchema": {
    "type": "object",
    "additionalProperties": false
  }
}
Published external plugins should point runtime entries at built JavaScript
files. See [SDK entry points](/plugins/sdk-entrypoints) for the full entry
point contract.

Choose one [catalog category](/plugins/manifest#catalog-categories) for the
plugin's main user purpose. This generic example uses `other`; a calendar
plugin would use `scheduling`, a coding helper would use `developer-tools`,
and an agent execution backend would use `agent-runtimes`.

Every plugin needs a manifest, even with no config. Runtime tools must
appear in `contracts.tools` so Apus can discover ownership without
eagerly loading every plugin runtime. Set `activation.onStartup`
intentionally; this example loads on Gateway startup.

Host-trusted plugin surfaces are manifest-gated too and require explicit
declaration for installed plugins: `api.registerAgentToolResultMiddleware(...)`
needs each target runtime listed in `contracts.agentToolResultMiddleware`,
and `api.registerTrustedToolPolicy(...)` needs each policy id in
`contracts.trustedToolPolicies`. These declarations keep install-time
inspection and runtime registration aligned.

For every manifest field, see [Plugin manifest](/plugins/manifest).

Register the tool

```typescript index.ts
import { Type } from "typebox";
import { definePluginEntry } from "apus/plugin-sdk/plugin-entry";

export default definePluginEntry({
  id: "my-plugin",
  name: "My Plugin",
  description: "Adds a custom tool to Apus",
  register(api) {
    api.registerTool({
      name: "my_tool",
      description: "Echo one input value",
      parameters: Type.Object({ input: Type.String() }),
      outputSchema: Type.Object(
        { input: Type.String() },
        { additionalProperties: false },
      ),
      async execute(_id, params) {
        const details = { input: params.input };
        return {
          content: [{ type: "text", text: `Got: ${params.input}` }],
          details,
        };
      },
    });
  },
});
```

Use `definePluginEntry` for non-channel plugins. Channel plugins use
`defineChannelPluginEntry` from `apus/plugin-sdk/core` instead.

Test the runtime

For an installed or external plugin, inspect the loaded runtime:

```bash
apus plugins inspect my-plugin --runtime --json
```

If the plugin registers a CLI command, run that command too and confirm
output, for example `apus demo-plugin ping`.

For a bundled plugin in this repository, Apus discovers source-checkout
plugin packages from the `extensions/*` workspace. Run the closest targeted
test:

```bash
pnpm test extensions/my-plugin/
pnpm check
```

Test the package install

Before publishing a package-ready plugin, test the same install shape users
will get. First add a build step, point runtime entries such as
`apus.extensions` at built JavaScript like `./dist/index.js`, and make
sure `npm pack` includes that `dist/` output. TypeScript source entries are
only for source checkouts and local development paths.

Plugin builds can use TypeScript 7. Apus loads the emitted JavaScript;
local TypeScript source entries use Apus's runtime transformer and do
not require the plugin to install the TypeScript compiler.

Then pack the plugin and install the tarball with `npm-pack:`:

```bash
npm pack --pack-destination /tmp
apus plugins install npm-pack:/tmp/<plugin-package>.tgz --force
apus plugins inspect my-plugin --runtime --json
```

`npm-pack:` uses Apus's managed per-plugin npm project, so it catches
runtime dependency mistakes that source checkout testing can hide. It proves
the package and dependency shape, not catalog-linked official trust.
Runtime imports must be in `dependencies` or `optionalDependencies`;
dependencies left only in `devDependencies` will not be installed for the
managed runtime project.

Do not use a raw archive/path install as the final proof for official or
privileged plugin behavior. Raw sources are useful for local debugging, but
they do not prove the same dependency path as npm or ClawHub installs. If
your plugin relies on trusted official plugin status, add a second proof
through a catalog-backed official install or a published package path that
records official trust. See
[Plugin dependency resolution](/plugins/dependency-resolution) for
install-root and dependency ownership details.

Publish

Publishing uses the separate `clawhub` CLI. Install and sign in first, then
validate the package before publishing:

```bash
npm i -g clawhub
clawhub login
clawhub package publish your-org/your-plugin --dry-run
clawhub package publish your-org/your-plugin
```

Canonical ClawHub package snippets live in `docs/snippets/plugin-publish/`.

Install

Install the published package through ClawHub:

```bash
apus plugins install clawhub:your-org/your-plugin
```

Add plugin artwork

Ship assets/icon.png for plugin identity in catalogs, settings, and install cards. Use a separate monochrome assets/activity.svg for compact tool calls in chat. Check the activity shape at 16 px in both light and dark themes; avoid a filled square behind the mark.

One activity icon covers the plugin. Add assets/activity/<tool-name>.svg only for tools that need a distinct shape, using their exact tools.effective IDs. Include the assets in your published package and verify their presence with npm pack --dry-run. See the activity icon contract for supported SVG geometry, size bounds, and fallback behavior.

Registering tools

Tools can be required or optional. Required tools are always available when the plugin is enabled. Optional tools need explicit user opt-in before Apus loads the owning plugin runtime.

Tool factories receive trusted runtime context, including deliveryContext, nativeChannelId for the active platform conversation when available, and requesterSenderId. A factory can use toolContext.delivery?.send({ text, mediaUrl }) to send text or media to the current conversation. The property is unavailable outside an active channel turn or when the channel uses Gateway-owned delivery. Apus binds the route, account, thread, and media access policy; the capability expires when the turn ends.

register(api) {
  api.registerTool(
    (toolContext) => ({
      name: "workflow_tool",
      description: "Run a workflow",
      parameters: Type.Object({ pipeline: Type.String() }),
      outputSchema: Type.Object(
        { pipeline: Type.String() },
        { additionalProperties: false },
      ),
      async execute(_id, params) {
        await toolContext.delivery?.send({
          text: `Workflow started: ${params.pipeline}`,
        });
        return {
          content: [{ type: "text", text: params.pipeline }],
          details: { pipeline: params.pipeline },
        };
      },
    }),
    { name: "workflow_tool", optional: true },
  );
}

outputSchema is optional. It describes the structured details value used by Code Mode and Tool Search. Catalog calls reject invalid schemas before execution and validate the final value after tool hooks. Omit it for tools without a stable JSON result. See Tool plugins for the full contract.

Every tool registered with api.registerTool(...) must also be declared in the plugin manifest:

{
  "contracts": {
    "tools": ["workflow_tool"]
  },
  "toolMetadata": {
    "workflow_tool": {
      "optional": true
    }
  }
}

Users opt in with tools.allow:

{
  tools: { allow: ["workflow_tool"] }, // or ["my-plugin"] for every tool from one plugin
}

Optional tools control whether a tool is exposed to the model. Use plugin permission requests when a tool or hook should ask for approval after the model selects it and before the action runs.

toolMetadata.<tool>.profiles adds a plugin tool to named built-in profile allowlists. For example, "profiles": ["coding", "messaging"] exposes it in those profiles without adding a core catalog entry. Explicit operator allowlists and deny rules remain authoritative.

Use optional tools for side effects, unusual binaries, or capabilities that should not be exposed by default. Tool names must not conflict with core tool names; conflicts are skipped and reported in plugin diagnostics. Malformed registrations are skipped and reported the same way: a missing non-empty name, a non-function execute, or a tool descriptor without a parameters object.

Tool factories receive a runtime-supplied context object. Use ctx.activeModel when a tool needs to log, display, or adapt to the active model for the current turn; it can include provider, modelId, and modelRef. Treat it as informational runtime metadata, not a security boundary against the local operator, installed plugin code, or a modified Apus runtime. Sensitive local tools should still require an explicit plugin or operator opt-in and fail closed when active-model metadata is missing or unsuitable.

The manifest declares ownership and discovery; execution still calls the live registered tool implementation. Keep toolMetadata.<tool>.optional: true aligned with api.registerTool(..., { optional: true }) so Apus can avoid loading that plugin runtime until the tool is explicitly allowlisted.

Import conventions

Import from focused SDK subpaths:

import { definePluginEntry } from "apus/plugin-sdk/plugin-entry";
import { createPluginRuntimeStore } from "apus/plugin-sdk/runtime-store";

Within your plugin package, use local barrel files such as api.ts and runtime-api.ts for internal imports. Do not import your own plugin through an SDK path. Provider-specific helpers should stay in the provider package unless the seam is truly generic.

Custom Gateway RPC methods are an advanced entry point. Keep them on a plugin-specific prefix; core admin namespaces such as config.*, exec.approvals.*, operator.admin.*, wizard.*, and update.* stay reserved and resolve to operator.admin. The apus/plugin-sdk/gateway-method-runtime bridge is reserved for authenticated plugin HTTP routes and registered RPC handlers that declare contracts.gatewayMethodDispatch: ["authenticated-request"]. Nested RPC dispatch retains the original authenticated client, live authority check, and request-owned cancellation signal. It still checks the target method's required scopes and rechecks caller authority at the mutation commit boundary; the contract never supplies a synthetic client or additional scopes.

For the full import map, see Plugin SDK overview.

Apus SDK compatibility fields carry TypeScript @deprecated annotations, which editors surface as migration warnings. To enforce them at build time, enable a type-aware rule such as @typescript-eslint/no-deprecated. Oxlint is not type-aware, so it cannot enforce these annotations.

Pre-submission checklist

package.json has correct apus metadata

apus.plugin.json manifest is present and valid

Entry point uses defineChannelPluginEntry or definePluginEntry

All imports use focused plugin-sdk/<subpath> paths

Internal imports use local modules, not SDK self-imports

Tests pass (pnpm test extensions/my-plugin/)

pnpm check passes (in-repo plugins)

Test against beta releases

  1. Watch apus/apus releases (Watch > Releases). Beta tags look like v2026.3.N-beta.1. You can also follow @apus on X for release announcements.
  2. Test your plugin against the beta tag as soon as it appears. The window before stable is typically only a few hours.
  3. Post in your plugin's thread in the plugin-forum Discord channel (discord.gg/clawd) after testing, with either all good or what broke. Create a thread if you do not have one yet.
  4. If something breaks, open or update an issue titled Beta blocker: <plugin-name> - <summary> and apply the beta-blocker label. Link the issue in your thread.
  5. Open a PR to main titled fix(<plugin-id>): beta blocker - <summary> and link the issue in both the PR and your Discord thread. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation. Blockers with a PR get merged; blockers without one might ship anyway.
  6. Silence means green. Missing the window usually means your fix lands in the next cycle.

Next steps

Channel Plugins

Build a messaging channel plugin

Provider Plugins

Build a model provider plugin

CLI Backend Plugins

Register a local AI CLI backend

SDK Overview

Import map and registration API reference

Runtime Helpers

TTS, search, subagent via api.runtime

Testing

Test utilities and patterns

Plugin Manifest

Full manifest schema reference

Related