Plugins extend Genesis 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 Genesis repository. Publish to ClawHub or npm and users install with genesis plugins install <package-name>. Genesis 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 install done

What kind of plugin?

For a channel plugin that isn't guaranteed to be installed when onboarding/setup runs, use createOptionalChannelSetupSurface(...) from genesis/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.

Create the package and manifest

```json package.json
{
  "name": "@myorg/genesis-my-plugin",
  "version": "1.0.0",
  "type": "module",
  "genesis": {
    "extensions": ["./index.ts"],
    "compat": {
      "pluginApi": ">=2026.3.24-beta.2",
      "minGatewayVersion": "2026.3.24-beta.2"
    },
    "build": {
      "genesisVersion": "2026.3.24-beta.2",
      "pluginSdkVersion": "2026.3.24-beta.2"
    }
  }
}
```

```json genesis.plugin.json
{
  "id": "my-plugin",
  "name": "My Plugin",
  "description": "Adds a custom tool to Genesis",
  "configSchema": {
    "type": "object",
    "additionalProperties": false
  }
}
```



Every plugin needs a manifest, even with no config. See
[Manifest](/plugins/manifest) for the full schema. The canonical ClawHub
publish snippets live in `docs/snippets/plugin-publish/`.

Write the entry point

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

export default definePluginEntry({
  id: "my-plugin",
  name: "My Plugin",
  description: "Adds a custom tool to Genesis",
  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).

Test and publish

**External plugins:** validate and publish with ClawHub, then install:

```bash
clawhub package publish your-org/your-plugin --dry-run
clawhub package publish your-org/your-plugin
genesis plugins install clawhub:@myorg/genesis-my-plugin
```

Genesis also checks ClawHub before npm for bare package specs like
`@myorg/genesis-my-plugin`.

**In-repo plugins:** place under the bundled plugin workspace tree — automatically discovered.

```bash
pnpm 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
Tool-result middleware api.registerAgentToolResultMiddleware(...) SDK Overview
Agent tools api.registerTool(...) Below
Custom commands api.registerCommand(...) Entry Points
Plugin hooks api.on(...) Plugin hooks
Internal 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.

Bundled plugins can use api.registerAgentToolResultMiddleware(...) when they need async tool-result rewriting before the model sees the output. Declare the targeted runtimes in contracts.agentToolResultMiddleware, for example ["pi", "codex"]. This is a trusted bundled-plugin seam; external plugins should prefer regular Genesis plugin hooks unless Genesis grows an explicit trust policy for this capability.

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 /approve command 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.
  • message_received: prefer the typed threadId field when you need inbound thread/topic routing. Keep metadata for channel-specific extras.
  • message_sending: prefer typed replyToId / threadId routing fields over channel-specific metadata keys.

The /approve command handles both exec and plugin approvals with bounded fallback: when an exec approval id is not found, Genesis 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 genesis/plugin-sdk/error-runtime instead of matching approval-expiry strings manually.

See Plugin hooks for examples and the hook reference.

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: true for 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 genesis/plugin-sdk/<subpath> paths:

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

// Wrong: monolithic root (deprecated, will be removed)
import { ... } from "genesis/plugin-sdk";

Genesis also resolves OpenClaw-compatible openclaw/plugin-sdk/<subpath> and @openclaw/plugin-sdk/<subpath> imports for existing npm plugins.

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 genesis/plugin-sdk/*.

Some generated genesis/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

**package.json** has correct `genesis` metadata
**genesis.plugin.json** manifest is present and valid
Entry point uses `defineChannelPluginEntry` or `definePluginEntry`
All imports use focused `plugin-sdk/` paths
Internal imports use local modules, not SDK self-imports
Tests pass (`pnpm test -- /my-plugin/`)
`pnpm check` passes (in-repo plugins)

Beta Release Testing

  1. Watch for GitHub release tags on PIXELZX0/Genesis and subscribe via Watch > Releases. Beta tags look like v2026.3.N-beta.1. You can also turn on notifications for the official Genesis X account @genesis 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 after testing with either all good or what broke. If you do not have a thread yet, create one.
  4. If something breaks, open or update an issue titled Beta blocker: <plugin-name> - <summary> and apply the beta-blocker label. Put the issue link 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. Maintainers watch these threads during beta testing.
  6. Silence means green. If you miss the window, your fix likely lands in the next cycle.

Next steps

Related