Plugin maintainer reference
Package packs and import paths
Which SDK subpaths a plugin should import, how a package pack turns extensions into plugins, and the catalog and install metadata a channel package can publish. Part of the Plugin architecture internals guide.
Plugin SDK import paths
Use narrow SDK subpaths instead of the monolithic openclaw/plugin-sdk root
barrel when authoring new plugins. Core subpaths:
| Subpath | Purpose |
|---|---|
openclaw/plugin-sdk/plugin-entry |
Plugin registration primitives |
openclaw/plugin-sdk/channel-core |
Channel entry/build helpers |
openclaw/plugin-sdk/core |
Generic shared helpers and umbrella contract |
Channel plugins pick from a family of narrow seams — channel-setup,
setup-runtime, setup-tools, channel-pairing,
channel-contract, channel-feedback, channel-inbound, channel-outbound,
command-auth, secret-input, webhook-ingress,
channel-targets, and channel-actions. Approval behavior should consolidate
on one approvalCapability contract rather than mixing across unrelated
plugin fields. See Channel plugins.
Runtime and config helpers live under matching focused *-runtime subpaths
(approval-runtime, agent-runtime, lazy-runtime, directory-runtime,
text-utility-runtime, runtime-store, system-event-runtime, heartbeat-runtime,
channel-activity-runtime, etc.). Prefer config-contracts,
plugin-config-runtime, runtime-config-snapshot, and config-mutation
instead of the broad config-runtime compatibility barrel.
Repo-internal entry points (per bundled plugin package root):
index.js— bundled plugin entryapi.js— helper/types barrelruntime-api.js— runtime-only barrelsetup-entry.js— setup plugin entry
External plugins should only import openclaw/plugin-sdk/* subpaths. Never
Facade-loaded entry points prefer the active runtime config snapshot when one exists, then fall back to the resolved config file on disk.
Capability-specific subpaths such as image-generation, media-understanding,
and speech exist because bundled plugins use them today. They are not
automatically long-term frozen external contracts — check the relevant SDK
reference page when relying on them.
Package packs
A plugin directory may include a package.json with openclaw.extensions:
{ "name": "my-pack", "openclaw": { "extensions": ["./src/safety.ts", "./src/tools.ts"], "setupEntry": "./src/setup-entry.ts" }}Each entry becomes a plugin. If the pack lists multiple extensions, the plugin
id becomes <manifestOrPackageName>/<fileBase> (manifest id wins when
present; otherwise the unscoped package.json name).
If your plugin imports npm deps, install them in that directory so
node_modules is available (npm install / pnpm install).
Security guardrail: every openclaw.extensions entry must stay inside the plugin
directory after symlink resolution. Entries that escape the package directory are
rejected.
Security note: openclaw plugins install installs plugin dependencies with a
project-local npm install --omit=dev --ignore-scripts (no lifecycle scripts,
no dev dependencies at runtime), ignoring inherited global npm install settings.
Keep plugin dependency trees "pure JS/TS" and avoid packages that require
postinstall builds.
Optional: openclaw.setupEntry can point at a lightweight setup-only module.
When OpenClaw needs setup surfaces for a disabled channel plugin, or
when a channel plugin is enabled but still unconfigured, it loads setupEntry
instead of the full plugin entry. This keeps startup and setup lighter
when your main plugin entry also wires tools, hooks, or other runtime-only
code.
Bundled channels can also publish setup-only contract-surface helpers that core can consult before the full channel runtime is loaded. The current setup promotion surface is:
singleAccountKeysToMovenamedAccountPromotionKeysresolveSingleAccountPromotionTarget(...)
Core uses that surface when it needs to promote a legacy single-account channel
config into channels.<id>.accounts.* without loading the full plugin entry.
Matrix is the current bundled example: it moves only auth/bootstrap keys into a
named promoted account when named accounts already exist, and it can preserve a
configured non-canonical default-account key instead of always creating
accounts.default.
Those setup patch adapters keep bundled contract-surface discovery lazy. Import time stays light; the promotion surface is loaded only on first use instead of re-entering bundled channel startup on module import.
When setup surfaces include gateway RPC methods, keep them on a
plugin-specific prefix. Core admin namespaces (config.*,
exec.approvals.*, wizard.*, update.*) remain reserved and always resolve
to operator.admin, even if a plugin requests a narrower scope.
Channel catalog metadata
Channel plugins can advertise setup/discovery metadata via openclaw.channel and
install hints via openclaw.install. This keeps the core catalog data-free.
Example:
{ "name": "@openclaw/nextcloud-talk", "openclaw": { "extensions": ["./index.ts"], "channel": { "id": "nextcloud-talk", "label": "Nextcloud Talk", "selectionLabel": "Nextcloud Talk (self-hosted)", "docsPath": "/channels/nextcloud-talk", "docsLabel": "nextcloud-talk", "blurb": "Self-hosted chat via Nextcloud Talk webhook bots.", "order": 65, "aliases": ["nc-talk", "nc"] }, "install": { "npmSpec": "@openclaw/nextcloud-talk", "localPath": "<bundled-plugin-local-path>", "defaultChoice": "npm" } }}Useful openclaw.channel fields beyond the minimal example:
detailLabel: secondary label for richer catalog/status surfacesdocsLabel: override link text for the docs linkpreferOver: lower-priority plugin/channel ids this catalog entry should outrankselectionDocsPrefix,selectionDocsOmitLabel,selectionExtras: selection-surface copy controlsmarkdownCapable: marks the channel as markdown-capable for outbound formatting decisionsexposure.configured: hide the channel from configured-channel listing surfaces when set tofalseexposure.setup: hide the channel from interactive setup/configure pickers when set tofalseexposure.docs: mark the channel as internal/private for docs navigation surfacesquickstartAllowFrom: opt the channel into the standard quickstartallowFromflowforceAccountBinding: require explicit account binding even when only one account existspreferSessionLookupForAnnounceTarget: prefer session lookup when resolving announce targets
OpenClaw can also merge external channel catalogs (for example, an MPM registry export). Drop a JSON file at one of:
~/.openclaw/mpm/plugins.json~/.openclaw/mpm/catalog.json~/.openclaw/plugins/catalog.json
Or point OPENCLAW_PLUGIN_CATALOG_PATHS (or OPENCLAW_MPM_CATALOG_PATHS) at
one or more JSON files (comma/semicolon/PATH-delimited). Each file should
contain { "entries": [ { "name": "@scope/pkg", "openclaw": { "channel": {...}, "install": {...} } } ] }. The parser also accepts "packages" or "plugins" as legacy aliases for the "entries" key.
Generated channel catalog entries and provider install catalog entries expose
normalized install-source facts next to the raw openclaw.install block. The
normalized facts identify whether the npm spec is an exact version or floating
selector, whether expected integrity metadata is present, and whether a local
source path is also available. When the catalog/package identity is known, the
normalized facts warn if the parsed npm package name drifts from that identity.
They also warn when defaultChoice is invalid or points at a source that is
not available, and when npm integrity metadata is present without a valid npm
source. Consumers should treat installSource as an additive optional field so
hand-built entries and catalog shims do not have to synthesize it.
This lets onboarding and diagnostics explain source-plane state without
importing plugin runtime.
Official external npm entries should prefer an exact npmSpec plus
expectedIntegrity. Bare package names and dist-tags still work for
compatibility, but they surface source-plane warnings so the catalog can move
toward pinned, integrity-checked installs without breaking existing plugins.
When an official package is renamed, the catalog entry may declare
legacyNpmPackageNames with the former package names. Trusted update rewrites
matching npm records to the current npmSpec, and migrates a catalog lookup
alias such as a channel id to the canonical plugin id. Duplicate alias+canonical
records drop only when the canonical install is also trusted official.
legacyPluginIds remains the contract for plugin-id cutovers that are not lookup
aliases.
When onboarding installs from a local catalog path, it records a managed plugin
plugin index entry with source: "path" and a workspace-relative
sourcePath when possible. The absolute operational load path stays in
plugins.load.paths; the install record avoids duplicating local workstation
paths into long-lived config. This keeps local development installs visible to
source-plane diagnostics without adding a second raw filesystem-path disclosure
surface. The persisted config_machine_state value under
plugins.installedIndex is the install source of truth and can be refreshed
without loading plugin runtime modules. Its installRecords map is durable
even when a plugin manifest is missing or invalid; its plugins payload is a
rebuildable manifest view.