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 compat and build. Canonical publish snippets are located in docs/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 for extensions, 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 for setupEntry. setupEntry must 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), The pluginApi version range this plugin supports. Required for external ClawHub publishing.

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 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.

FieldTypeDescription
idstringUnique identifier for the channel.
labelstringMain name shown for the channel.
selectionLabelstringLabel used in picker and setup interfaces when it needs to differ from label.
detailLabelstringSecondary label for richer channel catalogs and status displays.
docsPathstringDocumentation path for setup and selection links.
docsLabelstringOverrides the label in documentation links when it should not match the channel id.
blurbstringShort description for onboarding and catalog views.
ordernumberPosition in channel catalog sorting.
aliasesstring[]Additional aliases used during channel selection lookups.
preferOverstring[]Lower-priority plugin or channel ids that this channel should take precedence over.
systemImagestringOptional icon or system image name for channel UI catalogs.
selectionDocsPrefixstringText prepended to documentation links in selection interfaces.
selectionDocsOmitLabelbooleanDisplays the documentation path directly rather than a labeled link in selection text.
selectionExtrasstring[]Extra short strings appended to selection text.
markdownCapablebooleanIndicates the channel supports markdown for outbound formatting decisions.
exposureobjectControls channel visibility in setup, configured lists, and documentation surfaces.
quickstartAllowFrombooleanEnables the standard quickstart allowFrom setup flow for this channel.
forceAccountBindingbooleanForces explicit account binding even if only one account is available.
preferSessionLookupForAnnounceTargetbooleanUses session lookup when resolving announce targets for this channel.
setupobjectSerializable 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 surfaces
  • setup: shows the channel in interactive setup and configuration pickers
  • docs: marks the channel as public in documentation and navigation surfaces

openclaw.install

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

FieldTypeWhat it means
clawhubSpecstringThe official ClawHub spec for install, update, and onboarding install-on-demand workflows.
npmSpecstringThe official npm spec used as a fallback during install and update operations.
localPathstringA path for local development or for installing a bundled plugin.
defaultChoice"clawhub" | "npm" | "local"The preferred installation source when more than one is available.
minHostVersionstringThe lowest OpenClaw version that is supported, either >=x.y.z or >=x.y.z-prerelease.
expectedIntegritystringThe expected npm distribution integrity string, typically sha512-..., used for pinned installs.
allowInvalidConfigRecoverybooleanEnables bundled plugin reinstall flows to recover from certain stale configuration failures.
requiredPlatformPackagesstring[]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 setupEntry registers 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 pathUse it forKey exports
plugin-sdk/setup-runtimesetup-time runtime helpers that remain available in setupEntry or during deferred channel startupcreateSetupTranslator, createPatchedAccountSetupAdapter, createEnvPatchedAccountSetupAdapter, createSetupInputPresenceValidator, noteChannelLookupFailure, noteChannelLookupSummary, promptResolvedAllowFrom, splitSetupEntries, createAllowlistSetupWizardProxy, createDelegatedSetupWizardProxy
plugin-sdk/setup-toolssetup, install, CLI, archive, or docs helpersformatCliCommand, 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 account
  • namedAccountPromotionKeys: when named accounts already exist, only these keys move into the promoted account; shared policy or delivery keys stay at the channel root
  • resolveSingleAccountPromotionTarget(...): 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 defaultAccount points at an existing non-canonical key such as Ops, promotion keeps that account rather than creating a new accounts.default entry.

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 detection
  • createCliPathTextInput(...) for path-backed text inputs
  • createDelegatedSetupWizardProxy(...) when setupEntry needs to lazily forward status, prepare, or finalize behavior to a heavier full wizard
  • createDelegatedTextInputShouldPrompt(...) when setupEntry only needs to delegate a textInputs[*].shouldPrompt decision

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 install places the package into a per-plugin project under ~/.openclaw/npm/projects with lifecycle scripts turned off (--ignore-scripts). Keep plugin dependency trees limited to pure JS/TS and avoid packages that need postinstall builds.

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.