Plugin Setup and Configuration: Package Metadata and Manifests
This page covers plugin packaging with package.json metadata, manifests, setup entries, and config schemas. It is intended for developers creating channel or provider plugins for the OpenClaw system.
Read this when
- You are adding a setup wizard to a plugin
- You need to understand setup-entry.ts vs index.ts
- You are defining plugin config schemas or package.json openclaw metadata
Reference for plugin packaging (package.json metadata), manifests (openclaw.plugin.json), setup entries, and config schemas.
Tip
Need a step-by-step guide? Contextual packaging instructions are in the how-to guides: Channel plugins and Provider plugins.
Package metadata
An openclaw field is required in your package.json to declare what the plugin provides to the plugin system:
Channel plugin
{
"name": "@myorg/openclaw-my-channel",
"version": "1.0.0",
"type": "module",
"openclaw": {
"extensions": ["./index.ts"],
"setupEntry": "./setup-entry.ts",
"channel": {
"id": "my-channel",
"label": "My Channel",
"blurb": "Short description of the channel."
}
}
}
Provider plugin / ClawHub baseline
{
"name": "@myorg/openclaw-my-plugin",
"version": "1.0.0",
"type": "module",
"dependencies": {
"typebox": "1.1.39"
},
"peerDependencies": {
"openclaw": ">=2026.3.24-beta.2"
},
"openclaw": {
"extensions": ["./index.ts"],
"compat": {
"pluginApi": ">=2026.3.24-beta.2",
"minGatewayVersion": "2026.3.24-beta.2"
},
"build": {
"openclawVersion": "2026.3.24-beta.2",
"pluginSdkVersion": "2026.3.24-beta.2"
}
}
}
Note
External publishing on ClawHub requires
compatandbuild. Canonical publish snippets are located indocs/snippets/plugin-publish/.
openclaw fields
-
extensions(string[]), Entry point files, relative to the package root. Valid source entries for workspace and git checkout development. -
runtimeExtensions(string[]), Built JavaScript counterparts forextensions, used when OpenClaw loads an installed npm package. The source/built resolution order is described in SDK entry points. -
setupEntry(string), Lightweight setup-only entry, optional. -
runtimeSetupEntry(string), Built JavaScript counterpart forsetupEntry.setupEntrymust also be set. -
plugin(object), Fallback plugin identity for{ id, label }, used when a plugin lacks channel or provider metadata to derive an id or label. -
channel(object), Channel catalog metadata for setup, picker, quickstart, and status surfaces. -
install(object), Installation hints:npmSpec,localPath,defaultChoice,minHostVersion,expectedIntegrity,allowInvalidConfigRecovery,requiredPlatformPackages. -
startup(object), Flags controlling startup behavior. -
compat(object), ThepluginApiversion range this plugin supports. Required for external ClawHub publishing.
Note
Provider ids (
providers: string[]) belong to manifest metadata, not package metadata. Declare them inopenclaw.plugin.json, not here, see Plugin manifest.
openclaw.channel
openclaw.channel provides lightweight package metadata for channel discovery and setup surfaces before runtime loads.
Channel-owned setup fields
Channel plugins should define setup fields once in runtime code using defineChannelSetupContract(...) and publish the matching serializable projection under openclaw.channel.setup.fields. The runtime definition infers the plugin-local input type, parses both guided and non-interactive values, and keeps channel-specific keys out of core types. Package metadata lets openclaw channels add <channel-id> --help and openclaw channels add --channel <channel-id> --help discover only the selected channel's options without loading the plugin.
import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
export const setupContract = defineChannelSetupContract({
fields: {
endpoint: {
kind: "string",
cli: { flags: "--endpoint <url>", description: "Service endpoint" },
},
transport: {
kind: "choice",
choices: ["native", "container"],
cli: { flags: "--transport <kind>", description: "Transport owner" },
},
},
adapter: {
applyAccountConfig: ({ cfg, input }) => ({
...cfg,
channels: { ...cfg.channels, example: input },
}),
},
});
{
"openclaw": {
"channel": {
"id": "example",
"setup": {
"fields": [
{
"key": "endpoint",
"kind": "string",
"cli": { "flags": "--endpoint <url>", "description": "Service endpoint" }
},
{
"key": "transport",
"kind": "choice",
"choices": ["native", "container"],
"cli": { "flags": "--transport <kind>", "description": "Transport owner" }
}
]
}
}
}
}
Supported field kinds are string, boolean, integer, string-list, and choice. Use sensitive: true for credentials. Each field key must match the camelCased attribute name of its long CLI flag, including any negated form, such as apiToken for --api-token. Boolean fields may add cli.negatedFlags when both positive and --no-* forms are needed. channel, account, and the account display name remain the shared control envelope.
The setup/ChannelSetupInput adapter remains accessible for legacy external plugins. For new plugins, setupContract should be exposed; when both are present, OpenClaw will always select it first.
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for the channel. |
label | string | Main name shown for the channel. |
selectionLabel | string | Label used in picker and setup interfaces when it needs to differ from label. |
detailLabel | string | Secondary label for richer channel catalogs and status displays. |
docsPath | string | Documentation path for setup and selection links. |
docsLabel | string | Overrides the label in documentation links when it should not match the channel id. |
blurb | string | Short description for onboarding and catalog views. |
order | number | Position in channel catalog sorting. |
aliases | string[] | Additional aliases used during channel selection lookups. |
preferOver | string[] | Lower-priority plugin or channel ids that this channel should take precedence over. |
systemImage | string | Optional icon or system image name for channel UI catalogs. |
selectionDocsPrefix | string | Text prepended to documentation links in selection interfaces. |
selectionDocsOmitLabel | boolean | Displays the documentation path directly rather than a labeled link in selection text. |
selectionExtras | string[] | Extra short strings appended to selection text. |
markdownCapable | boolean | Indicates the channel supports markdown for outbound formatting decisions. |
exposure | object | Controls channel visibility in setup, configured lists, and documentation surfaces. |
quickstartAllowFrom | boolean | Enables the standard quickstart allowFrom setup flow for this channel. |
forceAccountBinding | boolean | Forces explicit account binding even if only one account is available. |
preferSessionLookupForAnnounceTarget | boolean | Uses session lookup when resolving announce targets for this channel. |
setup | object | Serializable channel-specific setup fields for lazy CLI option discovery. |
Example:
{
"openclaw": {
"channel": {
"id": "my-channel",
"label": "My Channel",
"selectionLabel": "My Channel (self-hosted)",
"detailLabel": "My Channel Bot",
"docsPath": "/channels/my-channel",
"docsLabel": "my-channel",
"blurb": "Webhook-based self-hosted chat integration.",
"order": 80,
"aliases": ["mc"],
"preferOver": ["my-channel-legacy"],
"selectionDocsPrefix": "Guide:",
"selectionExtras": ["Markdown"],
"markdownCapable": true,
"exposure": {
"configured": true,
"setup": true,
"docs": true
},
"quickstartAllowFrom": true
}
}
}
exposure supports:
configured: shows the channel in configured and status listing surfacessetup: shows the channel in interactive setup and configuration pickersdocs: marks the channel as public in documentation and navigation surfaces
openclaw.install
openclaw.install belongs to package metadata, not manifest metadata.
| Field | Type | What it means |
|---|---|---|
clawhubSpec | string | The official ClawHub spec for install, update, and onboarding install-on-demand workflows. |
npmSpec | string | The official npm spec used as a fallback during install and update operations. |
localPath | string | A path for local development or for installing a bundled plugin. |
defaultChoice | "clawhub" | "npm" | "local" | The preferred installation source when more than one is available. |
minHostVersion | string | The lowest OpenClaw version that is supported, either >=x.y.z or >=x.y.z-prerelease. |
expectedIntegrity | string | The expected npm distribution integrity string, typically sha512-..., used for pinned installs. |
allowInvalidConfigRecovery | boolean | Enables bundled plugin reinstall flows to recover from certain stale configuration failures. |
requiredPlatformPackages | string[] | Platform-specific npm aliases that are required and verified during npm install. |
Onboarding behavior
Install-on-demand surfaces in interactive onboarding use openclaw.install. If your plugin shows provider authentication choices or channel setup or catalog metadata before runtime loads, onboarding can ask for a ClawHub, npm, or local install, then install or enable the plugin, and proceed with the selected flow. ClawHub choices rely on clawhubSpec and are preferred when available; npm choices need trusted catalog metadata that includes a registry npmSpec. Exact versions and expectedIntegrity are optional pins that are enforced during install or update when they are set. Keep "what to show" in openclaw.plugin.json and "how to install it" in package.json.
minHostVersion enforcement
When minHostVersion is set, both install and non-bundled manifest registry loading enforce it. Older hosts skip external plugins, and invalid version strings are rejected. Bundled source plugins are assumed to share the same version as the host checkout.
Pinned npm installs
For pinned npm installs, place the exact version in npmSpec and include the expected artifact integrity:
{
"openclaw": {
"install": {
"npmSpec": "@wecom/wecom-openclaw-plugin@1.2.3",
"expectedIntegrity": "sha512-REPLACE_WITH_NPM_DIST_INTEGRITY",
"defaultChoice": "npm"
}
}
}
allowInvalidConfigRecovery scope
allowInvalidConfigRecovery is not a general workaround for broken configurations. It is a narrow recovery mechanism for bundled plugins only, allowing reinstall or setup to fix known upgrade leftovers, such as a missing bundled plugin path or a stale channels.<id> entry for the same plugin. If the configuration is broken for unrelated reasons, the install still fails closed and instructs the operator to run openclaw doctor --fix.
Deferred full load
Channel plugins can choose deferred loading with:
{
"openclaw": {
"extensions": ["./index.ts"],
"setupEntry": "./setup-entry.ts",
"startup": {
"deferConfiguredChannelFullLoadUntilAfterListen": true
}
}
}
When this is enabled, OpenClaw loads only setupEntry during the pre-listen startup phase, even for channels that are already configured. The full entry loads after the gateway starts listening.
Warning
Enable deferred loading only when your
setupEntryregisters everything the gateway needs before it starts listening, including channel registration, HTTP routes, and gateway methods. If the full entry owns required startup capabilities, keep the default behavior.
If your setup or full entry registers gateway RPC methods, keep them on a plugin-specific prefix. Reserved core admin namespaces (config.*, exec.approvals.*, wizard.*, update.*) remain owned by the core and always normalize to operator.admin.
Plugin manifest
Every native plugin must include an openclaw.plugin.json in the package root. OpenClaw uses it to validate configuration without executing plugin code.
{
"id": "my-plugin",
"name": "My Plugin",
"description": "Adds My Plugin capabilities to OpenClaw",
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"webhookSecret": {
"type": "string",
"description": "Webhook verification secret"
}
}
}
}
For channel plugins, add channels (and provider plugins add providers):
{
"id": "my-channel",
"channels": ["my-channel"],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
Even plugins with no configuration must ship a schema. An empty schema is valid:
{
"id": "my-plugin",
"configSchema": {
"type": "object",
"additionalProperties": false
}
}
See Plugin manifest for the complete schema reference.
ClawHub publishing
Skills and plugin packages use separate ClawHub publish commands. For plugin packages, use the package-specific command:
clawhub package publish your-org/your-plugin --dry-run
clawhub package publish your-org/your-plugin
Note
clawhub skill publish <path>is a different command used for publishing a skill folder, not a plugin package. See Publishing on ClawHub.
Setup entry
setup-entry.ts serves as a lighter-weight substitute for index.ts, loaded by OpenClaw when only setup surfaces are required (such as onboarding, config repair, or inspecting a disabled channel):
// setup-entry.ts
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { myChannelPlugin } from "./src/channel.js";
export default defineSetupPluginEntry(myChannelPlugin);
This prevents heavy runtime components (crypto libraries, CLI registrations, background services) from being loaded during setup operations.
Workspace channels that bundle setup-safe exports in sidecar modules can import defineBundledChannelSetupEntry(...) from openclaw/plugin-sdk/channel-entry-contract instead of using defineSetupPluginEntry(...). That bundled contract also provides an optional runtime export, keeping runtime wiring lightweight and explicit during setup.
When OpenClaw uses setupEntry instead of the full entry
- The channel is disabled yet still requires setup or onboarding interfaces.
- The channel is enabled but remains unconfigured.
- Deferred loading is active (
deferConfiguredChannelFullLoadUntilAfterListen).
What setupEntry must register
- The channel plugin object (accessed through
defineSetupPluginEntry). - Any HTTP routes needed before the gateway starts listening.
- Any gateway methods required during startup.
These startup gateway methods must still avoid reserved core admin namespaces like config.* or update.*.
What setupEntry should NOT include
- CLI registrations.
- Background services.
- Heavy runtime imports (crypto, SDKs).
- Gateway methods only necessary after startup.
Narrow setup helper imports
For hot setup-only paths, choose the narrow setup helper seams over the broader plugin-sdk/setup umbrella when only part of the setup surface is needed:
| Import path | Use it for | Key exports |
|---|---|---|
plugin-sdk/setup-runtime | setup-time runtime helpers that remain available in setupEntry or during deferred channel startup | createSetupTranslator, createPatchedAccountSetupAdapter, createEnvPatchedAccountSetupAdapter, createSetupInputPresenceValidator, noteChannelLookupFailure, noteChannelLookupSummary, promptResolvedAllowFrom, splitSetupEntries, createAllowlistSetupWizardProxy, createDelegatedSetupWizardProxy |
plugin-sdk/setup-tools | setup, install, CLI, archive, or docs helpers | formatCliCommand, detectBinary, extractArchive, resolveBrewExecutable, formatDocsLink, CONFIG_DIR |
Reach for the broader plugin-sdk/setup seam when the full shared setup toolbox is needed, including config-patch helpers such as moveSingleAccountChannelSectionToDefaultAccount(...).
Use createSetupTranslator(...) for fixed setup wizard copy. It picks the first nonblank value from OPENCLAW_LOCALE, LC_ALL, LC_MESSAGES, and LANG, in that sequence, falling back to English. Set OPENCLAW_LOCALE=en for an explicit English override. Keep plugin-specific setup text in plugin-owned code and use shared catalog keys only for common setup labels, status text, and official bundled plugin setup copy.
Setup patch adapters remain hot-path safe on import. Their bundled single-account promotion contract-surface lookup is lazy, so importing plugin-sdk/setup-runtime does not eagerly load bundled contract-surface discovery before the adapter is actually used.
Channel-owned setup input fields
ChannelSetupInput is a generic envelope shared by setup callers and channel
plugins. Its permanently typed fields are name, token, tokenFile,
useEnv, allowFrom, and defaultTo. Additional plugin-owned keys can still
appear on the runtime input object, but the shared type does not declare an
index signature. Each plugin must declare and narrow its own setup fields or
validate them with a plugin-owned schema at the adapter boundary:
import type { ChannelSetupAdapter, ChannelSetupInput } from "openclaw/plugin-sdk/channel-setup";
type AcmeSetupInput = ChannelSetupInput & {
workspaceId?: string;
webhookUrl?: string;
};
export const acmeSetupAdapter: ChannelSetupAdapter = {
applyAccountConfig: ({ cfg, input }) => {
const setupInput = input as AcmeSetupInput;
return {
...cfg,
channels: {
...cfg.channels,
acme: {
token: setupInput.token,
workspaceId: setupInput.workspaceId,
webhookUrl: setupInput.webhookUrl,
},
},
};
},
};
Channel-specific fields previously declared directly on
ChannelSetupInput remain temporarily typed for external source compatibility.
They are deprecated. A 2026-07-22 registry sweep of 426 published out-of-tree
channel plugins removed 21 fields with no readers and retained 22 with known
readers. Each retained field is deleted as soon as no published plugin reads it;
no version boundary is required. New and bundled plugins must not rely on this
tier; declare the fields they own locally.
Channel-owned single-account promotion
When a channel upgrades from a single-account top-level config to channels.<id>.accounts.*, the default shared behavior moves promoted account-scoped values into accounts.default.
Every channel plugin can extend or narrow that promotion through its setup adapter:
singleAccountKeysToMove: extra top-level keys that should move into the promoted accountnamedAccountPromotionKeys: when named accounts already exist, only these keys move into the promoted account; shared policy or delivery keys stay at the channel rootresolveSingleAccountPromotionTarget(...): choose which existing account receives promoted values
The promotion contract is considered complete once singleAccountKeysToMove is present. Even if the field is an empty array, declare it to opt out of legacy key promotion. Adapters that do not include this field keep a reader-backed pre-declaration promotion tier for plugins that have already been published. During the registry sweep on 2026-07-22, 23 keys with no published dependents were removed, while six common keys and the setup-only rooms key were retained. Each retained key is removed as soon as its published readers migrate to declarations, with no version boundary required.
When the doctor needs to load declarations from the lightweight bundled setup artifact, declare openclaw.setupFeatures.configPromotion: true in the plugin package manifest. The setup-only plugin surface and the full channel plugin must expose identical declarations.
When calling moveSingleAccountChannelSectionToDefaultAccount(...) with a plugin that has already been resolved, provide its setup adapter as setupSurface. Setup surfaces supplied by the caller take priority over loaded and bundled lookup, keeping scoped or setup-only plugins independent of global registration.
Note
The current bundled example is Matrix. If exactly one named Matrix account already exists, or if
defaultAccountpoints at an existing non-canonical key such asOps, promotion keeps that account rather than creating a newaccounts.defaultentry.
Config schema
Plugin config is validated against the JSON Schema found in your manifest. Users configure plugins through:
{
plugins: {
entries: {
"my-plugin": {
config: {
webhookSecret: "abc123",
},
},
},
},
}
During registration, your plugin receives this config as api.pluginConfig.
For config that applies only to a specific channel, use the channel config section instead:
{
channels: {
"my-channel": {
token: "bot-token",
allowFrom: ["user1", "user2"],
},
},
}
Building channel config schemas
To convert a Zod schema into the ChannelConfigSchema wrapper used by plugin-owned config artifacts, use buildChannelConfigSchema:
import { z } from "zod";
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
const accountSchema = z.object({
token: z.string().optional(),
allowFrom: z.array(z.string()).optional(),
accounts: z.object({}).catchall(z.any()).optional(),
defaultAccount: z.string().optional(),
});
const configSchema = buildChannelConfigSchema(accountSchema);
If you already write the contract as JSON Schema or TypeBox, use the direct helper so OpenClaw can skip Zod-to-JSON-Schema conversion on metadata paths:
import { Type } from "typebox";
import { buildJsonChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
const configSchema = buildJsonChannelConfigSchema(
Type.Object({
token: Type.Optional(Type.String()),
allowFrom: Type.Optional(Type.Array(Type.String())),
}),
);
For third-party plugins, the cold-path contract remains the plugin manifest: mirror the generated JSON Schema into openclaw.plugin.json#channelConfigs so that config schema, setup, and UI surfaces can inspect channels.<id> without loading runtime code.
Setup wizards
Channel plugins can offer interactive setup wizards for openclaw onboard. The wizard is a ChannelSetupWizard object on the ChannelPlugin:
import type { ChannelSetupWizard } from "openclaw/plugin-sdk/channel-setup";
const setupWizard: ChannelSetupWizard = {
channel: "my-channel",
status: {
configuredLabel: "Connected",
unconfiguredLabel: "Not configured",
resolveConfigured: ({ cfg }) => Boolean((cfg.channels as any)?.["my-channel"]?.token),
},
credentials: [
{
inputKey: "token",
providerHint: "my-channel",
credentialLabel: "Bot token",
preferredEnvVar: "MY_CHANNEL_BOT_TOKEN",
envPrompt: "Use MY_CHANNEL_BOT_TOKEN from environment?",
keepPrompt: "Keep current token?",
inputPrompt: "Enter your bot token:",
inspect: ({ cfg, accountId }) => {
const token = (cfg.channels as any)?.["my-channel"]?.token;
return {
accountConfigured: Boolean(token),
hasConfiguredValue: Boolean(token),
};
},
},
],
};
ChannelSetupWizard also supports textInputs, dmPolicy, allowFrom, groupAccess, prepare, finalize, and more. For a complete bundled example, see the Discord plugin's src/setup-core.ts.
Shared allowFrom prompts
For DM allowlist prompts that only need the standard note -> prompt -> parse -> merge -> patch flow, prefer the shared setup helpers from openclaw/plugin-sdk/setup: createPromptParsedAllowFromForAccount(...) and createTopLevelChannelParsedAllowFromPrompt(...).
Standard channel setup status
For channel setup status blocks that differ only by labels, scores, and optional extra lines, prefer createStandardChannelSetupStatus(...) from openclaw/plugin-sdk/setup instead of manually creating the same status object in each plugin.
Optional channel setup surface
For optional setup surfaces that should appear only in certain contexts, use createOptionalChannelSetupSurface from openclaw/plugin-sdk/channel-setup:
import { createOptionalChannelSetupSurface } from "openclaw/plugin-sdk/channel-setup";
const setupSurface = createOptionalChannelSetupSurface({
channel: "my-channel",
label: "My Channel",
npmSpec: "@myorg/openclaw-my-channel",
docsPath: "/channels/my-channel",
});
// Returns { setupAdapter, setupWizard }
When you need only one half of that optional-install surface, plugin-sdk/channel-setup also exposes the lower-level createOptionalChannelSetupAdapter(...) and createOptionalChannelSetupWizard(...) builders.
The generated optional adapter or wizard fails closed on real config writes. It reuses one install-required message across validateInput, applyAccountConfig, and finalize, and appends a docs link when docsPath is set.
Binary-backed setup helpers
For binary-backed setup UIs, prefer the shared delegated helpers rather than copying the same binary or status glue into every channel:
createDetectedBinaryStatus(...)for status blocks that vary only by labels, hints, scores, and binary detectioncreateCliPathTextInput(...)for path-backed text inputscreateDelegatedSetupWizardProxy(...)whensetupEntryneeds to lazily forward status, prepare, or finalize behavior to a heavier full wizardcreateDelegatedTextInputShouldPrompt(...)whensetupEntryonly needs to delegate atextInputs[*].shouldPromptdecision
Publishing and installing
External plugins: upload to ClawHub, then install:
npm
openclaw plugins install @myorg/openclaw-my-plugin
During the launch cutover, bare package specs are installed from npm unless their identifier matches a bundled or official plugin id. In that case, OpenClaw uses the local or official copy instead. For deterministic source selection, apply clawhub:, npm:, git:, or npm-pack:, refer to Manage plugins.
ClawHub only
openclaw plugins install clawhub:@myorg/openclaw-my-plugin
npm package spec
Use npm when a package has not yet been migrated to ClawHub, or when you require a direct npm install path during migration:
openclaw plugins install npm:@myorg/openclaw-my-plugin
In-repo plugins: put them inside the bundled plugin workspace tree; the build process discovers them automatically.
Info
For installs originating from npm,
openclaw plugins installplaces the package into a per-plugin project under~/.openclaw/npm/projectswith lifecycle scripts turned off (--ignore-scripts). Keep plugin dependency trees limited to pure JS/TS and avoid packages that needpostinstallbuilds.
Note
Plugin dependencies are not installed by the gateway at startup. npm, git, and ClawHub install flows each handle their own dependency convergence; local plugins must already have their dependencies installed.
Bundled package metadata is declared explicitly rather than inferred from built JavaScript at gateway startup. Runtime dependencies belong in the plugin package that owns them; the packaged OpenClaw startup never repairs or mirrors plugin dependencies.
Related
- Building plugins, a step-by-step getting started guide
- Plugin manifest, complete manifest schema reference
- SDK entry points,
definePluginEntryanddefineChannelPluginEntry