A plugin is just a small code module that extends CoderClaw 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 CoderClaw yet (or you want to keep optional features out of your main install).
Fast path:
coderclaw plugins list
coderclaw plugins install @coderclaw/voice-call
Npm specs are registry-only (package name + optional version/tag). Git/URL/file specs are rejected.
plugins.entries.<id>.config.See Voice Call for a concrete example plugin. Looking for third-party listings? See Community plugins.
@coderclaw/msteams if you use Teams.plugins.slots.memory)plugins.slots.memory = "memory-lancedb")@coderclaw/voice-call@coderclaw/zalouser@coderclaw/matrix@coderclaw/nostr@coderclaw/zalo@coderclaw/msteamsgoogle-antigravity-auth (disabled by default)google-gemini-cli-auth (disabled by default)qwen-portal-auth (disabled by default)github-copilot device login (bundled, disabled by default)CoderClaw 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:
skills directories in the plugin manifest)Plugins run in‑process with the Gateway, so treat them as trusted code. Tool authoring guide: Plugin agent tools.
Plugins can access selected core helpers via api.runtime. For telephony TTS:
const result = await api.runtime.tts.textToSpeechTelephony({
text: "Hello from CoderClaw",
cfg: api.config,
});
Notes:
messages.tts configuration (OpenAI or ElevenLabs).CoderClaw scans, in order:
plugins.load.paths (file or directory)<workspace>/.coderclaw/extensions/*.ts<workspace>/.coderclaw/extensions/*/index.ts~/.coderclaw/extensions/*.ts~/.coderclaw/extensions/*/index.ts<coderclaw>/extensions/*Bundled plugins must be enabled explicitly via plugins.entries.<id>.enabled
or coderclaw plugins enable <id>. Installed plugins are enabled by default,
but can be disabled the same way.
Each plugin must include a coderclaw.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.
A plugin directory may include a package.json with coderclaw.extensions:
{
"name": "my-pack",
"coderclaw": {
"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 note: coderclaw 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 plugins can advertise onboarding metadata via coderclaw.channel and
install hints via coderclaw.install. This keeps the core catalog data-free.
Example:
{
"name": "@coderclaw/nextcloud-talk",
"coderclaw": {
"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": "@coderclaw/nextcloud-talk",
"localPath": "extensions/nextcloud-talk",
"defaultChoice": "npm"
}
}
}
CoderClaw can also merge external channel catalogs (for example, an MPM registry export). Drop a JSON file at one of:
~/.coderclaw/mpm/plugins.json~/.coderclaw/mpm/catalog.json~/.coderclaw/plugins/catalog.jsonOr point CODERCLAW_PLUGIN_CATALOG_PATHS (or CODERCLAW_MPM_CATALOG_PATHS) at
one or more JSON files (comma/semicolon/PATH-delimited). Each file should
contain { "entries": [ { "name": "@scope/pkg", "coderclaw": { "channel": {...}, "install": {...} } } ] }.
Default plugin ids:
package.json name~/.../voice-call.ts → voice-call)If a plugin exports id, CoderClaw uses it but warns when it doesn’t match the
configured id.
{
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/dirsentries.<id>: per‑plugin toggles + configConfig changes require a gateway restart.
Validation rules (strict):
entries, allow, deny, or slots are errors.channels.<id> keys are errors unless a plugin manifest declares
the channel id.coderclaw.plugin.json (configSchema).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
},
},
}
If multiple plugins declare kind: "memory", only the selected one loads. Others
are disabled with diagnostics.
The Control UI uses config.schema (JSON Schema + uiHints) to render better forms.
CoderClaw augments uiHints at runtime based on discovered plugins:
plugins.entries.<id> / .enabled / .configplugins.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" }
}
}
coderclaw plugins list
coderclaw plugins info <id>
coderclaw plugins install <path> # copy a local file/dir into ~/.coderclaw/extensions/<id>
coderclaw plugins install ./extensions/voice-call # relative path ok
coderclaw plugins install ./plugin.tgz # install from a local tarball
coderclaw plugins install ./plugin.zip # install from a local zip
coderclaw plugins install -l ./extensions/voice-call # link (no copy) for dev
coderclaw plugins install @coderclaw/voice-call # install from npm
coderclaw plugins update <id>
coderclaw plugins update --all
coderclaw plugins enable <id>
coderclaw plugins disable <id>
coderclaw plugins doctor
plugins update only works for npm installs tracked under plugins.installs.
Plugins may also register their own top‑level commands (example: coderclaw voicecall).
Plugins export either:
(api) => { ... }{ id, name, configSchema, register(api) { ... } }Plugins can ship hooks and register them at runtime. This lets a plugin bundle event-driven automation without a separate hook pack install.
import { registerPluginHooksFromDir } from "coderclaw/plugin-sdk";
export default function register(api) {
registerPluginHooksFromDir(api, "./hooks");
}
Notes:
HOOK.md + handler.ts).coderclaw hooks list with plugin:<id>.coderclaw hooks; enable/disable the plugin instead.Plugins can register model provider auth flows so users can run OAuth or API-key setup inside CoderClaw (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:
coderclaw 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:
run receives a ProviderAuthContext with prompter, runtime,
openUrl, and oauth.createVpsAwareHandlers helpers.configPatch when you need to add default models or provider config.defaultModel so --set-default can update agent defaults.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:
channels.<id> (not plugins.entries).meta.label is used for labels in CLI/UI lists.meta.aliases adds alternate ids for normalization and CLI inputs.meta.preferOver lists channel ids to skip auto-enable when both are configured.meta.detailLabel and meta.systemImage let UIs show richer channel labels/icons.Use this when you want a new chat surface (a “messaging channel”), not a model provider.
Model provider docs live under /providers/*.
channels.<id>.channels.<id>.accounts.<accountId> for multi‑account setups.meta.label, meta.selectionLabel, meta.docsPath, meta.blurb control CLI/UI lists.meta.docsPath should point at a docs page like /channels/<id>.meta.preferOver lets a plugin replace another channel (auto-enable prefers it).meta.detailLabel and meta.systemImage are used by UIs for detail text/icons.config.listAccountIds + config.resolveAccountcapabilities (chat types, media, threads, etc.)outbound.deliveryMode + outbound.sendText (for basic send)setup (wizard), security (DM policy), status (health/diagnostics)gateway (start/stop/login), mentions, threading, streamingactions (message actions), commands (native command behavior)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.
See the dedicated guide: Plugin agent tools.
export default function (api) {
api.registerGatewayMethod("myplugin.status", ({ respond }) => {
respond(true, { ok: true });
});
}
export default function (api) {
api.registerCli(
({ program }) => {
program.command("mycmd").action(() => {
console.log("Hello");
});
},
{ commands: ["mycmd"] },
);
}
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 (if acceptsArgs: true)commandBody: The full command textconfig: The current CoderClaw configCommand 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:
/MyStatus matches /mystatus)help, status, reset, etc.) cannot be overridden by pluginsexport default function (api) {
api.registerService({
id: "my-service",
start: () => api.logger.info("ready"),
stop: () => api.logger.info("bye"),
});
}
pluginId.action (example: voicecall.status)snake_case (example: voice_call)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.
Recommended packaging:
coderclaw (this repo)@coderclaw/* (example: @coderclaw/voice-call)Publishing contract:
package.json must include coderclaw.extensions with one or more entry files..js or .ts (jiti loads TS at runtime).coderclaw plugins install <npm-spec> uses npm pack, extracts into ~/.coderclaw/extensions/<id>/, and enables it in config.plugins.entries.*.This repo includes a voice‑call plugin (Twilio or log fallback):
extensions/voice-callskills/voice-callcoderclaw voicecall start|statusvoice_callvoicecall.start, voicecall.statusprovider: "twilio" + twilio.accountSid/authToken/from (optional statusCallbackUrl, twimlUrl)provider: "log" (no network)See Voice Call and extensions/voice-call/README.md for setup and usage.
Plugins run in-process with the Gateway. Treat them as trusted code:
plugins.allow allowlists.Plugins can (and should) ship tests:
src/** (example: src/plugins/voice-call.plugin.test.ts).coderclaw.extensions points at the built entrypoint (dist/index.js).