--- title: "Plugin Setup and Config" sidebarTitle: "Setup and Config" summary: "Setup wizards, setup-entry.ts, config schemas, and package.json metadata" read_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 --- # Plugin Setup and Config Reference for plugin packaging (`package.json` metadata), manifests (`openclaw.plugin.json`), setup entries, and config schemas. **Looking for a walkthrough?** The how-to guides cover packaging in context: [Channel Plugins](/plugins/sdk-channel-plugins#step-1-package-and-manifest) and [Provider Plugins](/plugins/sdk-provider-plugins#step-1-package-and-manifest). ## Package metadata Your `package.json` needs an `openclaw` field that tells the plugin system what your plugin provides: **Channel plugin:** ```json { "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 publish baseline:** ```json openclaw-clawhub-package.json { "name": "@myorg/openclaw-my-plugin", "version": "1.0.0", "type": "module", "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" } } } ``` If you publish the plugin externally on ClawHub, those `compat` and `build` fields are required. The canonical publish snippets live in `docs/snippets/plugin-publish/`. ### `openclaw` fields | Field | Type | Description | | ------------ | ---------- | ------------------------------------------------------------------------------------------------------ | | `extensions` | `string[]` | Entry point files (relative to package root) | | `setupEntry` | `string` | Lightweight setup-only entry (optional) | | `channel` | `object` | Channel catalog metadata for setup, picker, quickstart, and status surfaces | | `providers` | `string[]` | Provider ids registered by this plugin | | `install` | `object` | Install hints: `npmSpec`, `localPath`, `defaultChoice`, `minHostVersion`, `allowInvalidConfigRecovery` | | `startup` | `object` | Startup behavior flags | ### `openclaw.channel` `openclaw.channel` is cheap package metadata for channel discovery and setup surfaces before runtime loads. | Field | Type | What it means | | -------------------------------------- | ---------- | ----------------------------------------------------------------------------- | | `id` | `string` | Canonical channel id. | | `label` | `string` | Primary channel label. | | `selectionLabel` | `string` | Picker/setup label when it should differ from `label`. | | `detailLabel` | `string` | Secondary detail label for richer channel catalogs and status surfaces. | | `docsPath` | `string` | Docs path for setup and selection links. | | `docsLabel` | `string` | Override label used for docs links when it should differ from the channel id. | | `blurb` | `string` | Short onboarding/catalog description. | | `order` | `number` | Sort order in channel catalogs. | | `aliases` | `string[]` | Extra lookup aliases for channel selection. | | `preferOver` | `string[]` | Lower-priority plugin/channel ids this channel should outrank. | | `systemImage` | `string` | Optional icon/system-image name for channel UI catalogs. | | `selectionDocsPrefix` | `string` | Prefix text before docs links in selection surfaces. | | `selectionDocsOmitLabel` | `boolean` | Show the docs path directly instead of a labeled docs link in selection copy. | | `selectionExtras` | `string[]` | Extra short strings appended in selection copy. | | `markdownCapable` | `boolean` | Marks the channel as markdown-capable for outbound formatting decisions. | | `showConfigured` | `boolean` | Controls whether configured-channel listing surfaces show this channel. | | `quickstartAllowFrom` | `boolean` | Opt this channel into the standard quickstart `allowFrom` setup flow. | | `forceAccountBinding` | `boolean` | Require explicit account binding even when only one account exists. | | `preferSessionLookupForAnnounceTarget` | `boolean` | Prefer session lookup when resolving announce targets for this channel. | Example: ```json { "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, "quickstartAllowFrom": true } } } ``` ### `openclaw.install` `openclaw.install` is package metadata, not manifest metadata. | Field | Type | What it means | | ---------------------------- | -------------------- | -------------------------------------------------------------------------------- | | `npmSpec` | `string` | Canonical npm spec for install/update flows. | | `localPath` | `string` | Local development or bundled install path. | | `defaultChoice` | `"npm"` \| `"local"` | Preferred install source when both are available. | | `minHostVersion` | `string` | Minimum supported OpenClaw version in the form `>=x.y.z`. | | `allowInvalidConfigRecovery` | `boolean` | Lets bundled-plugin reinstall flows recover from specific stale-config failures. | If `minHostVersion` is set, install and manifest-registry loading both enforce it. Older hosts skip the plugin; invalid version strings are rejected. `allowInvalidConfigRecovery` is not a general bypass for broken configs. It is for narrow bundled-plugin recovery only, so reinstall/setup can repair known upgrade leftovers like a missing bundled plugin path or stale `channels.` entry for that same plugin. If config is broken for unrelated reasons, install still fails closed and tells the operator to run `openclaw doctor --fix`. ### Deferred full load Channel plugins can opt into deferred loading with: ```json { "openclaw": { "extensions": ["./index.ts"], "setupEntry": "./setup-entry.ts", "startup": { "deferConfiguredChannelFullLoadUntilAfterListen": true } } } ``` When enabled, OpenClaw loads only `setupEntry` during the pre-listen startup phase, even for already-configured channels. The full entry loads after the gateway starts listening. Only enable deferred loading when your `setupEntry` registers everything the gateway needs before it starts listening (channel registration, HTTP routes, gateway methods). If the full entry owns required startup capabilities, keep the default behavior. If your setup/full entry registers gateway RPC methods, keep them on a plugin-specific prefix. Reserved core admin namespaces (`config.*`, `exec.approvals.*`, `wizard.*`, `update.*`) stay core-owned and always resolve to `operator.admin`. ## Plugin manifest Every native plugin must ship an `openclaw.plugin.json` in the package root. OpenClaw uses this to validate config without executing plugin code. ```json { "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 `kind` and `channels`: ```json { "id": "my-channel", "kind": "channel", "channels": ["my-channel"], "configSchema": { "type": "object", "additionalProperties": false, "properties": {} } } ``` Even plugins with no config must ship a schema. An empty schema is valid: ```json { "id": "my-plugin", "configSchema": { "type": "object", "additionalProperties": false } } ``` See [Plugin Manifest](/plugins/manifest) for the full schema reference. ## ClawHub publishing For plugin packages, use the package-specific ClawHub command: ```bash clawhub package publish your-org/your-plugin --dry-run clawhub package publish your-org/your-plugin ``` The legacy skill-only publish alias is for skills. Plugin packages should always use `clawhub package publish`. ## Setup entry The `setup-entry.ts` file is a lightweight alternative to `index.ts` that OpenClaw loads when it only needs setup surfaces (onboarding, config repair, disabled channel inspection). ```typescript // setup-entry.ts import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core"; import { myChannelPlugin } from "./src/channel.js"; export default defineSetupPluginEntry(myChannelPlugin); ``` This avoids loading heavy runtime code (crypto libraries, CLI registrations, background services) during setup flows. **When OpenClaw uses `setupEntry` instead of the full entry:** - The channel is disabled but needs setup/onboarding surfaces - The channel is enabled but unconfigured - Deferred loading is enabled (`deferConfiguredChannelFullLoadUntilAfterListen`) **What `setupEntry` must register:** - The channel plugin object (via `defineSetupPluginEntry`) - Any HTTP routes required before gateway listen - Any gateway methods needed during startup Those startup gateway methods should still avoid reserved core admin namespaces such as `config.*` or `update.*`. **What `setupEntry` should NOT include:** - CLI registrations - Background services - Heavy runtime imports (crypto, SDKs) - Gateway methods only needed after startup ### Narrow setup helper imports For hot setup-only paths, prefer the narrow setup helper seams over the broader `plugin-sdk/setup` umbrella when you only need part of the setup surface: | Import path | Use it for | Key exports | | ---------------------------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `plugin-sdk/setup-runtime` | setup-time runtime helpers that stay available in `setupEntry` / deferred channel startup | `noteChannelLookupFailure`, `noteChannelLookupSummary`, `promptResolvedAllowFrom`, `splitSetupEntries`, `createAllowlistSetupWizardProxy`, `createDelegatedSetupWizardProxy` | | `plugin-sdk/setup-adapter-runtime` | environment-aware account setup adapters | `createEnvPatchedAccountSetupAdapter` | | `plugin-sdk/setup-tools` | setup/install CLI/archive/docs helpers | `formatCliCommand`, `detectBinary`, `extractArchive`, `resolveBrewExecutable`, `formatDocsLink`, `CONFIG_DIR` | Use the broader `plugin-sdk/setup` seam when you want the full shared setup toolbox, including config-patch helpers such as `moveSingleAccountChannelSectionToDefaultAccount(...)`. ### Channel-owned single-account promotion When a channel upgrades from a single-account top-level config to `channels..accounts.*`, the default shared behavior is to move promoted account-scoped values into `accounts.default`. Bundled channels can narrow or override that promotion through their setup contract surface: - `singleAccountKeysToMove`: extra top-level keys that should move into the promoted account - `namedAccountPromotionKeys`: when named accounts already exist, only these keys move into the promoted account; shared policy/delivery keys stay at the channel root - `resolveSingleAccountPromotionTarget(...)`: choose which existing account receives promoted values Matrix is the current bundled example. If exactly one named Matrix account already exists, or if `defaultAccount` points at an existing non-canonical key such as `Ops`, promotion preserves that account instead of creating a new `accounts.default` entry. ## Config schema Plugin config is validated against the JSON Schema in your manifest. Users configure plugins via: ```json5 { plugins: { entries: { "my-plugin": { config: { webhookSecret: "abc123", }, }, }, }, } ``` Your plugin receives this config as `api.pluginConfig` during registration. For channel-specific config, use the channel config section instead: ```json5 { channels: { "my-channel": { token: "bot-token", allowFrom: ["user1", "user2"], }, }, } ``` ### Building channel config schemas Use `buildChannelConfigSchema` from `openclaw/plugin-sdk/core` to convert a Zod schema into the `ChannelConfigSchema` wrapper that OpenClaw validates: ```typescript import { z } from "zod"; import { buildChannelConfigSchema } from "openclaw/plugin-sdk/core"; 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); ``` ## Setup wizards Channel plugins can provide interactive setup wizards for `openclaw onboard`. The wizard is a `ChannelSetupWizard` object on the `ChannelPlugin`: ```typescript 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), }; }, }, ], }; ``` The `ChannelSetupWizard` type supports `credentials`, `textInputs`, `dmPolicy`, `allowFrom`, `groupAccess`, `prepare`, `finalize`, and more. See bundled plugin packages (for example the Discord plugin `src/channel.setup.ts`) for full examples. 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(...)`, `createTopLevelChannelParsedAllowFromPrompt(...)`, and `createNestedChannelParsedAllowFromPrompt(...)`. For channel setup status blocks that only vary by labels, scores, and optional extra lines, prefer `createStandardChannelSetupStatus(...)` from `openclaw/plugin-sdk/setup` instead of hand-rolling the same `status` object in each plugin. For optional setup surfaces that should only appear in certain contexts, use `createOptionalChannelSetupSurface` from `openclaw/plugin-sdk/channel-setup`: ```typescript 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 } ``` `plugin-sdk/channel-setup` also exposes the lower-level `createOptionalChannelSetupAdapter(...)` and `createOptionalChannelSetupWizard(...)` builders when you only need one half of that optional-install surface. The generated optional adapter/wizard fail closed on real config writes. They reuse one install-required message across `validateInput`, `applyAccountConfig`, and `finalize`, and append a docs link when `docsPath` is set. For binary-backed setup UIs, prefer the shared delegated helpers instead of copying the same binary/status glue into every channel: - `createDetectedBinaryStatus(...)` for status blocks that vary only by labels, hints, scores, and binary detection - `createCliPathTextInput(...)` for path-backed text inputs - `createDelegatedSetupWizardStatusResolvers(...)`, `createDelegatedPrepare(...)`, `createDelegatedFinalize(...)`, and `createDelegatedResolveConfigured(...)` when `setupEntry` needs to forward to a heavier full wizard lazily - `createDelegatedTextInputShouldPrompt(...)` when `setupEntry` only needs to delegate a `textInputs[*].shouldPrompt` decision ## Publishing and installing **External plugins:** publish to [ClawHub](/tools/clawhub) or npm, then install: ```bash openclaw plugins install @myorg/openclaw-my-plugin ``` OpenClaw tries ClawHub first and falls back to npm automatically. You can also force ClawHub explicitly: ```bash openclaw plugins install clawhub:@myorg/openclaw-my-plugin # ClawHub only ``` There is no matching `npm:` override. Use the normal npm package spec when you want the npm path after ClawHub fallback: ```bash openclaw plugins install @myorg/openclaw-my-plugin ``` **In-repo plugins:** place under the bundled plugin workspace tree and they are automatically discovered during build. **Users can install:** ```bash openclaw plugins install ``` For npm-sourced installs, `openclaw plugins install` runs `npm install --ignore-scripts` (no lifecycle scripts). Keep plugin dependency trees pure JS/TS and avoid packages that require `postinstall` builds. ## Related - [SDK Entry Points](/plugins/sdk-entrypoints) -- `definePluginEntry` and `defineChannelPluginEntry` - [Plugin Manifest](/plugins/manifest) -- full manifest schema reference - [Building Plugins](/plugins/building-plugins) -- step-by-step getting started guide