Building Plugins
Building Plugins
Plugins extend RemoteClaw with new capabilities: channels, model providers, speech, realtime transcription, realtime voice, media understanding, image generation, video generation, web fetch, web search, agent tools, or any combination.
You do not need to add your plugin to the RemoteClaw repository. Publish to
ClawHub or npm and users install with
remoteclaw plugins install <package-name>. RemoteClaw tries ClawHub first and
falls back to npm automatically.
Prerequisites
- Node >= 22 and a package manager (npm or pnpm)
- Familiarity with TypeScript (ESM)
- For in-repo plugins: repository cloned and
pnpm installdone
What kind of plugin?
If a channel plugin is optional and may not be installed when onboarding/setup
runs, use createOptionalChannelSetupSurface(...) from
remoteclaw/plugin-sdk/channel-setup. It produces a setup adapter + wizard pair
that advertises the install requirement and fails closed on real config writes
until the plugin is installed.
Quick start: tool plugin
This walkthrough creates a minimal plugin that registers an agent tool. Channel and provider plugins have dedicated guides linked above.
```json remoteclaw.plugin.json{ "id": "my-plugin", "name": "My Plugin", "description": "Adds a custom tool to RemoteClaw", "configSchema": { "type": "object", "additionalProperties": false }}```</CodeGroup>
Every plugin needs a manifest, even with no config. See[Manifest](/plugins/manifest) for the full schema. The canonical ClawHubpublish snippets live in `docs/snippets/plugin-publish/`.```typescriptimport { definePluginEntry } from "remoteclaw/plugin-sdk/plugin-entry";import { Type } from "@sinclair/typebox";
export default definePluginEntry({ id: "my-plugin", name: "My Plugin", description: "Adds a custom tool to RemoteClaw", register(api) { api.registerTool({ name: "my_tool", description: "Do a thing", parameters: Type.Object({ input: Type.String() }), async execute(_id, params) { return { content: [{ type: "text", text: `Got: ${params.input}` }] }; }, }); },});```
`definePluginEntry` is for non-channel plugins. For channels, use`defineChannelPluginEntry` — see [Channel Plugins](/plugins/sdk-channel-plugins).For full entry point options, see [Entry Points](/plugins/sdk-entrypoints).**External plugins:** validate and publish with ClawHub, then install:
```bashclawhub package publish your-org/your-plugin --dry-runclawhub package publish your-org/your-pluginremoteclaw plugins install clawhub:@myorg/remoteclaw-my-plugin```
RemoteClaw also checks ClawHub before npm for bare package specs like`@myorg/remoteclaw-my-plugin`.
**In-repo plugins:** place under the bundled plugin workspace tree — automatically discovered.
```bashpnpm test -- <bundled-plugin-root>/my-plugin/```Plugin capabilities
A single plugin can register any number of capabilities via the api object:
| Capability | Registration method | Detailed guide |
|---|---|---|
| Text inference (LLM) | api.registerProvider(...) | Provider Plugins |
| CLI inference backend | api.registerCliBackend(...) | CLI Backends |
| Channel / messaging | api.registerChannel(...) | Channel Plugins |
| Speech (TTS/STT) | api.registerSpeechProvider(...) | Provider Plugins |
| Realtime transcription | api.registerRealtimeTranscriptionProvider(...) | Provider Plugins |
| Realtime voice | api.registerRealtimeVoiceProvider(...) | Provider Plugins |
| Media understanding | api.registerMediaUnderstandingProvider(...) | Provider Plugins |
| Image generation | api.registerImageGenerationProvider(...) | Provider Plugins |
| Music generation | api.registerMusicGenerationProvider(...) | Provider Plugins |
| Video generation | api.registerVideoGenerationProvider(...) | Provider Plugins |
| Web fetch | api.registerWebFetchProvider(...) | Provider Plugins |
| Web search | api.registerWebSearchProvider(...) | Provider Plugins |
| Agent tools | api.registerTool(...) | Below |
| Custom commands | api.registerCommand(...) | Entry Points |
| Event hooks | api.registerHook(...) | Entry Points |
| HTTP routes | api.registerHttpRoute(...) | Internals |
| CLI subcommands | api.registerCli(...) | Entry Points |
For the full registration API, see SDK Overview.
If your plugin registers custom gateway RPC methods, keep them on a
plugin-specific prefix. Core admin namespaces (config.*,
exec.approvals.*, wizard.*, update.*) stay reserved and always resolve to
operator.admin, even if a plugin asks for a narrower scope.
Hook guard semantics to keep in mind:
before_tool_call:{ block: true }is terminal and stops lower-priority handlers.before_tool_call:{ block: false }is treated as no decision.before_tool_call:{ requireApproval: true }pauses agent execution and prompts the user for approval via the exec approval overlay, Telegram buttons, Discord interactions, or the/approvecommand on any channel.before_install:{ block: true }is terminal and stops lower-priority handlers.before_install:{ block: false }is treated as no decision.message_sending:{ cancel: true }is terminal and stops lower-priority handlers.message_sending:{ cancel: false }is treated as no decision.
The /approve command handles both exec and plugin approvals with bounded fallback: when an exec approval id is not found, RemoteClaw retries the same id through plugin approvals. Plugin approval forwarding can be configured independently via approvals.plugin in config.
If custom approval plumbing needs to detect that same bounded fallback case,
prefer isApprovalNotFoundError from remoteclaw/plugin-sdk/error-runtime
instead of matching approval-expiry strings manually.
See SDK Overview hook decision semantics for details.
Registering agent tools
Tools are typed functions the LLM can call. They can be required (always available) or optional (user opt-in):
register(api) { // Required tool — always available api.registerTool({ name: "my_tool", description: "Do a thing", parameters: Type.Object({ input: Type.String() }), async execute(_id, params) { return { content: [{ type: "text", text: params.input }] }; }, });
// Optional tool — user must add to allowlist api.registerTool( { name: "workflow_tool", description: "Run a workflow", parameters: Type.Object({ pipeline: Type.String() }), async execute(_id, params) { return { content: [{ type: "text", text: params.pipeline }] }; }, }, { optional: true }, );}Users enable optional tools in config:
{ tools: { allow: ["workflow_tool"] },}- Tool names must not clash with core tools (conflicts are skipped)
- Use
optional: truefor tools with side effects or extra binary requirements - Users can enable all tools from a plugin by adding the plugin id to
tools.allow
Import conventions
Always import from focused remoteclaw/plugin-sdk/<subpath> paths:
import { definePluginEntry } from "remoteclaw/plugin-sdk/plugin-entry";import { createPluginRuntimeStore } from "remoteclaw/plugin-sdk/runtime-store";
// Wrong: monolithic root (deprecated, will be removed)import { ... } from "remoteclaw/plugin-sdk";For the full subpath reference, see SDK Overview.
Within your plugin, use local barrel files (api.ts, runtime-api.ts) for
internal imports — never import your own plugin through its SDK path.
For provider plugins, keep provider-specific helpers in those package-root barrels unless the seam is truly generic. Current bundled examples:
- Anthropic: Claude stream wrappers and
service_tier/ beta helpers - OpenAI: provider builders, default-model helpers, realtime providers
- OpenRouter: provider builder plus onboarding/config helpers
If a helper is only useful inside one bundled provider package, keep it on that
package-root seam instead of promoting it into remoteclaw/plugin-sdk/*.
Some generated remoteclaw/plugin-sdk/<bundled-id> helper seams still exist for
bundled-plugin maintenance and compatibility, for example
plugin-sdk/feishu-setup or plugin-sdk/zalo-setup. Treat those as reserved
surfaces, not as the default pattern for new third-party plugins.
Pre-submission checklist
remoteclaw metadatadefineChannelPluginEntry or definePluginEntryplugin-sdk/<subpath> pathspnpm test -- <bundled-plugin-root>/my-plugin/)pnpm check passes (in-repo plugins)
Beta Release Testing
- Watch for GitHub release tags on remoteclaw/remoteclaw and subscribe via
Watch>Releases. Beta tags look likev2026.3.N-beta.1. You can also turn on notifications for the official RemoteClaw X account @remoteclaw for release announcements. - Test your plugin against the beta tag as soon as it appears. The window before stable is typically only a few hours.
- Post in your plugin’s thread in the
plugin-forumDiscord channel after testing with eitherall goodor what broke. If you do not have a thread yet, create one. - If something breaks, open or update an issue titled
Beta blocker: <plugin-name> - <summary>and apply thebeta-blockerlabel. Put the issue link in your thread. - Open a PR to
maintitledfix(<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. Maintainers watch these threads during beta testing. - Silence means green. If you miss the window, your fix likely lands in the next cycle.
Next steps
Related
- Plugin Architecture — internal architecture deep dive
- SDK Overview — Plugin SDK reference
- Manifest — plugin manifest format
- Channel Plugins — building channel plugins
- Provider Plugins — building provider plugins