31 KiB
summary, read_when, title
| summary | read_when | title | ||
|---|---|---|---|---|
| OpenClaw plugins/extensions: discovery, config, and safety |
|
Plugins |
Plugins (Extensions)
Quick start (new to plugins?)
A plugin is just a small code module that extends OpenClaw with extra features (commands, tools, and Gateway RPC).
Most of the time, you’ll use plugins when you want a feature that’s not built into core OpenClaw yet (or you want to keep optional features out of your main install).
Fast path:
- See what’s already loaded:
openclaw plugins list
- Install an official plugin (example: Voice Call):
openclaw plugins install @openclaw/voice-call
Npm specs are registry-only (package name + optional version/tag). Git/URL/file specs are rejected.
- Restart the Gateway, then configure under
plugins.entries.<id>.config.
See Voice Call for a concrete example plugin. Looking for third-party listings? See Community plugins.
Available plugins (official)
- Microsoft Teams is plugin-only as of 2026.1.15; install
@openclaw/msteamsif you use Teams. - Memory (Core) — bundled memory search plugin (enabled by default via
plugins.slots.memory) - Memory (LanceDB) — bundled long-term memory plugin (auto-recall/capture; set
plugins.slots.memory = "memory-lancedb") - Voice Call —
@openclaw/voice-call - Zalo Personal —
@openclaw/zalouser - Matrix —
@openclaw/matrix - Nostr —
@openclaw/nostr - Zalo —
@openclaw/zalo - Microsoft Teams —
@openclaw/msteams - Google Antigravity OAuth (provider auth) — bundled as
google-antigravity-auth(disabled by default) - Gemini CLI OAuth (provider auth) — bundled as
google-gemini-cli-auth(disabled by default) - Qwen OAuth (provider auth) — bundled as
qwen-portal-auth(disabled by default) - Copilot Proxy (provider auth) — local VS Code Copilot Proxy bridge; distinct from built-in
github-copilotdevice login (bundled, disabled by default)
OpenClaw plugins are TypeScript modules loaded at runtime via jiti. Config validation does not execute plugin code; it uses the plugin manifest and JSON Schema instead. See Plugin manifest.
Plugins can register:
- Gateway RPC methods
- Gateway HTTP routes
- Agent tools
- CLI commands
- Background services
- Context engines
- Optional config validation
- Skills (by listing
skillsdirectories in the plugin manifest) - Auto-reply commands (execute without invoking the AI agent)
Plugins run in‑process with the Gateway, so treat them as trusted code. Tool authoring guide: Plugin agent tools.
Runtime helpers
Plugins can access selected core helpers via api.runtime. For telephony TTS:
const result = await api.runtime.tts.textToSpeechTelephony({
text: "Hello from OpenClaw",
cfg: api.config,
});
Notes:
- Uses core
messages.ttsconfiguration (OpenAI or ElevenLabs). - Returns PCM audio buffer + sample rate. Plugins must resample/encode for providers.
- Edge TTS is not supported for telephony.
For STT/transcription, plugins can call:
const { text } = await api.runtime.stt.transcribeAudioFile({
filePath: "/tmp/inbound-audio.ogg",
cfg: api.config,
// Optional when MIME cannot be inferred reliably:
mime: "audio/ogg",
});
Notes:
- Uses core media-understanding audio configuration (
tools.media.audio) and provider fallback order. - Returns
{ text: undefined }when no transcription output is produced (for example skipped/unsupported input).
Gateway HTTP routes
Plugins can expose HTTP endpoints with api.registerHttpRoute(...).
api.registerHttpRoute({
path: "/acme/webhook",
auth: "plugin",
match: "exact",
handler: async (_req, res) => {
res.statusCode = 200;
res.end("ok");
return true;
},
});
Route fields:
path: route path under the gateway HTTP server.auth: required. Use"gateway"to require normal gateway auth, or"plugin"for plugin-managed auth/webhook verification.match: optional."exact"(default) or"prefix".replaceExisting: optional. Allows the same plugin to replace its own existing route registration.handler: returntruewhen the route handled the request.
Notes:
api.registerHttpHandler(...)is obsolete. Useapi.registerHttpRoute(...).- Plugin routes must declare
authexplicitly. - Exact
path + matchconflicts are rejected unlessreplaceExisting: true, and one plugin cannot replace another plugin's route.
Plugin SDK import paths
Use SDK subpaths instead of the monolithic openclaw/plugin-sdk import when
authoring plugins:
openclaw/plugin-sdk/corefor generic plugin APIs, provider auth types, and shared helpers.openclaw/plugin-sdk/compatfor bundled/internal plugin code that needs broader shared runtime helpers thancore.openclaw/plugin-sdk/telegramfor Telegram channel plugins.openclaw/plugin-sdk/discordfor Discord channel plugins.openclaw/plugin-sdk/slackfor Slack channel plugins.openclaw/plugin-sdk/signalfor Signal channel plugins.openclaw/plugin-sdk/imessagefor iMessage channel plugins.openclaw/plugin-sdk/whatsappfor WhatsApp channel plugins.openclaw/plugin-sdk/linefor LINE channel plugins.openclaw/plugin-sdk/msteamsfor the bundled Microsoft Teams plugin surface.- Bundled extension-specific subpaths are also available:
openclaw/plugin-sdk/acpx,openclaw/plugin-sdk/bluebubbles,openclaw/plugin-sdk/copilot-proxy,openclaw/plugin-sdk/device-pair,openclaw/plugin-sdk/diagnostics-otel,openclaw/plugin-sdk/diffs,openclaw/plugin-sdk/feishu,openclaw/plugin-sdk/google-gemini-cli-auth,openclaw/plugin-sdk/googlechat,openclaw/plugin-sdk/irc,openclaw/plugin-sdk/llm-task,openclaw/plugin-sdk/lobster,openclaw/plugin-sdk/matrix,openclaw/plugin-sdk/mattermost,openclaw/plugin-sdk/memory-core,openclaw/plugin-sdk/memory-lancedb,openclaw/plugin-sdk/minimax-portal-auth,openclaw/plugin-sdk/nextcloud-talk,openclaw/plugin-sdk/nostr,openclaw/plugin-sdk/open-prose,openclaw/plugin-sdk/phone-control,openclaw/plugin-sdk/qwen-portal-auth,openclaw/plugin-sdk/synology-chat,openclaw/plugin-sdk/talk-voice,openclaw/plugin-sdk/test-utils,openclaw/plugin-sdk/thread-ownership,openclaw/plugin-sdk/tlon,openclaw/plugin-sdk/twitch,openclaw/plugin-sdk/voice-call,openclaw/plugin-sdk/zalo, andopenclaw/plugin-sdk/zalouser.
Compatibility note:
openclaw/plugin-sdkremains supported for existing external plugins.- New and migrated bundled plugins should use channel or extension-specific
subpaths; use
corefor generic surfaces andcompatonly when broader shared helpers are required.
Read-only channel inspection
If your plugin registers a channel, prefer implementing
plugin.config.inspectAccount(cfg, accountId) alongside resolveAccount(...).
Why:
resolveAccount(...)is the runtime path. It is allowed to assume credentials are fully materialized and can fail fast when required secrets are missing.- Read-only command paths such as
openclaw status,openclaw status --all,openclaw channels status,openclaw channels resolve, and doctor/config repair flows should not need to materialize runtime credentials just to describe configuration.
Recommended inspectAccount(...) behavior:
- Return descriptive account state only.
- Preserve
enabledandconfigured. - Include credential source/status fields when relevant, such as:
tokenSource,tokenStatusbotTokenSource,botTokenStatusappTokenSource,appTokenStatussigningSecretSource,signingSecretStatus
- You do not need to return raw token values just to report read-only
availability. Returning
tokenStatus: "available"(and the matching source field) is enough for status-style commands. - Use
configured_unavailablewhen a credential is configured via SecretRef but unavailable in the current command path.
This lets read-only commands report “configured but unavailable in this command path” instead of crashing or misreporting the account as not configured.
Performance note:
- Plugin discovery and manifest metadata use short in-process caches to reduce bursty startup/reload work.
- Set
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE=1orOPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE=1to disable these caches. - Tune cache windows with
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MSandOPENCLAW_PLUGIN_MANIFEST_CACHE_MS.
Discovery & precedence
OpenClaw scans, in order:
- Config paths
plugins.load.paths(file or directory)
- Workspace extensions
<workspace>/.openclaw/extensions/*.ts<workspace>/.openclaw/extensions/*/index.ts
- Global extensions
~/.openclaw/extensions/*.ts~/.openclaw/extensions/*/index.ts
- Bundled extensions (shipped with OpenClaw, mostly disabled by default)
<openclaw>/extensions/*
Most bundled plugins must be enabled explicitly via
plugins.entries.<id>.enabled or openclaw plugins enable <id>.
Default-on bundled plugin exceptions:
device-pairphone-controltalk-voice- active memory slot plugin (default slot:
memory-core)
Installed plugins are enabled by default, but can be disabled the same way.
Hardening notes:
- If
plugins.allowis empty and non-bundled plugins are discoverable, OpenClaw logs a startup warning with plugin ids and sources. - Candidate paths are safety-checked before discovery admission. OpenClaw blocks candidates when:
- extension entry resolves outside plugin root (including symlink/path traversal escapes),
- plugin root/source path is world-writable,
- path ownership is suspicious for non-bundled plugins (POSIX owner is neither current uid nor root).
- Loaded non-bundled plugins without install/load-path provenance emit a warning so you can pin trust (
plugins.allow) or install tracking (plugins.installs).
Each plugin must include a openclaw.plugin.json file in its root. If a path
points at a file, the plugin root is the file's directory and must contain the
manifest.
If multiple plugins resolve to the same id, the first match in the order above wins and lower-precedence copies are ignored.
Package packs
A plugin directory may include a package.json with openclaw.extensions:
{
"name": "my-pack",
"openclaw": {
"extensions": ["./src/safety.ts", "./src/tools.ts"]
}
}
Each entry becomes a plugin. If the pack lists multiple extensions, the plugin id
becomes name/<fileBase>.
If your plugin imports npm deps, install them in that directory so
node_modules is available (npm install / pnpm install).
Security guardrail: every openclaw.extensions entry must stay inside the plugin
directory after symlink resolution. Entries that escape the package directory are
rejected.
Security note: openclaw plugins install installs plugin dependencies with
npm install --ignore-scripts (no lifecycle scripts). Keep plugin dependency
trees "pure JS/TS" and avoid packages that require postinstall builds.
Channel catalog metadata
Channel plugins can advertise onboarding metadata via openclaw.channel and
install hints via openclaw.install. This keeps the core catalog data-free.
Example:
{
"name": "@openclaw/nextcloud-talk",
"openclaw": {
"extensions": ["./index.ts"],
"channel": {
"id": "nextcloud-talk",
"label": "Nextcloud Talk",
"selectionLabel": "Nextcloud Talk (self-hosted)",
"docsPath": "/channels/nextcloud-talk",
"docsLabel": "nextcloud-talk",
"blurb": "Self-hosted chat via Nextcloud Talk webhook bots.",
"order": 65,
"aliases": ["nc-talk", "nc"]
},
"install": {
"npmSpec": "@openclaw/nextcloud-talk",
"localPath": "extensions/nextcloud-talk",
"defaultChoice": "npm"
}
}
}
OpenClaw can also merge external channel catalogs (for example, an MPM registry export). Drop a JSON file at one of:
~/.openclaw/mpm/plugins.json~/.openclaw/mpm/catalog.json~/.openclaw/plugins/catalog.json
Or point OPENCLAW_PLUGIN_CATALOG_PATHS (or OPENCLAW_MPM_CATALOG_PATHS) at
one or more JSON files (comma/semicolon/PATH-delimited). Each file should
contain { "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] }.
Plugin IDs
Default plugin ids:
- Package packs:
package.jsonname - Standalone file: file base name (
~/.../voice-call.ts→voice-call)
If a plugin exports id, OpenClaw uses it but warns when it doesn’t match the
configured id.
Config
{
plugins: {
enabled: true,
allow: ["voice-call"],
deny: ["untrusted-plugin"],
load: { paths: ["~/Projects/oss/voice-call-extension"] },
entries: {
"voice-call": { enabled: true, config: { provider: "twilio" } },
},
},
}
Fields:
enabled: master toggle (default: true)allow: allowlist (optional)deny: denylist (optional; deny wins)load.paths: extra plugin files/dirsslots: exclusive slot selectors such asmemoryandcontextEngineentries.<id>: per‑plugin toggles + config
Config changes require a gateway restart.
Validation rules (strict):
- Unknown plugin ids in
entries,allow,deny, orslotsare errors. - Unknown
channels.<id>keys are errors unless a plugin manifest declares the channel id. - Plugin config is validated using the JSON Schema embedded in
openclaw.plugin.json(configSchema). - If a plugin is disabled, its config is preserved and a warning is emitted.
Plugin slots (exclusive categories)
Some plugin categories are exclusive (only one active at a time). Use
plugins.slots to select which plugin owns the slot:
{
plugins: {
slots: {
memory: "memory-core", // or "none" to disable memory plugins
contextEngine: "legacy", // or a plugin id such as "lossless-claw"
},
},
}
Supported exclusive slots:
memory: active memory plugin ("none"disables memory plugins)contextEngine: active context engine plugin ("legacy"is the built-in default)
If multiple plugins declare kind: "memory" or kind: "context-engine", only
the selected plugin loads for that slot. Others are disabled with diagnostics.
Context engine plugins
Context engine plugins own session context orchestration for ingest, assembly,
and compaction. Register them from your plugin with
api.registerContextEngine(id, factory), then select the active engine with
plugins.slots.contextEngine.
Use this when your plugin needs to replace or extend the default context pipeline rather than just add memory search or hooks.
Control UI (schema + labels)
The Control UI uses config.schema (JSON Schema + uiHints) to render better forms.
OpenClaw augments uiHints at runtime based on discovered plugins:
- Adds per-plugin labels for
plugins.entries.<id>/.enabled/.config - Merges optional plugin-provided config field hints under:
plugins.entries.<id>.config.<field>
If you want your plugin config fields to show good labels/placeholders (and mark secrets as sensitive),
provide uiHints alongside your JSON Schema in the plugin manifest.
Example:
{
"id": "my-plugin",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"apiKey": { "type": "string" },
"region": { "type": "string" }
}
},
"uiHints": {
"apiKey": { "label": "API Key", "sensitive": true },
"region": { "label": "Region", "placeholder": "us-east-1" }
}
}
CLI
openclaw plugins list
openclaw plugins info <id>
openclaw plugins install <path> # copy a local file/dir into ~/.openclaw/extensions/<id>
openclaw plugins install ./extensions/voice-call # relative path ok
openclaw plugins install ./plugin.tgz # install from a local tarball
openclaw plugins install ./plugin.zip # install from a local zip
openclaw plugins install -l ./extensions/voice-call # link (no copy) for dev
openclaw plugins install @openclaw/voice-call # install from npm
openclaw plugins install @openclaw/voice-call --pin # store exact resolved name@version
openclaw plugins update <id>
openclaw plugins update --all
openclaw plugins enable <id>
openclaw plugins disable <id>
openclaw plugins doctor
plugins update only works for npm installs tracked under plugins.installs.
If stored integrity metadata changes between updates, OpenClaw warns and asks for confirmation (use global --yes to bypass prompts).
Plugins may also register their own top‑level commands (example: openclaw voicecall).
Plugin API (overview)
Plugins export either:
- A function:
(api) => { ... } - An object:
{ id, name, configSchema, register(api) { ... } }
Context engine plugins can also register a runtime-owned context manager:
export default function (api) {
api.registerContextEngine("lossless-claw", () => ({
info: { id: "lossless-claw", name: "Lossless Claw", ownsCompaction: true },
async ingest() {
return { ingested: true };
},
async assemble({ messages }) {
return { messages, estimatedTokens: 0 };
},
async compact() {
return { ok: true, compacted: false };
},
}));
}
Then enable it in config:
{
plugins: {
slots: {
contextEngine: "lossless-claw",
},
},
}
Plugin hooks
Plugins can register hooks at runtime. This lets a plugin bundle event-driven automation without a separate hook pack install.
Example
export default function register(api) {
api.registerHook(
"command:new",
async () => {
// Hook logic here.
},
{
name: "my-plugin.command-new",
description: "Runs when /new is invoked",
},
);
}
Notes:
- Register hooks explicitly via
api.registerHook(...). - Hook eligibility rules still apply (OS/bins/env/config requirements).
- Plugin-managed hooks show up in
openclaw hooks listwithplugin:<id>. - You cannot enable/disable plugin-managed hooks via
openclaw hooks; enable/disable the plugin instead.
Agent lifecycle hooks (api.on)
For typed runtime lifecycle hooks, use api.on(...):
export default function register(api) {
api.on(
"before_prompt_build",
(event, ctx) => {
return {
prependSystemContext: "Follow company style guide.",
};
},
{ priority: 10 },
);
}
Important hooks for prompt construction:
before_model_resolve: runs before session load (messagesare not available). Use this to deterministically overridemodelOverrideorproviderOverride.before_prompt_build: runs after session load (messagesare available). Use this to shape prompt input.before_agent_start: legacy compatibility hook. Prefer the two explicit hooks above.
Core-enforced hook policy:
- Operators can disable prompt mutation hooks per plugin via
plugins.entries.<id>.hooks.allowPromptInjection: false. - When disabled, OpenClaw blocks
before_prompt_buildand ignores prompt-mutating fields returned from legacybefore_agent_startwhile preserving legacymodelOverrideandproviderOverride.
before_prompt_build result fields:
prependContext: prepends text to the user prompt for this run. Best for turn-specific or dynamic content.systemPrompt: full system prompt override.prependSystemContext: prepends text to the current system prompt.appendSystemContext: appends text to the current system prompt.
Prompt build order in embedded runtime:
- Apply
prependContextto the user prompt. - Apply
systemPromptoverride when provided. - Apply
prependSystemContext + current system prompt + appendSystemContext.
Merge and precedence notes:
- Hook handlers run by priority (higher first).
- For merged context fields, values are concatenated in execution order.
before_prompt_buildvalues are applied before legacybefore_agent_startfallback values.
Migration guidance:
- Move static guidance from
prependContexttoprependSystemContext(orappendSystemContext) so providers can cache stable system-prefix content. - Keep
prependContextfor per-turn dynamic context that should stay tied to the user message.
Provider plugins (model auth)
Plugins can register model provider auth flows so users can run OAuth or API-key setup inside OpenClaw (no external scripts needed).
Register a provider via api.registerProvider(...). Each provider exposes one
or more auth methods (OAuth, API key, device code, etc.). These methods power:
openclaw models auth login --provider <id> [--method <id>]
Example:
api.registerProvider({
id: "acme",
label: "AcmeAI",
auth: [
{
id: "oauth",
label: "OAuth",
kind: "oauth",
run: async (ctx) => {
// Run OAuth flow and return auth profiles.
return {
profiles: [
{
profileId: "acme:default",
credential: {
type: "oauth",
provider: "acme",
access: "...",
refresh: "...",
expires: Date.now() + 3600 * 1000,
},
},
],
defaultModel: "acme/opus-1",
};
},
},
],
});
Notes:
runreceives aProviderAuthContextwithprompter,runtime,openUrl, andoauth.createVpsAwareHandlershelpers.- Return
configPatchwhen you need to add default models or provider config. - Return
defaultModelso--set-defaultcan update agent defaults.
Register a messaging channel
Plugins can register channel plugins that behave like built‑in channels
(WhatsApp, Telegram, etc.). Channel config lives under channels.<id> and is
validated by your channel plugin code.
const myChannel = {
id: "acmechat",
meta: {
id: "acmechat",
label: "AcmeChat",
selectionLabel: "AcmeChat (API)",
docsPath: "/channels/acmechat",
blurb: "demo channel plugin.",
aliases: ["acme"],
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
resolveAccount: (cfg, accountId) =>
cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? {
accountId,
},
},
outbound: {
deliveryMode: "direct",
sendText: async () => ({ ok: true }),
},
};
export default function (api) {
api.registerChannel({ plugin: myChannel });
}
Notes:
- Put config under
channels.<id>(notplugins.entries). meta.labelis used for labels in CLI/UI lists.meta.aliasesadds alternate ids for normalization and CLI inputs.meta.preferOverlists channel ids to skip auto-enable when both are configured.meta.detailLabelandmeta.systemImagelet UIs show richer channel labels/icons.
Channel onboarding hooks
Channel plugins can define optional onboarding hooks on plugin.onboarding:
configure(ctx)is the baseline setup flow.configureInteractive(ctx)can fully own interactive setup for both configured and unconfigured states.configureWhenConfigured(ctx)can override behavior only for already configured channels.
Hook precedence in the wizard:
configureInteractive(if present)configureWhenConfigured(only when channel status is already configured)- fallback to
configure
Context details:
configureInteractiveandconfigureWhenConfiguredreceive:configured(trueorfalse)label(user-facing channel name used by prompts)- plus the shared config/runtime/prompter/options fields
- Returning
"skip"leaves selection and account tracking unchanged. - Returning
{ cfg, accountId? }applies config updates and records account selection.
Write a new messaging channel (step‑by‑step)
Use this when you want a new chat surface (a "messaging channel"), not a model provider.
Model provider docs live under /providers/*.
- Pick an id + config shape
- All channel config lives under
channels.<id>. - Prefer
channels.<id>.accounts.<accountId>for multi‑account setups.
- Define the channel metadata
meta.label,meta.selectionLabel,meta.docsPath,meta.blurbcontrol CLI/UI lists.meta.docsPathshould point at a docs page like/channels/<id>.meta.preferOverlets a plugin replace another channel (auto-enable prefers it).meta.detailLabelandmeta.systemImageare used by UIs for detail text/icons.
- Implement the required adapters
config.listAccountIds+config.resolveAccountcapabilities(chat types, media, threads, etc.)outbound.deliveryMode+outbound.sendText(for basic send)
- Add optional adapters as needed
setup(wizard),security(DM policy),status(health/diagnostics)gateway(start/stop/login),mentions,threading,streamingactions(message actions),commands(native command behavior)
- Register the channel in your plugin
api.registerChannel({ plugin })
Minimal config example:
{
channels: {
acmechat: {
accounts: {
default: { token: "ACME_TOKEN", enabled: true },
},
},
},
}
Minimal channel plugin (outbound‑only):
const plugin = {
id: "acmechat",
meta: {
id: "acmechat",
label: "AcmeChat",
selectionLabel: "AcmeChat (API)",
docsPath: "/channels/acmechat",
blurb: "AcmeChat messaging channel.",
aliases: ["acme"],
},
capabilities: { chatTypes: ["direct"] },
config: {
listAccountIds: (cfg) => Object.keys(cfg.channels?.acmechat?.accounts ?? {}),
resolveAccount: (cfg, accountId) =>
cfg.channels?.acmechat?.accounts?.[accountId ?? "default"] ?? {
accountId,
},
},
outbound: {
deliveryMode: "direct",
sendText: async ({ text }) => {
// deliver `text` to your channel here
return { ok: true };
},
},
};
export default function (api) {
api.registerChannel({ plugin });
}
Load the plugin (extensions dir or plugins.load.paths), restart the gateway,
then configure channels.<id> in your config.
Agent tools
See the dedicated guide: Plugin agent tools.
Register a gateway RPC method
export default function (api) {
api.registerGatewayMethod("myplugin.status", ({ respond }) => {
respond(true, { ok: true });
});
}
Register CLI commands
export default function (api) {
api.registerCli(
({ program }) => {
program.command("mycmd").action(() => {
console.log("Hello");
});
},
{ commands: ["mycmd"] },
);
}
Register auto-reply commands
Plugins can register custom slash commands that execute without invoking the AI agent. This is useful for toggle commands, status checks, or quick actions that don't need LLM processing.
export default function (api) {
api.registerCommand({
name: "mystatus",
description: "Show plugin status",
handler: (ctx) => ({
text: `Plugin is running! Channel: ${ctx.channel}`,
}),
});
}
Command handler context:
senderId: The sender's ID (if available)channel: The channel where the command was sentisAuthorizedSender: Whether the sender is an authorized userargs: Arguments passed after the command (ifacceptsArgs: true)commandBody: The full command textconfig: The current OpenClaw config
Command options:
name: Command name (without the leading/)description: Help text shown in command listsacceptsArgs: Whether the command accepts arguments (default: false). If false and arguments are provided, the command won't match and the message falls through to other handlersrequireAuth: Whether to require authorized sender (default: true)handler: Function that returns{ text: string }(can be async)
Example with authorization and arguments:
api.registerCommand({
name: "setmode",
description: "Set plugin mode",
acceptsArgs: true,
requireAuth: true,
handler: async (ctx) => {
const mode = ctx.args?.trim() || "default";
await saveMode(mode);
return { text: `Mode set to: ${mode}` };
},
});
Notes:
- Plugin commands are processed before built-in commands and the AI agent
- Commands are registered globally and work across all channels
- Command names are case-insensitive (
/MyStatusmatches/mystatus) - Command names must start with a letter and contain only letters, numbers, hyphens, and underscores
- Reserved command names (like
help,status,reset, etc.) cannot be overridden by plugins - Duplicate command registration across plugins will fail with a diagnostic error
Register background services
export default function (api) {
api.registerService({
id: "my-service",
start: () => api.logger.info("ready"),
stop: () => api.logger.info("bye"),
});
}
Naming conventions
- Gateway methods:
pluginId.action(example:voicecall.status) - Tools:
snake_case(example:voice_call) - CLI commands: kebab or camel, but avoid clashing with core commands
Skills
Plugins can ship a skill in the repo (skills/<name>/SKILL.md).
Enable it with plugins.entries.<id>.enabled (or other config gates) and ensure
it’s present in your workspace/managed skills locations.
Distribution (npm)
Recommended packaging:
- Main package:
openclaw(this repo) - Plugins: separate npm packages under
@openclaw/*(example:@openclaw/voice-call)
Publishing contract:
- Plugin
package.jsonmust includeopenclaw.extensionswith one or more entry files. - Entry files can be
.jsor.ts(jiti loads TS at runtime). openclaw plugins install <npm-spec>usesnpm pack, extracts into~/.openclaw/extensions/<id>/, and enables it in config.- Config key stability: scoped packages are normalized to the unscoped id for
plugins.entries.*.
Example plugin: Voice Call
This repo includes a voice‑call plugin (Twilio or log fallback):
- Source:
extensions/voice-call - Skill:
skills/voice-call - CLI:
openclaw voicecall start|status - Tool:
voice_call - RPC:
voicecall.start,voicecall.status - Config (twilio):
provider: "twilio"+twilio.accountSid/authToken/from(optionalstatusCallbackUrl,twimlUrl) - Config (dev):
provider: "log"(no network)
See Voice Call and extensions/voice-call/README.md for setup and usage.
Safety notes
Plugins run in-process with the Gateway. Treat them as trusted code:
- Only install plugins you trust.
- Prefer
plugins.allowallowlists. - Restart the Gateway after changes.
Testing plugins
Plugins can (and should) ship tests:
- In-repo plugins can keep Vitest tests under
src/**(example:src/plugins/voice-call.plugin.test.ts). - Separately published plugins should run their own CI (lint/build/test) and validate
openclaw.extensionspoints at the built entrypoint (dist/index.js).