Plugin Setup and Configuration Reference

Reference for packaging OpenClaw plugins with package.json metadata, manifests, setup entries, and config schemas. For developers creating channel or provider plugins.

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 packaging plugins (package.json metadata), manifests (openclaw.plugin.json), setup entries, and config schemas.

Tip

Need a step-by-step guide? The how-to docs show packaging in action: Channel plugins and Provider plugins.

Package metadata

A package.json must carry an openclaw field so the plugin system knows what it provides:

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

For external ClawHub publishing, compat and build are mandatory. Canonical publish examples are located in docs/snippets/plugin-publish/.

openclaw fields

  • extensions (string[]), Files that act as entry points, relative to the package root. These are valid source entries for workspace and git checkout development.

  • runtimeExtensions (string[]), Built JavaScript counterparts for extensions, preferred when OpenClaw loads an installed npm package. The resolution order between source and built is covered in SDK entry points.

  • setupEntry (string), A minimal setup-only entry, optional.

  • runtimeSetupEntry (string), Built JavaScript counterpart for setupEntry. Setting setupEntry is also required.

  • plugin (object), Fallback identity for { id, label }, applied when a plugin lacks channel/provider metadata to derive an id or label.

  • channel (object), Channel catalog metadata for setup, picker, quickstart, and status surfaces.

  • install (object), Installation guidance covering npmSpec, localPath, defaultChoice, minHostVersion, expectedIntegrity, allowInvalidConfigRecovery, and requiredPlatformPackages.

  • startup (object), Flags controlling startup behavior.

  • compat (object), The pluginApi version range this plugin supports. External ClawHub publishes require it.

Note

Provider ids (providers: string[]) belong to manifest metadata, not package metadata. Declare them in openclaw.plugin.json, not here, see Plugin manifest.

openclaw.channel

openclaw.channel serves as 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 equal 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.

For a boolean useEnv field, assign envVars to the static environment variable names that the plugin runtime depends on. When any declared variable is empty, non-interactive channel setup will reject --use-env before config is written. If a single variable from the list suffices, such as an inline credential or an alternative file path, set envVarMode: "any". Leaving envVars unset keeps the plugin's current validation behavior intact.

Existing external plugins can continue using the released setup/ChannelSetupInput adapter. New plugins should expose setupContract, and OpenClaw always selects it when both options are present.

FieldTypeWhat it means
idstringThe canonical identifier for the channel.
labelstringThe main label shown for the channel.
selectionLabelstringLabel for picker/setup when it needs to differ from label.
detailLabelstringSecondary detail label for richer channel catalogs and status surfaces.
docsPathstringDocs path used for setup and selection links.
docsLabelstringOverride label for docs links when it should differ from the channel id.
blurbstringShort description for onboarding and catalogs.
ordernumberSort order within channel catalogs.
aliasesstring[]Additional lookup aliases for channel selection.
preferOverstring[]Lower-priority plugin/channel ids this channel should outrank.
systemImagestringOptional icon or system-image name for channel UI catalogs.
selectionDocsPrefixstringPrefix text before docs links in selection surfaces.
selectionDocsOmitLabelbooleanShow the docs path directly instead of a labeled docs link in selection copy.
selectionExtrasstring[]Extra short strings appended in selection copy.
markdownCapablebooleanMarks the channel as markdown-capable for outbound formatting decisions.
exposureobjectChannel visibility controls for setup, configured lists, and docs surfaces.
quickstartAllowFrombooleanOpt this channel into the standard quickstart allowFrom setup flow.
forceAccountBindingbooleanRequire explicit account binding even when only one account exists.
preferSessionLookupForAnnounceTargetbooleanPrefer session lookup when resolving announce targets for this channel.
setupobjectSerializable channel-owned setup fields used 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
    }
  }
}

The following capabilities are provided by exposure:

  • configured: the channel appears in configured and status style listing surfaces
  • setup: interactive setup and configure pickers include the channel
  • docs: documentation and navigation surfaces treat the channel as public-facing

openclaw.install

openclaw.install belongs to package metadata, not to manifest metadata.

FieldTypeWhat it means
clawhubSpecstringThe standard ClawHub spec governing install/update and onboarding's install-on-demand flows.
npmSpecstringThe standard npm spec used as a fallback for install/update operations.
localPathstringA path for local development or when the plugin is bundled.
defaultChoice"clawhub" | "npm" | "local"Which source to prefer for installation when several are available.
minHostVersionstringThe lowest OpenClaw version supported, either >=x.y.z or >=x.y.z-prerelease.
expectedIntegritystringThe expected npm dist integrity string, typically sha512-..., used for pinned installs.
allowInvalidConfigRecoverybooleanAllows bundled-plugin reinstall flows to recover from specific stale-config errors.
requiredPlatformPackagesstring[]Platform-specific npm aliases that must be verified during npm install.

Onboarding behavior

For install-on-demand surfaces, interactive onboarding relies on openclaw.install: when your plugin exposes provider auth choices or channel setup/catalog metadata before runtime loads, onboarding can prompt for a ClawHub, npm, or local install, install or enable the plugin, and then proceed with the chosen flow. ClawHub options use clawhubSpec and take precedence when available; npm options need trusted catalog metadata with a registry npmSpec (exact versions and expectedIntegrity act as optional pins, enforced on install/update if 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; invalid version strings are rejected. Bundled source plugins are assumed to share the host checkout's version.

Pinned npm installs

For pinned npm installs, store 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 configs. It serves only narrow bundled-plugin recovery, allowing reinstall/setup to fix known upgrade leftovers such as a missing bundled plugin path or a stale channels.<id> entry for that same plugin. If config is broken for unrelated reasons, install still fails closed and directs the operator to run openclaw doctor --fix.

Setup-time gateway methods

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.*) remain core-owned 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 config without running 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 without config must provide a schema. An empty schema is valid:

{
  "id": "my-plugin",
  "configSchema": {
    "type": "object",
    "additionalProperties": false
  }
}

Refer to 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 for publishing a skill folder, not a plugin package. See Publishing on ClawHub.

Setup entry

setup-entry.ts is a lightweight alternative to index.ts that OpenClaw loads when it only needs setup surfaces (onboarding, config repair, disabled channel inspection):

// setup-entry.ts
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { myChannelPlugin } from "./src/channel.js";

export default defineSetupPluginEntry(myChannelPlugin);

This prevents loading heavy runtime code (crypto libraries, CLI registrations, background services) during setup flows.

Bundled workspace channels that keep setup-safe exports in sidecar modules can use defineBundledChannelSetupEntry(...) from openclaw/plugin-sdk/channel-entry-contract rather than defineSetupPluginEntry(...). That bundled contract additionally supports an optional runtime export, allowing setup-time runtime wiring to remain lightweight and explicit.

When OpenClaw uses setupEntry instead of the full entry

  • The channel is disabled but still requires setup or onboarding surfaces.
  • The channel is enabled yet remains unconfigured.

What setupEntry must register

  • The channel plugin object, accessed via defineSetupPluginEntry.
  • Setup-time runtime surfaces declared through registerSetupRuntime, when applicable.

Setup-time gateway methods should continue to avoid reserved core admin namespaces like config.* or update.*.

What setupEntry should NOT include

  • CLI registrations.
  • Background services.
  • Heavy runtime imports, including crypto and SDKs.
  • Gateway methods needed only 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 only part of the setup surface is required:

Import pathUse it forKey exports
plugin-sdk/setup-runtimesetup-time runtime helpers that stay available in setupEntrycreateSetupTranslator, createPatchedAccountSetupAdapter, createEnvPatchedAccountSetupAdapter, createSetupInputPresenceValidator, noteChannelLookupFailure, noteChannelLookupSummary, promptResolvedAllowFrom, splitSetupEntries, createAllowlistSetupWizardProxy, createDelegatedSetupWizardProxy
plugin-sdk/setup-toolssetup/install CLI/archive/docs helpersformatCliCommand, detectBinary, extractArchive, resolveBrewExecutable, formatDocsLink, CONFIG_DIR

When the complete shared setup toolbox is needed, including config-patch helpers such as moveSingleAccountChannelSectionToDefaultAccount(...), the broader plugin-sdk/setup seam should be used.

Fixed setup wizard copy comes from createSetupTranslator(...). It picks the first nonblank value among OPENCLAW_LOCALE, LC_ALL, LC_MESSAGES, and LANG, in that sequence, defaulting to English if none are present. Set OPENCLAW_LOCALE=en to force an English override explicitly. Plugin-specific setup text belongs in plugin-owned code; shared catalog keys should be reserved for common setup labels, status text, and official bundled plugin setup copy.

The 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 serves as 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 may still appear on the runtime input object, but the shared type declares no 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 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

The presence of singleAccountKeysToMove marks the promotion contract complete. Declare the field even when it is an empty array to opt out of legacy key promotion. Adapters that omit the field retain a reader-backed pre-declaration promotion tier for already-published plugins. The 2026-07-22 registry sweep removed 23 keys with no published dependents and retained six common keys plus the setup-only rooms key. Each retained key is deleted as soon as its published readers migrate to declarations; no version boundary is required.

Declare openclaw.setupFeatures.configPromotion: true in the plugin package manifest when doctor must load these declarations from the lightweight bundled setup artifact. The setup-only plugin surface and the full channel plugin must expose the same declarations.

When you call moveSingleAccountChannelSectionToDefaultAccount(...) with a plugin that has already been resolved, supply its setup adapter through setupSurface. Setup surfaces provided by the caller take priority over both loaded and bundled lookup, so scoped or setup-only plugins do not need to rely on global registration.

Note

Matrix serves as the current bundled example. When a single named Matrix account is already present, or when defaultAccount refers to an existing non-canonical key such as Ops, promotion keeps that account intact rather than generating a fresh accounts.default entry.

Config schema

Plugin configuration is checked against the JSON Schema defined in your manifest. Users set up plugins through:

{
  plugins: {
    entries: {
      "my-plugin": {
        config: {
          webhookSecret: "abc123",
        },
      },
    },
  },
}

At registration time, your plugin receives this configuration as api.pluginConfig.

For configuration tied to a specific channel, turn to the channel config section:

{
  channels: {
    "my-channel": {
      token: "bot-token",
      allowFrom: ["user1", "user2"],
    },
  },
}

Building channel config schemas

Use buildChannelConfigSchema to turn a Zod schema into the ChannelConfigSchema wrapper that plugin-owned config artifacts rely on:

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 your contract is already written as JSON Schema or TypeBox, go with the direct helper so OpenClaw avoids 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: copy the generated JSON Schema into openclaw.plugin.json#channelConfigs so config schema, setup, and UI surfaces can look at channels.<id> without loading runtime code.

Setup wizards

Channel plugins can offer interactive setup wizards for openclaw onboard. The wizard takes the form of a ChannelSetupWizard object placed 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),
        };
      },
    },
  ],
};

Beyond that, ChannelSetupWizard supports textInputs, dmPolicy, allowFrom, groupAccess, prepare, finalize, and others. A complete bundled example lives in 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, reach for the shared setup helpers in openclaw/plugin-sdk/setup: createPromptParsedAllowFromForAccount(...) and createTopLevelChannelParsedAllowFromPrompt(...).

Standard channel setup status

When channel setup status blocks differ only in labels, scores, and optional extra lines, use createStandardChannelSetupStatus(...) from openclaw/plugin-sdk/setup rather than building the same status object by hand in every plugin.

Optional channel setup surface

For optional setup surfaces meant to show up only in specific 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 }

plugin-sdk/channel-setup additionally exposes the lower-level createOptionalChannelSetupAdapter(...) and createOptionalChannelSetupWizard(...) builders when you need just one half of that optional-install surface.

The generated optional adapter and wizard fail closed on real config writes. They share a single install-required message across validateInput, applyAccountConfig, and finalize, and attach a docs link when docsPath is set.

Binary-backed setup helpers

For binary-backed setup UIs, use the shared delegated helpers instead of duplicating the binary and status glue in every channel:

  • createDetectedBinaryStatus(...) for status blocks that differ only in labels, hints, scores, and binary detection
  • createCliPathTextInput(...) for path-backed text inputs
  • createDelegatedSetupWizardProxy(...) when setupEntry needs to pass status, prepare, or finalize behavior to a heavier full wizard lazily
  • createDelegatedTextInputShouldPrompt(...) when setupEntry only needs to hand off a textInputs[*].shouldPrompt decision

Publishing and installing

External plugins: publish to ClawHub, then install:

npm

openclaw plugins install @myorg/openclaw-my-plugin

Bare package specs pull directly from npm during the launch cutover, except when the name matches a bundled or official plugin id, in which case OpenClaw falls back to that local/official copy. To pin the source deterministically, use clawhub:, npm:, git:, or npm-pack:, see Manage plugins.

ClawHub only

openclaw plugins install clawhub:@myorg/openclaw-my-plugin

npm package spec

Choose npm when a package has not yet migrated to ClawHub, or when a direct npm install path is needed during the migration window:

openclaw plugins install npm:@myorg/openclaw-my-plugin

In-repo plugins: place them under the bundled plugin workspace tree; the build discovers them automatically.

Info

For installs sourced from npm, openclaw plugins install places the package into a per-plugin project under ~/.openclaw/npm/projects and disables lifecycle scripts (--ignore-scripts). Keep plugin dependency trees pure JS/TS and steer clear of packages that demand postinstall builds.

Note

Gateway startup never installs plugin dependencies. Dependency convergence is handled by the npm, git, and ClawHub install flows; local plugins must have their dependencies already in place.

Bundled package metadata is declared explicitly, never inferred from built JavaScript at gateway startup. Runtime dependencies belong to the plugin package that owns them; packaged OpenClaw startup neither repairs nor mirrors plugin dependencies.

3,393 words · updated Aug 17, 2026