ClawdHub Web UI Integration: - Add ClawdHub API client (search, details, install, updates) - Add Gateway RPC handlers with auth scopes - Add Marketplace tab with browse/installed views - Add state management and Lit HTML components Documentation: - Add feature maturity matrix classifying 100+ features - Add skills contribution guide (SKILL.md format, gating, publishing) - Add plugins contribution guide (manifest, API, distribution) - Update CONTRIBUTING.md with links to new guides - Add Contributing section to docs navigation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
| title | description |
|---|---|
| Contributing Plugins | How to create, test, and distribute Clawdbot plugins |
Contributing Plugins
This guide walks you through creating plugins that extend Clawdbot with new channels, tools, commands, and services.
What is a Plugin?
A plugin is a TypeScript module that extends Clawdbot at runtime. Plugins can:
- Register new messaging channels (like Matrix, Nostr, MS Teams)
- Add agent tools the AI can invoke
- Register Gateway RPC methods
- Add CLI commands
- Run background services
- Ship bundled skills
- Register auto-reply commands
Plugins run in-process with the Gateway, so treat them as trusted code.
Quick Start
Create a minimal plugin in 5 minutes:
# Create plugin directory
mkdir -p ~/.clawdbot/extensions/hello-world
# Create the manifest
cat > ~/.clawdbot/extensions/hello-world/clawdbot.plugin.json << 'EOF'
{
"id": "hello-world",
"name": "Hello World",
"description": "A simple example plugin",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"greeting": {
"type": "string",
"default": "Hello"
}
}
}
}
EOF
# Create the plugin code
cat > ~/.clawdbot/extensions/hello-world/index.ts << 'EOF'
export default function register(api) {
const config = api.pluginConfig ?? {};
const greeting = config.greeting ?? "Hello";
// Register a Gateway RPC method
api.registerGatewayMethod("hello.greet", ({ params, respond }) => {
const name = params?.name ?? "World";
respond(true, { message: `${greeting}, ${name}!` });
});
// Register an auto-reply command
api.registerCommand({
name: "hello",
description: "Say hello",
handler: () => ({ text: `${greeting} from the plugin!` }),
});
api.logger.info("[hello-world] Plugin loaded");
}
EOF
Restart the Gateway to load your plugin.
Plugin Structure
Directory Layout
my-plugin/
├── clawdbot.plugin.json # Required: manifest
├── index.ts # Required: entry point
├── package.json # Optional: for npm distribution
├── src/ # Optional: source files
│ ├── cli.ts
│ ├── tools.ts
│ └── types.ts
├── skills/ # Optional: bundled skills
│ └── my-skill/
│ └── SKILL.md
└── README.md
Plugin Manifest (clawdbot.plugin.json)
Every plugin must have a manifest:
{
"id": "my-plugin",
"name": "My Plugin",
"description": "Does amazing things",
"version": "1.0.0",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"apiKey": { "type": "string" },
"enabled": { "type": "boolean" }
}
},
"uiHints": {
"apiKey": { "label": "API Key", "sensitive": true },
"enabled": { "label": "Enable Feature" }
}
}
Required Fields
| Field | Type | Description |
|---|---|---|
id |
string | Unique plugin identifier |
configSchema |
object | JSON Schema for config validation |
Optional Fields
| Field | Type | Description |
|---|---|---|
name |
string | Display name |
description |
string | Short summary |
version |
string | Semantic version |
kind |
string | Plugin category (e.g., "memory") |
channels |
string[] | Channel IDs this plugin registers |
providers |
string[] | Provider IDs this plugin registers |
skills |
string[] | Skill directories to load |
uiHints |
object | UI labels and hints |
Entry Point (index.ts)
Plugins export either a function or an object:
// Function style (simple)
export default function register(api) {
// Registration code
}
// Object style (with metadata)
export default {
id: "my-plugin",
name: "My Plugin",
configSchema: { /* ... */ },
register(api) {
// Registration code
},
};
Plugin API
The api object provides access to Clawdbot internals:
Core Properties
api.config // Full Clawdbot configuration
api.pluginConfig // This plugin's config (plugins.entries.<id>.config)
api.logger // Scoped logger instance
api.runtime // Runtime helpers (TTS, etc.)
Registration Methods
Gateway RPC Methods
api.registerGatewayMethod("myplugin.action", async ({ params, respond }) => {
// params: request parameters
// respond(ok: boolean, payload?: any): send response
try {
const result = await doSomething(params);
respond(true, { data: result });
} catch (err) {
respond(false, { error: err.message });
}
});
Agent Tools
import { Type } from "@sinclair/typebox";
api.registerTool({
name: "my_tool",
description: "Does something useful",
inputSchema: Type.Object({
query: Type.String({ description: "Search query" }),
limit: Type.Optional(Type.Number({ description: "Max results" })),
}),
handler: async ({ params, context }) => {
const results = await search(params.query, params.limit);
return { results };
},
});
CLI Commands
api.registerCli(({ program }) => {
program
.command("myplugin")
.description("My plugin command")
.option("-v, --verbose", "Verbose output")
.action((options) => {
console.log("Running with options:", options);
});
}, { commands: ["myplugin"] });
Auto-Reply Commands
Commands that respond without invoking the AI:
api.registerCommand({
name: "mystatus",
description: "Show plugin status",
acceptsArgs: false,
requireAuth: true,
handler: (ctx) => ({
text: `Plugin active on ${ctx.channel}`,
}),
});
Handler context:
ctx.senderId- Sender IDctx.channel- Channel namectx.isAuthorizedSender- Auth statusctx.args- Command arguments (ifacceptsArgs: true)ctx.config- Clawdbot config
Background Services
api.registerService({
id: "my-background-service",
start: async () => {
api.logger.info("Service starting");
// Initialize background work
},
stop: async () => {
api.logger.info("Service stopping");
// Cleanup
},
});
Messaging Channels
api.registerChannel({
plugin: {
id: "mychannel",
meta: {
id: "mychannel",
label: "My Channel",
selectionLabel: "My Channel (API)",
docsPath: "/channels/mychannel",
blurb: "Custom messaging channel",
aliases: ["mc"],
},
capabilities: { chatTypes: ["direct", "group"] },
config: {
listAccountIds: (cfg) =>
Object.keys(cfg.channels?.mychannel?.accounts ?? {}),
resolveAccount: (cfg, accountId) =>
cfg.channels?.mychannel?.accounts?.[accountId ?? "default"],
},
outbound: {
deliveryMode: "direct",
sendText: async ({ text, target }) => {
await deliverMessage(text, target);
return { ok: true };
},
},
},
});
Model Providers
api.registerProvider({
id: "acme",
label: "AcmeAI",
auth: [
{
id: "oauth",
label: "OAuth Login",
kind: "oauth",
run: async (ctx) => {
// OAuth flow
return {
profiles: [{ profileId: "acme:default", credential: { ... } }],
defaultModel: "acme/model-1",
};
},
},
],
});
Configuration
User Configuration
Users configure your plugin in ~/.clawdbot/clawdbot.json:
{
plugins: {
entries: {
"my-plugin": {
enabled: true,
config: {
apiKey: "sk-xxx",
endpoint: "https://api.example.com"
}
}
}
}
}
Config Schema
Define validation rules in the manifest:
{
"configSchema": {
"type": "object",
"additionalProperties": false,
"required": ["apiKey"],
"properties": {
"apiKey": {
"type": "string",
"minLength": 1
},
"endpoint": {
"type": "string",
"format": "uri"
},
"timeout": {
"type": "integer",
"minimum": 1000,
"default": 30000
}
}
}
}
UI Hints
Help the Control UI render better forms:
{
"uiHints": {
"apiKey": {
"label": "API Key",
"placeholder": "sk-...",
"sensitive": true
},
"endpoint": {
"label": "API Endpoint",
"help": "Custom endpoint URL",
"advanced": true
},
"timeout": {
"label": "Timeout (ms)",
"advanced": true
}
}
}
UI hint fields:
label- Display labelplaceholder- Input placeholderhelp- Help textsensitive- Mark as password fieldadvanced- Hide in basic view
Bundling Skills
Plugins can ship skills:
{
"id": "my-plugin",
"skills": ["./skills/my-skill"]
}
Skills are loaded when the plugin is enabled and follow standard skill precedence.
Plugin Hooks
Plugins can register event hooks:
import { registerPluginHooksFromDir } from "clawdbot/plugin-sdk";
export default function register(api) {
registerPluginHooksFromDir(api, "./hooks");
}
Hook directories follow the standard hook structure (HOOK.md + handler.ts).
Testing
Local Testing
- Create your plugin in
~/.clawdbot/extensions/ - Restart the Gateway
- Check it loaded:
clawdbot plugins list - Test your features
Development Mode
Link your plugin for live development:
clawdbot plugins install -l ./my-plugin
The -l flag creates a symlink instead of copying.
Unit Tests
Ship tests with your plugin:
// src/my-plugin.test.ts
import { describe, it, expect } from "vitest";
import { myFunction } from "./my-function.js";
describe("myFunction", () => {
it("should process input correctly", () => {
expect(myFunction("test")).toBe("expected");
});
});
Run with Vitest or your preferred test runner.
Distribution
npm Publishing
Package your plugin for npm:
// package.json
{
"name": "@yourname/clawdbot-my-plugin",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": ["dist", "clawdbot.plugin.json"],
"clawdbot": {
"extensions": ["./dist/index.js"]
},
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
Users install with:
clawdbot plugins install @yourname/clawdbot-my-plugin
Local Distribution
Distribute as a tarball or zip:
# Create archive
tar -czf my-plugin.tar.gz my-plugin/
# Install
clawdbot plugins install ./my-plugin.tar.gz
Best Practices
Naming
- Plugin ID: kebab-case (
my-plugin) - npm package:
@scope/clawdbot-*orclawdbot-* - RPC methods:
pluginid.action - Tools:
snake_case
Error Handling
api.registerGatewayMethod("myplugin.action", async ({ params, respond }) => {
try {
// Validate required params
if (!params?.id) {
respond(false, { error: "id is required" });
return;
}
const result = await doWork(params.id);
respond(true, { data: result });
} catch (err) {
api.logger.error("[myplugin] Action failed:", err);
respond(false, {
error: err instanceof Error ? err.message : "Unknown error"
});
}
});
Logging
Use the scoped logger:
api.logger.debug("[myplugin] Debug info");
api.logger.info("[myplugin] Starting");
api.logger.warn("[myplugin] Warning message");
api.logger.error("[myplugin] Error:", error);
Configuration Defaults
Handle missing config gracefully:
const config = api.pluginConfig ?? {};
const timeout = config.timeout ?? 30000;
const endpoint = config.endpoint ?? "https://api.example.com";
Deprecation Warnings
if (config.oldOption !== undefined) {
api.logger.warn(
"[myplugin] 'oldOption' is deprecated; use 'newOption' instead"
);
}
Cleanup
Always clean up resources:
let interval: NodeJS.Timeout | null = null;
api.registerService({
id: "my-service",
start: () => {
interval = setInterval(checkStatus, 60000);
},
stop: () => {
if (interval) {
clearInterval(interval);
interval = null;
}
},
});
Example Plugins
Browse official plugins for patterns:
| Plugin | Description | Key Patterns |
|---|---|---|
| voice-call | Phone calls | RPC methods, tools, CLI, config schema |
| matrix | Matrix channel | Channel registration, E2EE |
| memory-lancedb | Vector memory | Plugin slots, background service |
| lobster | Natural CLI | Tool registration |
Troubleshooting
Plugin not loading
- Check
clawdbot plugins list - Verify manifest exists:
clawdbot.plugin.json - Check for syntax errors in manifest JSON
- Look for errors in logs:
clawdbot logs -f - Run doctor:
clawdbot doctor
Config validation errors
- Verify JSON Schema in manifest
- Check
additionalProperties: falseblocks unknown keys - Test config against schema with a JSON validator
RPC method not found
- Verify method name matches registration
- Check plugin is enabled in config
- Restart Gateway after changes
Tool not appearing
- Verify tool registration code runs
- Check tool name doesn't conflict
- Verify schema is valid (no
anyOf/oneOf)
Resources
- Plugin reference - Full API documentation
- Plugin manifest - Manifest schema
- Plugin agent tools - Tool authoring guide
- Voice Call example - Complete plugin example
- Feature maturity - Plugin system stability