Skill: tenequm/openclaw-reference (version 2026.4.8)

--- name: openclaw-ref description: OpenClaw platform reference - plugin system, extensions, configuration, boot/provisioning, channels, models, CLI. Use when working on openclaw codebase, building openclaw plugins/extensions, configuring openclaw instances, provisioning openclaw gateways, designing agent provisioning flows (e.g. agentbox), or debugging openclaw config/plugin/channel issues. Triggers on openclaw, openclaw config, openclaw plugin, openclaw extension, openclaw channel, openclaw gateway, openclaw provisioning, openclaw onboarding, openclaw boot, openclaw skills, BOOT.md, openclaw.plugin.json, openclaw-x402, agentbox provisioning. metadata: version: "2026.4.8" last_refresh_sha: cfe71e2e44c0c57329985dc20986138a4b605892 --- # OpenClaw Platform Reference Structured reference for the OpenClaw assistant platform. Source repo: `~/Projects/openclaw/`. Tracking version: **2026.4.7** (refresh: 2026-04-07). ## Quick Navigation Load the relevant reference file based on the task: ### Plugin & Extension System - **`references/plugin-system.md`** - Plugin discovery, loading, manifest format, registration API, installation CLI, SDK exports. Read when building/debugging plugins or understanding how extensions are loaded. ### Configuration - **`references/configuration.md`** - Full config structure (`OpenClawConfig`), config paths, file format (JSON5), env var substitution, `$include` directives, validation (Zod), per-plugin config, programmatic read/write. Read when modifying or generating openclaw config. ### Boot & Provisioning - **`references/boot-provisioning.md`** - Gateway startup sequence, BOOT.md mechanism, onboarding flows (interactive/non-interactive), plugin loading during boot, sidecar startup order, health checks. Read when designing automated provisioning or debugging startup. ### Channels & Extensions - **`references/channels-extensions.md`** - All built-in channels (Telegram, Discord, Slack, Signal, iMessage, WhatsApp, Web) + extension channels (MS Teams, Matrix, Zalo, Voice Call, Feishu). Channel plugin registration, per-channel config, Telegram specifics. Read when adding/configuring channels. ### Models & Providers - **`references/models-providers.md`** - Provider configuration, `models.mode` (merge/replace), x402 providers, model catalog structure, provider registration via plugins. Read when configuring model access or building provider plugins. ### CLI Commands - **`references/cli-commands.md`** - Key CLI commands for config, plugins, channels, agents, onboarding, gateway, skills. Read when scripting openclaw setup or building automation. ### GitHub Context (live issues, PRs, gotchas) - **`references/github-context.md`** - Open bugs, breaking changes, recent impactful PRs, plugin/config known issues, dev gotchas synthesized from GitHub. Refreshed via `/refresh-openclaw`. Read before starting any non-trivial openclaw work to avoid known pitfalls. ## Key File Paths (repo-root relative) ``` src/plugins/ - Plugin loader, discovery, registry, install, types src/plugin-sdk/ - 100+ scoped exports for plugin consumption src/config/ - Config loading, types, validation, defaults, paths src/gateway/ - Gateway server, boot, startup, server methods src/commands/ - CLI commands (onboard, config, etc.) src/cli/ - CLI wiring (plugins-cli, skills-cli, etc.) extensions/ - Extension implementations (channels, memory, etc.) skills/ - Built-in skill definitions docs/ - Documentation (Mintlify) ``` ## Config File Locations - Main config: `~/.openclaw/openclaw.json` (parsed as JSON5) - Extensions: `~/.openclaw/extensions/` - Skills: `~/.openclaw/skills/` (managed) + workspace skills - Agents: `~/.openclaw/agents/<agent-id>/` - Sessions: `~/.openclaw/agents/<agent-id>/sessions/` - Credentials: `~/.openclaw/credentials/` - Override: `OPENCLAW_CONFIG_PATH` env var ## Plugin Manifest Quick Ref ```json { "id": "plugin-id", "name": "Display Name", "kind": "memory", "channels": ["channel-id"], "providers": ["provider-id"], "configSchema": { "type": "object" }, "uiHints": { "field": { "label": "...", "sensitive": true } } } ``` ## Config Structure Quick Ref Top-level keys in `OpenClawConfig`: `meta`, `auth`, `acp`, `env`, `secrets`, `plugins`, `skills`, `models`, `agents`, `tools`, `channels`, `session`, `hooks`, `gateway`, `logging`, `browser`, `memory`, `messages`, `approvals`, `cron`

path: LICENSE.txt
content: Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

1. Definitions.

"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.

"Licensor" shall mean the copyright owner or entity authorized by the
copyright owner that is granting the License.

"Legal Entity" shall mean the union of the acting entity and all other
entities that control, are controlled by, or are under common control with
that entity. For the purposes of this definition, "control" means (i) the
power, direct or indirect, to cause the direction or management of such
entity, whether by contract or otherwise, or (ii) ownership of fifty percent
(50%) or more of the outstanding shares, or (iii) beneficial ownership of
such entity.

"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.

"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation source, and
configuration files.

"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object
code, generated documentation, and conversions to other media types.

"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that is
included in or attached to the work (an example is provided in the Appendix
below).

"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial
revisions, annotations, elaborations, or other modifications represent, as a
whole, an original work of authorship. For the purposes of this License,
Derivative Works shall not include works that remain separable from, or
merely link (or bind by name) to the interfaces of, the Work and Derivative
Works thereof.

"Contribution" shall mean any work of authorship, including the original
version of the Work and any modifications or additions to that Work or
Derivative Works thereof, that is intentionally submitted to Licensor for
inclusion in the Work by the copyright owner or by an individual or Legal
Entity authorized to submit on behalf of the copyright owner. For the
purposes of this definition, "submitted" means any form of electronic, verbal,
or written communication sent to the Licensor or its representatives,
including but not limited to communication on electronic mailing lists, source
code control systems, and issue tracking systems that are managed by, or on
behalf of, the Licensor for the purpose of discussing and improving the Work,
but excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."

"Contributor" shall mean Licensor and any individual or Legal Entity on
behalf of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.

2. Grant of Copyright License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide,
non-exclusive, no-charge, royalty-free, irrevocable copyright license to
reproduce, prepare Derivative Works of, publicly display, publicly perform,
sublicense, and distribute the Work and such Derivative Works in Source or
Object form.

3. Grant of Patent License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide,
non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
section) patent license to make, have made, use, offer to sell, sell, import,
and otherwise transfer the Work, where such license applies only to those
patent claims licensable by such Contributor that are necessarily infringed by
their Contribution(s) alone or by combination of their Contribution(s) with
the Work to which such Contribution(s) was submitted. If You institute patent
litigation against any entity (including a cross-claim or counterclaim in a
lawsuit) alleging that the Work or a Contribution incorporated within the Work
constitutes direct or contributory patent infringement, then any patent
licenses granted to You under this License for that Work shall terminate as of
the date such litigation is filed.

4. Redistribution. You may reproduce and distribute copies of the Work or
Derivative Works thereof in any medium, with or without modifications, and in
Source or Object form, provided that You meet the following conditions:

(a) You must give any other recipients of the Work or Derivative Works a copy
of this License; and

(b) You must cause any modified files to carry prominent notices stating that
You changed the files; and

(c) You must retain, in the Source form of any Derivative Works that You
distribute, all copyright, patent, trademark, and attribution notices from
the Source form of the Work, excluding those notices that do not pertain to
any part of the Derivative Works; and

(d) If the Work includes a "NOTICE" text file as part of its distribution,
then any Derivative Works that You distribute must include a readable copy of
the attribution notices contained within such NOTICE file, excluding those
notices that do not pertain to any part of the Derivative Works, in at least
one of the following places: within a NOTICE text file distributed as part of
the Derivative Works; within the Source form or documentation, if provided
along with the Derivative Works; or, within a display generated by the
Derivative Works, if and wherever such third-party notices normally appear.
The contents of the NOTICE file are for informational purposes only and do not
modify the License. You may add Your own attribution notices within Derivative
Works that You distribute, alongside or as an addendum to the NOTICE text from
the Work, provided that such additional attribution notices cannot be
construed as modifying the License.

You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a
whole, provided Your use, reproduction, and distribution of the Work otherwise
complies with the conditions stated in this License.

5. Submission of Contributions. Unless You explicitly state otherwise, any
Contribution intentionally submitted for inclusion in the Work by You to the
Licensor shall be under the terms and conditions of this License, without any
additional terms or conditions. Notwithstanding the above, nothing herein
shall supersede or modify the terms of any separate license agreement you may
have executed with Licensor regarding such Contributions.

6. Trademarks. This License does not grant permission to use the trade names,
trademarks, service marks, or product names of the Licensor, except as
required for reasonable and customary use in describing the origin of the Work
and reproducing the content of the NOTICE file.

7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
writing, Licensor provides the Work (and each Contributor provides its
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied, including, without limitation, any warranties
or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any risks
associated with Your exercise of permissions under this License.

8. Limitation of Liability. In no event and under no legal theory, whether in
tort (including negligence), contract, or otherwise, unless required by
applicable law (such as deliberate and grossly negligent acts) or agreed to in
writing, shall any Contributor be liable to You for damages, including any
direct, indirect, special, incidental, or consequential damages of any
character arising as a result of this License or out of the use or inability to
use the Work (including but not limited to damages for loss of goodwill, work
stoppage, computer failure or malfunction, or any and all other commercial
damages or losses), even if such Contributor has been advised of the
possibility of such damages.

9. Accepting Warranty or Additional Liability. While redistributing the Work
or Derivative Works thereof, You may choose to offer, and charge a fee for,
acceptance of support, warranty, indemnity, or other liability obligations
and/or rights consistent with this License. However, in accepting such
obligations, You may act only on Your own behalf and on Your sole
responsibility, not on behalf of any other Contributor, and only if You agree
to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.

END OF TERMS AND CONDITIONS


path: references/boot-provisioning.md
content: # OpenClaw Boot & Provisioning

## Gateway Startup Sequence

Key files: `src/gateway/server-startup.ts`, `src/gateway/boot.ts`

### Boot Order

1. **Load config** - `loadConfig()` reads + validates config
2. **Seed control UI origins** - `maybeSeedControlUiAllowedOriginsAtStartup()` persists `gateway.controlUi.allowedOrigins` for non-loopback installs upgraded to v2026.2.26+ without required origins
3. **Record startup write hash** - if config was mutated (generated token or seeded origins), re-read snapshot and capture `startupInternalWriteHash` so the config reloader ignores this self-inflicted write
4. **Channel plugin startup maintenance** - `runChannelPluginStartupMaintenance()` replaces the old per-channel startup migration (e.g. Matrix migration). Runs generic channel plugin lifecycle hooks before channel start
5. **Session migration** - `runStartupSessionMigration()` migrates legacy session state
6. **Load plugins** - `loadOpenClawPlugins()` discovers and registers all plugins
7. **Clean stale locks** - remove stale session lock files (stale threshold: 30min)
8. **Start Gmail watcher** - if `hooks.gmail.account` configured; validates `hooks.gmail.model` against catalog
9. **Load internal hooks** - clear previous hooks, load from config + directory discovery
10. **Prewarm primary model** - `prewarmConfiguredPrimaryModel()` resolves the configured default model via static resolution (no runtime hooks) to surface configuration errors early
11. **Start channels** - Telegram, Discord, Slack, etc. (skippable via `OPENCLAW_SKIP_CHANNELS=1`)
12. **Fire `gateway:startup` event** - delayed 250ms, plugins can hook into this (requires `hooks.internal.enabled`)
13. **Start plugin services** - `startPluginServices()` for registry lifecycle
14. **ACP identity reconcile** - if `acp.enabled`, reconcile pending session identities on startup
15. **Start memory backend** - `startGatewayMemoryBackend()` for qmd memory initialization (LanceDB runtime bootstrapped on demand at first use)
16. **Schedule restart sentinel** - wake check for updates (delayed 750ms)
17. **Subagent orphan recovery** - `scheduleSubagentOrphanRecovery()` runs on every startup cycle (not only cold restore) because same-process SIGUSR1 restarts keep subagent registry memory alive
18. **Start MCP loopback server** - `startMcpLoopbackServer()` starts an HTTP JSON-RPC server on a random loopback port for MCP tool bridging (e.g. Claude CLI loopback MCP)

### Plugin Loading During Boot

```typescript
function ensurePluginRegistryLoaded() {
  const config = loadConfig();
  const workspaceDir = resolveAgentWorkspaceDir(config, agentId);
  loadOpenClawPlugins({ config, workspaceDir, logger });
}
```

Checks during load:
- Allowlist enforcement (`plugins.allow`)
- Per-plugin enable state (`entries.<id>.enabled`)
- Manifest validation (config schema)
- Provenance tracking (warns about untracked plugins)

## BOOT.md Mechanism

Key file: `src/gateway/boot.ts`

On gateway startup:
1. Looks for `BOOT.md` in agent workspace dir
2. If exists and non-empty, runs agent with boot instructions as prompt
3. Agent executes BOOT.md tasks (send messages, configure, run setup)
4. Session state restored after boot completes (snapshots + restores main session mapping)
5. Returns discriminated union:
   - `{ status: "skipped", reason: "missing" | "empty" }`
   - `{ status: "ran" }`
   - `{ status: "failed", reason: string }`

- Boot runs in a dedicated session (`boot-<timestamp>-<uuid>`)
- Agent runs with `senderIsOwner: true`, `deliver: false`
- BOOT.md prompt instructs agent to use message tool with `action=send` and `channel` + `target` fields
- Silent reply token used to suppress unnecessary output
- Error formatting uses `formatErrorMessage()` (unified error serializer)

Use BOOT.md for one-shot initialization tasks that need agent intelligence (not just config).

## Config Reload Startup Loop Prevention

Key files: `src/gateway/config-reload.ts`, `src/gateway/server.impl.ts`

At startup, the gateway may mutate its own config file (seeding control UI origins, generating auth tokens). The config file watcher (chokidar) would see this write and trigger a reload loop.

Prevention mechanism:
1. After startup writes, the gateway re-reads the config snapshot and captures `startupInternalWriteHash`
2. `startGatewayConfigReloader()` receives `initialInternalWriteHash` and stores it as `lastAppliedWriteHash`
3. On watcher trigger, if the snapshot hash matches `lastAppliedWriteHash`, the reload is silently skipped
4. After the first non-self write, `lastAppliedWriteHash` is cleared (set to `null`) so future external changes reload normally

In-process config writes use `subscribeToWrites` listener to bypass the file watcher entirely: the listener receives the runtime config and persisted hash directly, sets `pendingInProcessConfig`, and schedules an immediate reload with `scheduleAfter(0)`.

### Shared Gateway Session Generation

Key file: `src/gateway/server/ws-shared-generation.ts`

On config write or reload, the gateway computes a `sharedGatewaySessionGeneration` - a SHA-256 hash of the shared auth mode + secret. When the generation changes (e.g. token rotation), all WS clients authenticated via the old shared secret are disconnected with code 4001 ("gateway auth changed"). This ensures stale credentials cannot persist across hot-reload token rotations.

## HTTP Request Pipeline

Key file: `src/gateway/server-http.ts`

HTTP requests are processed through `runGatewayHttpRequestStages()`, a stage-based pipeline with per-stage try/catch error isolation.

```typescript
type GatewayHttpRequestStage = {
  name: string;
  run: () => Promise<boolean> | boolean;
};

async function runGatewayHttpRequestStages(
  stages: readonly GatewayHttpRequestStage[],
): Promise<boolean>;
```

Each stage returns `true` if it handled the request (short-circuits remaining stages) or `false` to pass through. If a stage throws, the error is logged and the stage is skipped - subsequent stages (control-ui, gateway probes, etc.) remain reachable. This prevents plugin-owned route/runtime code that fails to load (e.g. missing optional dependency) from producing cascade 500s across unrelated routes.

### Gateway Auth Bypass Paths

Previously hardcoded Mattermost slash callback paths. Now generalized: `resolvePluginGatewayAuthBypassPaths()` iterates all bundled channel plugins and collects paths from `plugin.gateway.resolveGatewayAuthBypassPaths({ cfg })`. This allows any channel plugin to declare paths that bypass gateway auth (e.g. webhook callback endpoints).

### Plugin Route Operator Scopes

After plugin route auth succeeds, `resolvePluginRouteRuntimeOperatorScopes()` extracts the authenticated request's operator scopes and passes them as `gatewayRequestOperatorScopes` to the plugin HTTP handler. This enables plugin routes to enforce fine-grained operator-scoped access control.

## Node Command Policy

Key file: `src/gateway/node-command-policy.ts`

Determines which commands a connected node is allowed to execute. Decoupled from pairing state - the allowlist is resolved purely from config and platform metadata.

### Platform Defaults

Commands are grouped by platform (`ios`, `android`, `macos`, `linux`, `windows`, `unknown`). Platform is resolved from `node.platform` via prefix matching, then `node.deviceFamily` via token matching.

| Platform | Defaults |
|----------|----------|
| `ios` | canvas, camera, location, device, contacts, calendar, reminders, photos, motion, `system.notify` |
| `android` | canvas, camera, location, notifications (incl. actions), `system.notify`, device (incl. permissions/health), contacts, calendar, callLog, reminders, photos, motion |
| `macos` | canvas, camera, location, device, contacts, calendar, reminders, photos, motion, `system.run`/`system.which`/`system.notify`/`browser.proxy` |
| `linux`/`windows` | `system.run`, `system.which`, `system.notify`, `browser.proxy` |
| `unknown` | canvas, camera, location, `system.notify` |

### Dangerous Commands (opt-in)

`DEFAULT_DANGEROUS_NODE_COMMANDS`: `camera.snap`, `camera.clip`, `screen.record`, `contacts.add`, `calendar.add`, `reminders.add`, `sms.send`, `sms.search`. Not in any platform default - must be explicitly added via `gateway.nodes.allowCommands`.

### Allowlist Resolution

```typescript
resolveNodeCommandAllowlist(cfg, node): Set<string>
// = PLATFORM_DEFAULTS[platform] + cfg.gateway.nodes.allowCommands - cfg.gateway.nodes.denyCommands
```

### Command Validation

`isNodeCommandAllowed()` checks: command non-empty, in allowlist, and declared by node (node must advertise supported commands at connect time). If the node declared no commands, all invocations are rejected.

## Node Pairing Reconciliation

Key file: `src/gateway/node-connect-reconcile.ts`

`reconcileNodePairingOnConnect()` runs when a node connects via WebSocket:

1. Resolves `nodeId` from device ID or client ID
2. Resolves command allowlist via `resolveNodeCommandAllowlist()` using platform/deviceFamily from connect params
3. Filters declared commands against allowlist via `normalizeDeclaredNodeCommands()`
4. For already-paired nodes: compares declared commands against previously approved commands. If new commands are declared that were not previously approved, a new pairing request is issued (command upgrade flow). The effective commands remain the previously approved set until the upgrade is approved
5. If node is not already paired, calls `requestPairing()` to create a pending pairing request
6. Returns `{ nodeId, effectiveCommands, pendingPairing? }`

## Node Catalog

Key file: `src/gateway/node-catalog.ts`

`createKnownNodeCatalog()` merges three sources into a unified node view:

- **Device pairing** - `PairedDevice` entries filtered to `role: "node"`
- **Node pairing** - `NodePairingPairedNode` approved entries with caps/commands/permissions
- **Live connections** - `NodeSession` currently connected nodes

Node IDs from all three sources are unioned. For each node, `buildEffectiveKnownNode()` resolves fields using live > nodePairing > devicePairing precedence. The `effective` field on each entry is a `NodeListNode` with merged `caps`, `commands` (sorted, deduplicated), version info, connection state, and pairing state.

`listKnownNodes()` sorts: connected first, then alphabetical by displayName/nodeId.

## Cron Busy-Wait Drift Fix

Key file: `src/cron/service/timer.ts`

Recurring main-session wake-now jobs (cron/every schedule, `sessionTarget: "main"`, `wakeMode: "now"`) previously blocked the cron lane in a busy-wait loop when the main session had requests in flight. This caused the cron job's measured duration to reflect queue wait time instead of actual bookkeeping.

Fix: when `heartbeatResult.status === "skipped"` with `reason: "requests-in-flight"` and the job is recurring (`schedule.kind !== "at"`), the cron lane now calls `requestHeartbeatNow()` (fire-and-forget nudge) and returns immediately with `{ status: "ok" }`. One-shot `at` jobs still busy-wait up to `wakeNowHeartbeatBusyMaxWaitMs` (default 2min) with 250ms retry delay before falling back to the nudge.

## Cron Legacy Delivery Migration (Doctor)

Key files: `src/commands/doctor-cron.ts`, `src/commands/doctor-cron-legacy-delivery.ts`, `src/commands/doctor-cron-store-migration.ts`

`maybeRepairLegacyCronStore()` detects and normalizes legacy cron job storage:

**Store migration** (`normalizeStoredCronJobs`):
- `jobId` -> `id`
- Bare string schedule -> `{ kind: "cron", expr }`
- `schedule.cron` -> `schedule.expr`
- Legacy payload kind normalization (`agentturn` -> `agentTurn`, `systemevent` -> `systemEvent`)
- Top-level payload/delivery fields inlined into nested `payload`/`delivery` objects
- Missing `state`, `enabled`, `wakeMode`, `name`, `sessionTarget` fields populated with defaults
- `delivery.mode: "deliver"` -> `"announce"`
- Legacy `payload.provider` -> `delivery.channel`

**Legacy delivery migration** (`normalizeLegacyDeliveryInput`):
- Extracts `deliver`, `bestEffortDeliver`, `channel`, `provider`, `to`, `threadId` from payload
- Builds or merges into structured `delivery` object with `mode: "announce"` or `"none"`
- Strips legacy fields from payload after migration

**Notify fallback** (`migrateLegacyNotifyFallback`):
- Jobs with `notify: true` and existing webhook delivery: just removes `notify` flag
- Jobs with `notify: true` but no delivery: migrates to `delivery.mode: "webhook"` using `cron.webhook` config
- Warns if `cron.webhook` is unset and migration cannot proceed automatically

## Bundled Plugin Runtime Deps Doctor Check

Key file: `src/commands/doctor-bundled-plugin-runtime-deps.ts`

`maybeRepairBundledPluginRuntimeDeps()` scans bundled plugins for missing native dependencies.

**Scan** (`scanBundledPluginRuntimeDeps`):
- Skips source checkout roots (`.git` + `src` + `extensions` present)
- Reads `package.json` from each plugin in `dist/extensions/`
- Collects `dependencies` + `optionalDependencies` across all plugins
- Detects missing deps (sentinel: `node_modules/<name>/package.json` absent) and version conflicts across plugins

**Repair**:
- Runs `npm install --omit=dev --no-save --package-lock=false --ignore-scripts --legacy-peer-deps` with pinned versions
- Strips npm global config env vars (`npm_config_global`, `npm_config_location`, `npm_config_prefix`) to prevent nested global install

**Conflict reporting**: if multiple plugins pin different versions of the same dep, warns with per-plugin version breakdown.

## Chat-Native Task Board

Key file: `src/commands/tasks.ts`

CLI subcommands for managing background tasks:

| Subcommand | Purpose |
|------------|---------|
| `tasks list` | List tasks with optional `--runtime` / `--status` filters. Shows task ID, kind, status, delivery, run, child session, summary. |
| `tasks show <id>` | Detailed view of a single task (lookup by ID or token prefix). All fields including timestamps, labels, errors, progress. |
| `tasks notify <id> <policy>` | Update a task's notify policy (`TaskNotifyPolicy`). |
| `tasks cancel <id>` | Cancel a running task. Calls `cancelTaskById()` which coordinates with the task runtime. |
| `tasks audit` | List audit findings with `--severity` / `--code` / `--limit` filters. Now unified across both Tasks and TaskFlows - each finding includes a `Scope` column (`Task` or `TaskFlow`). |
| `tasks maintenance` | Reconcile, stamp cleanup, prune. Dry-run by default; `--apply` to write changes. Reports audit health before/after. Now includes TaskFlow registry maintenance alongside task registry maintenance. |

All subcommands support `--json` for machine-readable output. Task lookup uses `reconcileTaskLookupToken()` for prefix matching on shortened IDs.

### TaskFlow Support

Tasks now integrate with `TaskFlowRecord` - multi-step task orchestration records with `flowId`, `status` (running/succeeded/failed/lost/blocked), revision tracking, and control mode. The `flows` CLI command (`src/commands/flows.ts`) provides list/show/cancel subcommands for TaskFlow records.

## LanceDB Runtime Bootstrap

Key file: `extensions/memory-lancedb/lancedb-runtime.ts`

The `memory-lancedb` plugin bootstraps `@lancedb/lancedb` on demand via `createLanceDbRuntimeLoader()`:

1. **Try bundled import** - direct `import("@lancedb/lancedb")` (works in dev / monorepo)
2. **Try cached runtime** - check `<stateDir>/plugin-runtimes/memory-lancedb/lancedb/` for previously installed runtime matching manifest
3. **Auto-install** - if both fail, run `npm install` into the runtime dir with the pinned version from the plugin's `package.json`

Key behaviors:
- Load promise is singleton-cached; reset on failure so retries are possible
- Manifest comparison gates cache validity (version bump triggers reinstall)
- `OPENCLAW_NIX_MODE=1` disables auto-install (fail-fast for Nix environments)
- Exposed via `loadLanceDbModule()` for use in the plugin index

## Skill Secrets Runtime Resolution

Key file: `src/agents/skills/runtime-config.ts`

Skill env overrides (`applySkillEnvOverrides`, `applySkillSnapshotEnvOverrides`) now prefer the runtime config snapshot over the static config:

```typescript
function resolveSkillRuntimeConfig(config?: OpenClawConfig): OpenClawConfig | undefined {
  return getRuntimeConfigSnapshot() ?? config;
}
```

This ensures SecretRef-resolved secrets (only available after secrets provider initializes at runtime) are used for skill env injection, rather than raw config values from disk.

## OpenAI Codex Proxy Bootstrap

Key file: `extensions/openai/openai-codex-provider.runtime.ts`

The Codex OAuth refresh wrapper now bootstraps the global undici proxy dispatcher before calling the upstream `getOAuthApiKey`. Without this, OAuth token refreshes behind an HTTP proxy would fail because the proxy dispatcher was only set up during initial agent run startup, not during deferred token refresh.

## Onboarding Flows

Key files: `src/commands/onboard.ts`, `src/commands/onboard-interactive.ts`, `src/commands/onboard-non-interactive.ts`

### CLI

```bash
openclaw onboard [OPTIONS]
```

### Options

| Flag | Purpose |
|------|---------|
| `--non-interactive` | No prompts (requires `--accept-risk`) |
| `--mode local\|remote` | Local vs remote execution |
| `--flow quick\|advanced\|manual` | Onboarding depth (`manual` mapped to `advanced`) |
| `--auth-choice token\|openai-codex\|...` | Auth method |
| `--reset` | Reset config/creds/sessions first |
| `--reset-scope config\|config+creds+sessions\|full` | What to reset (default: `config+creds+sessions`) |
| `--workspace PATH` | Custom agent workspace |
| `--secret-input-mode plaintext\|ref` | Secret storage mode |
| `--accept-risk` | Required for non-interactive |

### Auth Choice Changes

- Auth choices are now plugin-owned contract IDs. The `BuiltInAuthChoice` type no longer enumerates every provider - it is a small set of legacy aliases (`oauth`, `setup-token`, `token`) that are normalized elsewhere
- `claude-cli` deprecated: falls back to setup-token flow with warning
- `codex-cli` deprecated: falls back to OpenAI Codex OAuth with warning
- Non-interactive mode rejects deprecated auth choices with error
- `setup-token` and `token` now produce a unified error guiding users to the correct `--auth-choice`/`--token-provider` combination instead of silently failing
- `oauth` choice now produces an explicit error explaining it is no longer supported directly

### Onboard Talk Fallback Fix

- Fresh setup no longer persists talk fallback secrets into config
- Talk API key resolution (`applyTalkApiKey`) only injects env-resolved key when no provider-specific or legacy key is already configured

### Gateway Probe Modernization

`probeGatewayReachable()` now uses `probeGateway()` (a dedicated health probe utility) instead of `callGateway()` with `method: "health"`. Returns structured `{ ok, detail }` results.

### Control UI Link Resolution

`resolveControlUiLinks()` moved from `onboard-helpers.ts` to `src/gateway/control-ui-links.ts` as a shared utility.

### Interactive Flow

1. Prompts for channels, auth, models
2. Saves config progressively
3. Creates initial workspace/sessions

### Non-Interactive Local Flow

1. Accepts env vars or flags (no prompts)
2. Validates all inputs
3. Fast automated setup
4. Use for scripted/automated provisioning

### Non-Interactive Remote Flow

1. Remote SSH/gateway setup
2. For headless/container deployments

## Exec Approval Manager

Key file: `src/gateway/exec-approval-manager.ts`

In-memory manager for exec command approvals (used by Telegram exec approvals for OpenCode/Codex).

```typescript
class ExecApprovalManager {
  create(request, timeoutMs, id?): ExecApprovalRecord;
  register(record, timeoutMs): Promise<ExecApprovalDecision | null>;  // preferred over waitForDecision
  resolve(recordId, decision, resolvedBy?): boolean;
  expire(recordId, resolvedBy?): boolean;
  consumeAllowOnce(recordId): boolean;    // atomic one-time approval consumption
  awaitDecision(recordId): Promise<...>;  // DEPRECATED: use register() instead
  lookupPendingId(input): ExecApprovalIdLookupResult;  // exact, prefix, or ambiguous match
  getSnapshot(recordId): ExecApprovalSnapshot | undefined;  // read-only snapshot of current state
  listPendingRecords(): ExecApprovalRecord[];  // all records that have not been resolved yet
}
```

- `ExecApprovalDecision`: `"allow-once"`, `"allow-always"`, or `"deny"`
- Resolved entries kept for 15s grace period for late `awaitDecision` calls
- Supports caller metadata: `requestedByConnId`, `requestedByDeviceId`, `requestedByClientId`
- `lookupPendingId` supports prefix matching for shortened approval IDs (uses `normalizeLowercaseStringOrEmpty` for case-insensitive matching)
- After approval completion, the agent session is resumed via `sendExecApprovalFollowup()` which dispatches a `callGatewayTool("agent", ...)` with the original session key and a structured prompt containing the exec result. Denied commands get a distinct prompt instructing the agent not to retry.

### Approval Replay on Startup

Key files: `src/infra/exec-approval-channel-runtime.ts`, `src/gateway/server-methods/exec-approval.ts`

When a channel's approval runtime connects (or reconnects) to the gateway:
1. After the gateway client connects and the `ready` promise settles, the runtime calls `exec.approval.list` and `plugin.approval.list` to fetch all currently pending approval records
2. Each pending request is replayed through `handleRequested()` with `{ ignoreIfInactive: true }` so requests are only processed if the channel runtime is still active
3. Duplicate requests (same `id`) are deduplicated and silently ignored
4. This ensures approval requests created while a channel was disconnected are surfaced to the operator without waiting for the next incoming request

### Shared Approval Helpers

Key file: `src/gateway/server-methods/approval-shared.ts`

Common approval lifecycle helpers extracted from exec-approval and plugin-approval handlers:
- `isApprovalDecision()` - type guard for valid decision strings
- `resolvePendingApprovalRecord()` - lookup by exact ID or prefix with ambiguity detection
- `respondPendingApprovalLookupError()` - standardized error response for missing/ambiguous IDs
- `handlePendingApprovalRequest()` - unified request broadcast, delivery routing, and two-phase response flow
- `handleApprovalResolve()` - unified resolve with validation, broadcast, and forwarding

### New Gateway Methods

- `exec.approval.get` - fetch a single pending approval by ID (supports prefix matching). Returns command text, preview, allowed decisions, host, nodeId, agentId, expiry
- `exec.approval.list` - list all pending exec approval records
- `plugin.approval.list` - list all pending plugin approval records
- Reserved prefix: approval IDs starting with `plugin:` are rejected for exec approvals

### iOS Push Notification Delivery

Key file: `src/gateway/exec-approval-ios-push.ts`

New delivery channel for exec approval notifications to paired iOS devices via APNs (Apple Push Notification service):

- Targets paired devices with iOS platform, operator role, and `operator.approvals` scope
- Supports two transports: `direct` (APNs auth from env) and `relay` (via gateway relay config)
- On `handleRequested`: sends alert push to all qualifying devices with command preview and approval ID
- On `handleResolved`: sends silent wake push so the iOS app can update UI
- On `handleExpired`: sends silent wake push for cleanup
- Stale APNs registrations are auto-cleared based on APNs error responses
- Delivery state tracked per approval ID with pushed nodeIds

### Per-Decision Validation

`resolveExecApprovalAllowedDecisions()` computes the valid decision set for each approval request based on the effective policy. When a resolve request includes `allow-always` but the policy only permits `allow-once`, the resolve is rejected with `APPROVAL_ALLOW_ALWAYS_UNAVAILABLE`.

## Channel Health Monitor

Key files: `src/gateway/channel-health-monitor.ts`, `src/gateway/channel-health-policy.ts`

Periodically checks channel health and auto-restarts unhealthy channels.

### Health Evaluation Reasons

- `healthy` - normal operation
- `not-running` - channel stopped
- `disconnected` - running but not connected
- `stale-socket` - connected but no events within threshold (half-dead WebSocket)
- `stuck` - busy but no run activity within 25min
- `busy` - actively processing (healthy short-circuit)
- `startup-connect-grace` - within connect grace window after start
- `unmanaged` - disabled/unconfigured accounts

### Restart Reasons

`ChannelRestartReason` type:
- `gave-up` - reconnectAttempts >= 10
- `stopped` - channel stopped unexpectedly
- `stale-socket` - no events within threshold
- `stuck` - busy but no progress
- `disconnected` - running but not connected

### Timing Defaults (via `ChannelHealthTimingPolicy`)

- Check interval: 5 minutes (`channelHealthCheckMinutes` config)
- Monitor startup grace: 60s
- Channel connect grace: 120s (`DEFAULT_CHANNEL_CONNECT_GRACE_MS`)
- Stale event threshold: 30 minutes (`channelStaleEventThresholdMinutes` config)
- Cooldown: 2 cycles between restarts per channel
- Max restarts: 10/hour per channel (`channelMaxRestartsPerHour` config)

### Health Check Skips

- `isHealthMonitorEnabled` - checks if monitoring is enabled
- `isManuallyStopped` - skips monitoring for manually stopped channels

### Busy Lifecycle Guard

`busyStateInitializedForLifecycle` prevents stale busy fields from previous channel lifecycle causing false healthy short-circuit.

### Stale Socket Exclusions

- Previously hardcoded Telegram exclusion is now plugin-declared via `plugin.status.skipStaleSocketHealthCheck`
- Webhook-mode channels excluded from stale-socket checks
- Lifecycle-aware: ignores stale `lastEventAt` from previous lifecycle when channel restarted

### Channel Approval Handler Bootstrap

Key file: `src/infra/approval-handler-bootstrap.ts`

`startChannelApprovalHandlerBootstrap()` is called during channel account start. It initializes the channel's approval handler lifecycle (exec and plugin approvals). The approval runtime is scoped per channel account and disposed when the channel stops. This replaces per-channel inline approval wiring with a centralized assembly pattern.

### Restart Flow

stop -> `resetRestartAttempts` -> start -> record timestamp

## Node Pending Work

Key file: `src/gateway/server-methods/nodes-pending.ts`

Gateway methods for pending node work queue:

```typescript
"node.pending.drain"    // Drain pending work items for calling node (by device/client ID)
"node.pending.enqueue"  // Enqueue work for a specific node, with optional APNs wake + reconnect retry
```

- Enqueue supports priority and expiry
- Wake flow: APNs push -> wait for reconnect (two attempts with retry) -> nudge fallback
- Drain includes `defaultStatus` in response

## Gateway Auth

Key file: `src/gateway/auth.ts`

### Auth Modes

- `none` - no authentication
- `token` - shared secret token (config or `OPENCLAW_GATEWAY_TOKEN` env)
- `password` - shared password (config or `OPENCLAW_GATEWAY_PASSWORD` env)
- `trusted-proxy` - identity from reverse proxy headers (`trustedProxy.userHeader`)

### Auth Methods (result)

Auth resolution returns a method value: `none`, `token`, `password`, `trusted-proxy`, `device-token`, `bootstrap-token`.

### Auth Resolution

- Mode auto-detected: override > config > password presence > token presence > default (token)
- Tailscale: Whois-verified identity for WS Control UI surface only (not HTTP). Skipped when the client provides an explicit shared secret (token/password), preventing Tailscale header auth from shadowing valid credential-based connections
- Rate limiting: per-IP sliding window, lockout on exceeded threshold. Tailscale branch now uses `withSerializedRateLimitAttempt()` to serialize attempts for the same {scope, ip} key across pre-check and failure write, preventing race conditions
- Missing credentials (no token/password provided) do not burn rate-limit slots
- Lockout expired entries reset attempt counters correctly (prevents infinite escalation)
- `GatewaySecretRefUnavailableError` thrown when SecretRef is used but secrets provider not initialized
- `allowRealIpFallback`: trusts X-Real-IP only when explicitly enabled (default: false)

### Effective Shared Gateway Auth

`resolveEffectiveSharedGatewayAuth()` returns `{ mode, secret }` for token or password modes, `null` otherwise. Used by the shared session generation hash and config-write enforcement.

### Trusted Proxy Auth

- `requiredHeaders`: headers that must be present from the proxy
- `allowUsers`: optional allowlist of user identities

### Credential Resolution

Key files: `src/gateway/credentials.ts`, `src/gateway/credential-planner.ts`

- Local mode: `gateway.auth.token` with `gateway.remote.token` as fallback
- Remote mode: `gateway.remote.*` with env and local config fallback chain
- Precedence: gateway service context uses `config-first`, others use `env-first`
- Unresolved `${VAR}` placeholders in credential values are rejected (`trimCredentialToUndefined`)
- `CLAWDBOT_*` legacy env vars supported as fallback
- `GatewayCredentialPlan` type for structured credential resolution
- `resolveGatewayProbeCredentialsFromConfig` - probe-specific, remote-only fallback
- `resolveGatewayDriftCheckCredentialsFromConfig` - drift check, local mode, config-first, empty env

### Startup Auth Preflight

During `prepareGatewayStartupConfig()`, if the secrets runtime snapshot resolves a plaintext `gateway.auth.token` or `gateway.auth.password`, those values are merged into the auth override before `ensureGatewayStartupAuth()`. This ensures SecretRef-resolved credentials are available for startup auth bootstrap.

## Container Bind Detection

Key file: `src/gateway/net.ts`

`isContainerEnvironment()` detects Docker, Podman, and Kubernetes containers using:
1. Sentinel files: `/.dockerenv`, `/run/.containerenv`, `/var/run/.containerenv`
2. cgroup markers in `/proc/1/cgroup`: docker, containerd, kubepods, lxc patterns

Result is cached per-process. Used by:
- `resolveGatewayBindHost()`: in `auto` mode, containers default to `0.0.0.0` instead of loopback (makes port-forwarding work)
- `defaultGatewayBindMode()`: returns `"auto"` inside containers, `"loopback"` on bare-metal/VMs. Tailscale serve/funnel always returns `"loopback"` regardless of container detection

### Localhost Canonicalization

`parseHostForAddressChecks()` now strips trailing dots from the normalized host before comparison (e.g. `localhost.` -> `localhost`).

## SSRF Guard and Proxy Configuration

Key file: `src/infra/net/fetch-guard.ts`

The guarded fetch pipeline (`guardedFetch`) validates URLs against SSRF policies before dispatching.

### Operator-Configured Proxy Fix

When `dispatcherPolicy.allowPrivateProxy === true`, the explicit proxy hostname is operator-configured and trusted. The SSRF guard previously checked the proxy hostname against the target-scoped `hostnameAllowlist` (e.g. `["api.telegram.org"]`), which rejected `localhost` and other local proxy hostnames. Fix: `assertExplicitProxyAllowed()` clears the `hostnameAllowlist` for the proxy hostname check while keeping private-network IP validation in place via `allowPrivateNetwork`. This restores Telegram media downloads (and any channel using a local proxy) after the url-fetch security hardening in 2026.4.x.

### Cross-Origin Redirect Replay

New option `allowCrossOriginUnsafeRedirectReplay`: when true, allows replaying unsafe request methods and bodies across cross-origin redirects. Sensitive cross-origin headers (Authorization/Cookie) are still stripped.

### Non-Pinned DNS Dispatchers

`createPolicyDispatcherWithoutPinnedDns()` creates HTTP/1.1 dispatchers (direct, env-proxy, or explicit-proxy) without DNS pinning. Used when the caller opts out of DNS-pinned security for trusted operator-controlled URLs.

## MCP Loopback Server

Key files: `src/gateway/mcp-http.ts`, `src/gateway/mcp-http.*.ts`

JSON-RPC 2.0 HTTP server for MCP (Model Context Protocol) tool bridging. Starts on a random loopback port during gateway startup.

- **Auth**: bearer token generated at startup, validated per-request
- **Protocol**: POST to `/mcp` with JSON-RPC body (single or batch requests)
- **Tool cache**: `McpLoopbackToolCache` caches resolved tool sets per session/provider/account scope
- **Request context**: session key and provider resolved from request headers
- **Handlers**: `handleMcpJsonRpc()` dispatches to MCP-spec method handlers (tools/list, tools/call, etc.)
- **Transport helpers**: split into `mcp-http.loopback-runtime.ts` (activation state), `mcp-http.request.ts` (validation/parsing), `mcp-http.runtime.ts` (tool resolution cache), `mcp-http.schema.ts` (response schema)

## Session Compaction Checkpoints

Key file: `src/gateway/session-compaction-checkpoints.ts`

Session compaction now saves checkpoints (pre-compaction snapshots) that can be restored:
- Max 25 checkpoints per session (`MAX_COMPACTION_CHECKPOINTS_PER_SESSION`)
- Checkpoint reasons: `manual`, `timeout-retry`, `budget`, `overflow`
- Gateway methods: `sessions.compaction.list`, `sessions.compaction.get`, `sessions.compaction.restore`, `sessions.compaction.branch`
- Each checkpoint captures `sessionId`, `sessionFile`, `leafId`
- Restore clones the checkpoint's session entry with a new session ID and file

## AgentBox Provisioning Flow

How agentbox provisions openclaw instances (reference pattern for automated provisioning):

```
1. VM created (Hetzner) with cloud-init
2. Boot script runs (agentbox-init.sh):
   a. Wait for user systemd readiness
   b. Fetch config from backend API: GET /instances/config?serverId=X&secret=Y
   c. Configure reverse proxy (Caddy HTTPS)
   d. Write ~/.openclaw/openclaw.json (merged config via jq)
   e. Generate wallet (openclaw-x402 CLI)
   f. Set gateway token in systemd drop-in
   g. Start gateway service (systemd user unit)
   h. Wait for health check (up to 120s, gateway cold start ~72s)
3. VM calls back to backend with wallet + gatewayToken
4. Backend funds wallet + mints SATI NFT
5. Instance state: provisioning -> minting -> running
```

### Config Merge Pattern (jq)

```bash
# Fetch base config from API
curl -s "$BACKEND_URL/instances/config?serverId=$SERVER_ID&[REDACTED_SECRET]" | \
  jq --arg rpc "$RPC_URL" \
     --arg token "$TELEGRAM_TOKEN" \
     '.plugins.entries["openclaw-x402"].config.rpcUrl = $rpc |
      if $token != "" then .channels.telegram.[REDACTED_SECRET] else . end' \
  > ~/.openclaw/openclaw.json
```

### Gateway Auth

- Token-based: `OPENCLAW_GATEWAY_TOKEN` env var
- Systemd drop-in: `~/.config/systemd/user/openclaw-gateway.service.d/token.conf`
- Config: `gateway.auth.token` and `gateway.remote.token` must match
- API access: `Bearer <gatewayToken>` for `/v1/chat/completions`

## Health Check

Key file: `src/gateway/server/health-state.ts`

After starting gateway, verify:
```bash
# Check port
ss -ltnp | grep 18789

# Check gateway health
curl -s http://localhost:18789/health

# Check channel status
openclaw channels status --probe
```

### Gateway Snapshot Privacy

`buildGatewaySnapshot()` now accepts `{ includeSensitive?: boolean }`. When false (default for unauthenticated callers), `configPath`, `stateDir`, and `authMode` are omitted from the response. Authenticated admin callers still receive the full snapshot.

### Readiness Check

`/ready` and `/readyz` endpoints return detailed readiness state (channel health, startup grace, etc.) only when the request is authenticated or from a local direct connection. Unauthenticated external requests receive only `{ ready: boolean }`.

### Graceful WebSocket Shutdown

`createGatewayCloseHandler()` now implements a timed shutdown for WebSocket connections:
1. Call `wss.close()` and wait up to 1s (`WEBSOCKET_CLOSE_GRACE_MS`)
2. If not closed within grace period, terminate all tracked clients
3. Wait an additional 250ms (`WEBSOCKET_CLOSE_FORCE_CONTINUE_MS`) then continue shutdown regardless

## Key Provisioning Gotchas

1. **Gateway cold start is slow** (~72s on small VMs) - plan for timeout
2. **Plugin fetch interceptor needs gateway token** - token must be in both systemd env AND config file
3. **Skills auto-discovered** from `~/.openclaw/skills/` - no explicit config needed
4. **Config strictly validated** - malformed JSON breaks startup entirely
5. **Channels require explicit config** - Telegram is opt-in, not auto-discovered
6. **`models.mode: "replace"`** hides all 700+ builtins - only providers in config appear
7. **Plugin `registerProvider()`** is auth-only, does NOT populate model catalog - metadata must be defined in config
8. **Bootstrap secrets** (gateway.auth.password) must be plaintext strings or env vars - secrets provider system is not initialized at gateway startup
9. **Exec child commands** marked with `OPENCLAW_CLI` env var
10. **Memory flush** protects bootstrap files during flush operations
11. **One-shot mode** tears down cached memory managers to prevent exit hangs
12. **Default reasoning config** now defaults to off (PR #50405) - agents expecting reasoning must enable it explicitly
13. **LanceDB runtime auto-installs** on packaged/global installs where bundled import fails - requires `npm` on PATH; disable with `OPENCLAW_NIX_MODE=1`
14. **Skill SecretRef secrets** only available after secrets provider initializes - skill env overrides now prefer `getRuntimeConfigSnapshot()` over static config so resolved secrets propagate
15. **Codex OAuth behind proxy** - proxy dispatcher must be bootstrapped before OAuth refresh; now handled automatically by the `getOAuthApiKey` wrapper
16. **Startup config reload loop** - gateway records `startupInternalWriteHash` after self-mutations (token generation, origin seeding); config reloader skips watcher triggers matching this hash
17. **HTTP stage cascade 500s** - `runGatewayHttpRequestStages()` isolates each stage in try/catch; a broken plugin facade or missing optional dep no longer takes down unrelated HTTP routes
18. **Node command policy is config-only** - allowlist resolution is decoupled from pairing state; determined by platform defaults + `gateway.nodes.allowCommands` - `gateway.nodes.denyCommands`
19. **Cron recurring wake-now drift** - recurring main-session cron jobs no longer busy-wait when the main lane is busy; they fire a `requestHeartbeatNow` nudge and release the cron lane immediately
20. **Bundled plugin runtime deps** - packaged installs may be missing native deps from bundled plugins; `openclaw doctor --fix` runs `npm install` with pinned versions into the package root
21. **Container auto-bind** - inside Docker/Podman/K8s containers, the gateway auto-binds to `0.0.0.0` instead of loopback so port-forwarding works. `isContainerEnvironment()` detects via sentinel files and cgroup markers
22. **SSRF guard vs operator proxy** - when `allowPrivateProxy: true`, the SSRF guard clears the target-scoped hostname allowlist for the proxy hostname check so `localhost` proxies are not rejected. Private-network IP checks still apply
23. **Approval replay on startup** - channel approval runtimes replay `exec.approval.list` and `plugin.approval.list` on connect to surface approvals created while the channel was offline
24. **Shared auth session generation** - token/password rotation triggers a SHA-256 generation hash change that disconnects all stale shared-auth WS clients (code 4001)
25. **Node command upgrade** - when a reconnecting paired node declares new commands not in its previously approved set, a new pairing request is issued and the effective commands remain the old approved set until the upgrade is approved
26. **iOS exec approval push** - paired iOS devices with `operator.approvals` scope receive APNs push notifications for pending exec approvals; supports direct APNs auth and relay transport
27. **Plugin gateway auth bypass** - channel plugins can now declare custom auth-bypass paths via `plugin.gateway.resolveGatewayAuthBypassPaths()` instead of hardcoded path lists
28. **Stale socket health check is plugin-declared** - channels opt out of stale-socket health checks via `plugin.status.skipStaleSocketHealthCheck` instead of hardcoded channel ID checks
29. **MCP loopback server** - gateway starts a JSON-RPC MCP server on a random loopback port for tool bridging (e.g. Claude CLI MCP). Bearer-token authenticated; tool results cached per session scope
30. **Session compaction checkpoints** - compactions now save restore points (max 25 per session) that can be listed, inspected, restored, or branched via gateway methods
31. **Startup secrets pruning** - when `OPENCLAW_SKIP_CHANNELS=1`, channel secret surfaces are pruned from the config before secrets runtime preparation, avoiding unnecessary secret resolution for skipped channels
32. **Subagent orphan recovery** - runs on every startup cycle (not just cold restore) to handle orphaned subagent entries left by SIGUSR1 in-process restarts
33. **Restart sentinel threading** - post-restart notification delivery now uses the channel plugin's `resolveReplyTransport()` to determine thread routing, replacing the hardcoded Slack `replyToId` mapping


path: references/channels-extensions.md
content: # OpenClaw Channels & Extensions

## Built-in Channels

| Channel | Code Path | Config Key | Notes |
|---------|-----------|------------|-------|
| Telegram | `extensions/telegram/src/` | `channels.telegram` | Long polling or webhook, bot token required |
| Discord | `extensions/discord/src/` | `channels.discord` | Bot token, guild-based |
| Slack | `extensions/slack/src/` | `channels.slack` | App token + bot token |
| Signal | `extensions/signal/src/` | `channels.signal` | signal-cli based, phone number required |
| iMessage | `extensions/imessage/src/` | `channels.imessage` | macOS only, BlueBubbles or native |
| WhatsApp | `extensions/whatsapp/` | `channels.whatsapp` | Baileys library for WhatsApp Web protocol |
| Web UI | `src/channels/web/` | `channels.web` | Built-in web chat interface |

## Extension Channels

| Extension | Path | Config Key | Notes |
|-----------|------|------------|-------|
| MS Teams | `extensions/msteams/` | via plugin config | Microsoft Graph API |
| Matrix | `extensions/matrix/` | via plugin config | matrix-js-sdk, thread binding commands |
| Zalo | `extensions/zalo/` | via plugin config | Zalo Official Account API |
| Zalo User | `extensions/zalouser/` | via plugin config | Zalo personal account |
| Voice Call | `extensions/voice-call/` | via plugin config | Twilio/SIP voice |
| Feishu/Lark | `extensions/feishu/` | via plugin config | Feishu bot API |
| BlueBubbles | `extensions/bluebubbles/` | via plugin config | iMessage via BlueBubbles server |
| Mattermost | `extensions/mattermost/` | via plugin config | Mattermost WebSocket + slash HTTP |
| WhatsApp | `extensions/whatsapp/` | via plugin config | WhatsApp via Baileys, npm-publishable |
| ACPX | `extensions/acpx/` | via plugin config | Embedded ACP runtime backend |
| QQ Bot | `extensions/qqbot/` | via plugin config | QQ Bot API |
| SearXNG | `extensions/searxng/` | via plugin config | Self-hosted meta-search web search provider |
| Nostr | `extensions/nostr/` | via plugin config | NIP-04 encrypted DMs via nostr-tools |
| Google Chat | `extensions/googlechat/` | via plugin config | Google Chat via google-auth-library |
| LINE | `extensions/line/` | via plugin config | LINE Messaging API |
| IRC | `extensions/irc/` | via plugin config | IRC protocol |
| Twitch | `extensions/twitch/` | via plugin config | Twitch chat via Twurple |
| Nextcloud Talk | `extensions/nextcloud-talk/` | via plugin config | Nextcloud Talk API |
| Synology Chat | `extensions/synology-chat/` | via plugin config | Synology Chat webhook/bot |
| Tlon | `extensions/tlon/` | via plugin config | Tlon/Urbit Landscape messaging |
| QA Channel | `extensions/qa-channel/` | via plugin config | Internal QA testing channel with bus-based transport (NEW) |
| Webhooks | `extensions/webhooks/` | via plugin config | Authenticated inbound webhooks binding external automation to TaskFlows (NEW) |

## Extension Plugins (Non-Channel)

| Extension | Path | Type | Notes |
|-----------|------|------|-------|
| SearXNG | `extensions/searxng/` | Web search provider | Registers via `api.registerWebSearchProvider()`; self-hosted SearXNG meta-search, no API key required; configurable categories and language |
| Memory Wiki | `extensions/memory-wiki/` | Knowledge vault | Persistent wiki compiler and Obsidian-friendly knowledge vault; supports vault modes (isolated, bridge, unsafe-local), belief-layer digests, and Obsidian CLI integration (NEW) |
| Alibaba | `extensions/alibaba/` | AI provider | Video generation provider via Alibaba Model Studio / DashScope API (NEW) |
| Amazon Bedrock Mantle | `extensions/amazon-bedrock-mantle/` | AI provider | Bedrock model discovery layer with sync runtime registration (NEW) |
| Arcee | `extensions/arcee/` | AI provider | Arcee AI provider via direct API or OpenRouter (NEW) |
| Comfy | `extensions/comfy/` | AI provider | ComfyUI image/music/video generation; local or cloud mode with workflow path config (NEW) |
| Fireworks | `extensions/fireworks/` | AI provider | Fireworks AI provider (NEW) |
| Qwen | `extensions/qwen/` | AI provider | Qwen Cloud provider with media understanding + video generation; China and Global endpoints (NEW) |
| Runway | `extensions/runway/` | AI provider | Runway video generation provider (NEW) |
| StepFun | `extensions/stepfun/` | AI provider | StepFun provider with China and Global endpoints, Step Plan support (NEW) |
| Vydra | `extensions/vydra/` | AI provider | Vydra speech, image, and video generation provider (NEW) |
| QA Lab | `extensions/qa-lab/` | Internal testing | Internal QA test harness (NEW) |

## Channel Plugin Registration

```typescript
// In plugin's register() function:
api.registerChannel({
  id: "my-channel",
  protocol: "my-protocol",
  plugin: {
    start: async (runtime) => { ... },
    stop: async () => { ... },
    sendMessage: async (msg) => { ... },
    // ... event handlers
  }
});
```

## Channel Plugin Type Changes

The `ChannelPlugin` type gained several new adapter slots:

- `approvalCapability?: ChannelApprovalCapability` - replaces the old `approvals?: ChannelApprovalAdapter` field; unifies auth + delivery + render + native approval surfaces into a single capability object
- `secrets?: ChannelSecretsAdapter` - channel-specific secret target registry entries and runtime config assignments
- `doctor?: ChannelDoctorAdapter` - channel-specific doctor diagnostic contracts
- `setupWizard` now accepts both `ChannelSetupWizard` and `ChannelSetupWizardAdapter`

New adapter types added:
- `ChannelSecretsAdapter` - secret target registry, unsupported SecretRef surface patterns, runtime config assignments
- `ChannelExposure` - fine-grained visibility control: `configured`, `setup`, `docs` booleans per channel (`src/channels/plugins/exposure.ts`)
- `ChannelLegacyStateMigrationPlan` - structured `copy`/`move` migration plans with source/target paths
- `ChannelConfigAdapter` gained `hasConfiguredState` and `hasPersistedAuthState` probes
- `ChannelSetupAdapter` gained `singleAccountKeysToMove`, `namedAccountPromotionKeys`, and `resolveSingleAccountPromotionTarget`

Thread and messaging adapter additions:
- `replyToMode` gained a new `"batched"` option alongside `"off"`, `"first"`, `"all"`
- `ChannelOutboundAdapter` gained `sanitizeText`, `supportsPollDurationSeconds`, `supportsAnonymousPolls`
- `ChannelMessagingAdapter` gained `transformReplyPayload`, `resolveLegacyGroupSessionKey`, `canonicalizeLegacySessionKey`, `deriveLegacySessionChatType`
- `ChannelAgentPromptAdapter` gained `inboundFormattingHints` returning `{ text_markup, rules }`
- `ChannelMessageActionAdapter` gained `resolveCliActionRequest` and `messageActionTargetAliases`

Key files: `src/channels/plugins/types.core.ts`, `src/channels/plugins/types.adapters.ts`, `src/channels/plugins/types.plugin.ts`

## Thread Bindings (Telegram, Discord & Matrix)

Telegram, Discord, and Matrix support thread-bound sessions via `threadBindings` config:

```json
{
  "threadBindings": {
    "enabled": true,
    "idleHours": 24,
    "maxAgeHours": 0,
    "spawnSubagentSessions": false,
    "spawnAcpSessions": false
  }
}
```

- Per-account binding manager with persistent state
- Supports idle timeout and hard max age expiration
- Auto-sweep mechanism cleans expired bindings
- `spawnSubagentSessions`: auto-create + bind threads for `sessions_spawn({ thread: true })`
- `spawnAcpSessions`: auto-create + bind threads for `/acp spawn`

Thread binding spawn policy is now **plugin-driven** instead of hardcoded per channel. `resolveDefaultTopLevelPlacement()` reads `conversationBindings.defaultTopLevelPlacement` from the channel plugin; channels reporting `"child"` (Discord, Matrix) get thread-based spawning by default. The old hardcoded `DISCORD_THREAD_BINDING_CHANNEL`/`MATRIX_THREAD_BINDING_CHANNEL` constants are removed.

Error messages are now generic and include the channel name dynamically: `"Thread bindings are disabled for ${channel} (set channels.${channel}.threadBindings.enabled=true ...)"` instead of channel-specific strings.

Matrix thread binding commands: `/acp spawn`, `/session spawn`, `/focus`, `/unfocus` - wired through `bindings.compileConfiguredBinding`/`matchInboundConversation`.

Key files: `src/channels/thread-bindings-policy.ts`, `extensions/telegram/src/thread-bindings.ts`, Discord thread binding via plugin SDK

## Centralized Inbound Mention Policy

Group mention gating was centralized across all channels into a shared `resolveInboundMentionDecision({ facts, policy })` API (`src/channels/mention-gating.ts`). Previously each channel implemented its own mention bypass logic.

Key types:
- `InboundMentionFacts` - `canDetectMention`, `wasMentioned`, `hasAnyMention`, `implicitMentionKinds`
- `InboundMentionPolicy` - `isGroup`, `requireMention`, `allowedImplicitMentionKinds`, `allowTextCommands`, `hasControlCommand`, `commandAuthorized`
- `InboundImplicitMentionKind` - `"reply_to_bot"`, `"quoted_bot"`, `"bot_thread_participant"`, `"native"`
- `InboundMentionDecision` - `effectiveWasMentioned`, `shouldSkip`, `implicitMention`, `matchedImplicitMentionKinds`, `shouldBypassMention`

Channels updated to use the shared API: Discord, Slack, Telegram, Signal, iMessage, WhatsApp, Google Chat, MS Teams, LINE, Nextcloud Talk, Zalo User. The legacy flat-params overload is deprecated but still accepted.

## Telegram Channel Config

```json
{
  "channels": {
    "telegram": {
      "token": "123456:ABC-DEF...",
      "allowlist": ["username1", "username2"],
      "mode": "longpoll",
      "commands": { "enabled": true },
      "customCommands": [{ "name": "my_cmd", "description": "..." }],
      "threadBindings": { "enabled": true },
      "dmHistoryLimit": 50,
      "network": { "autoSelectFamily": true }
    }
  }
}
```

- **Token**: raw bot token (no `TELEGRAM_BOT_TOKEN=` prefix)
- **Allowlist**: recommended for single-user DM policy
- **Mode**: `longpoll` (default, more reliable) or `webhook`
- **Custom commands**: names must match `[a-z0-9_]{1,32}` - NO hyphens
- **Streaming modes**: `"off"`, `"partial"`, `"block"`, `"progress"`
- **Direct DMs**: `direct` config keyed by chat ID for per-user settings
- **Topics**: support `agentId` for topic-specific agent routing
- **Network fallback**: resolver-scoped dispatchers with IPv4 fallback retry on `ETIMEDOUT`/`ENETUNREACH`/`UND_ERR_CONNECT_TIMEOUT` (`extensions/telegram/src/fetch.ts`)
- **Duplicate prevention**: deduplicates messages when preview edit times out before delivery confirmation
- **Exec approvals**: per-account `execApprovals` config with `approvers` list, `target` (`"dm"`, `"channel"`, `"both"`), and inline approval buttons for OpenCode/Codex flows (`extensions/telegram/src/exec-approvals.ts`). Now uses shared `createChannelExecApprovalProfile()` from plugin-sdk for client enablement, approver checks, request filtering, and local prompt suppression
- **Direct delivery hooks**: bridges direct delivery to internal `message:sent` hooks (`extensions/telegram/src/bot/delivery.replies.ts`); media loader now injected via plugin-sdk path
- **Safe send retry**: `isSafeToRetrySendError()` only retries pre-connect errors (`ECONNREFUSED`, `ENOTFOUND`, `EAI_AGAIN`, `ENETUNREACH`, `EHOSTUNREACH`) to prevent duplicate messages; post-connect errors like `ECONNRESET`/`ETIMEDOUT` are not retried for non-idempotent sends (`extensions/telegram/src/network-errors.ts`)
- **Topic delivery routing preserved**: DM topic `threadId` propagated correctly in announce/delivery contexts so replies land in the correct topic thread
- **Media retry with file-too-big guard**: `resolveTelegramFileWithRetry()` retries `getFile` up to 3 attempts with 1-4s jitter delay; permanently skips Telegram Bot API >20MB `file is too big` errors instead of retrying (`extensions/telegram/src/bot/delivery.resolve-media.ts`)
- **Lazy send runtime loading**: Telegram send runtime is now lazily loaded from entrypoints instead of eagerly imported, reducing startup cost (NEW)

## Discord Channel Config

```json
{
  "channels": {
    "discord": {
      "token": "bot-token-here",
      "guildId": "optional-specific-guild",
      "threadBindings": { "enabled": true },
      "slashCommand": { "ephemeral": true },
      "autoPresence": true,
      "autoArchiveDuration": 1440,
      "intents": { "presence": false, "guildMembers": false }
    }
  }
}
```

- **Token**: raw Discord bot token
- **Inbound worker**: keyed async queue for ordered message processing per session (30min timeout)
- **Event queue**: configurable `listenerTimeout`, `maxQueueSize`, `maxConcurrency`
- **Streaming modes**: `"off"`, `"partial"`, `"block"`, `"progress"`
- **Reaction notification**: `"off"`, `"own"`, `"all"`, `"allowlist"`
- **maxLinesPerMessage**: effective value applied in live replies; resolved per-account with root/account config merge (`extensions/discord/src/accounts.ts`)
- **Account helper cycle broken**: account resolution and inspect now import from `extensions/discord/src/runtime-api.ts` cleanly
- **Strict DM allowlist auth**: enforced for DM component interactions
- **Native command auth replies**: privileged native slash commands (e.g. `/config show`, `/plugins list`) now return explicit "You are not authorized" reply on auth failure instead of falling through to Discord's generic empty-interaction fallback; gated on `CommandSource === "native"` in `rejectUnauthorizedCommand()`/`rejectNonOwnerCommand()` gates
- **Bounded inbound media downloads**: attachment and sticker fetches use [REDACTED_SECRET] (60s idle) and [REDACTED_SECRET] (120s total) timeouts per individual download; the inbound worker abort signal remains the outer bound for the full message (`extensions/discord/src/monitor/timeouts.ts`)
- **Cover image support for event create**: `guild-admin` action handler now accepts an `image` param for event cover art; trusted media roots passed to loader (PR #60883) (NEW)
- **DM ACP binding identity stabilized**: fixes identity resolution for ACP bindings in Discord DM contexts (NEW)

## Discord Architecture

Inbound event processing system:
- `extensions/discord/src/monitor/inbound-worker.ts` - keyed async queue per session
- `extensions/discord/src/monitor/inbound-job.ts` - job serialization and execution
- `extensions/discord/src/monitor/timeouts.ts` - timeout normalization and abort signal management
- `extensions/discord/src/monitor/message-handler.ts` - debounce + worker integration

Voice subsystem additions:
- `extensions/discord/src/voice/capture-state.ts` - per-speaker voice capture state with generation tracking (NEW)
- `extensions/discord/src/voice/receive-recovery.ts` - DAVE encryption failure recovery with `DecryptionFailed` pattern detection, passthrough fallback (initial 30s / rearm 15s expiry), and reconnect threshold (3 failures in 30s window) (NEW)
- `extensions/discord/src/voice/sanitize.ts` - voice input text sanitization (NEW)

Approval handler runtime:
- `extensions/discord/src/approval-handler.runtime.ts` - centralized Discord-specific approval rendering and delivery (626 lines); implements `ChannelApprovalNativeRuntimeAdapter` with pending/resolved/expired view rendering via Carbon UI components; replaces bulk of old `exec-approvals.ts` logic (NEW)

## Approval Architecture (Plugin-SDK Split)

The approval system underwent a major structural split in the plugin-sdk. The old monolithic `approval-runtime` module was decomposed into purpose-specific modules:

| Module | Runtime export | Purpose |
|--------|---------------|---------|
| `approval-auth-helpers.ts` | `approval-auth-runtime` | Approver resolution, sender auth |
| `approval-client-helpers.ts` | `approval-client-runtime` | Client enablement, target recipient matching, profile factory, request filter matching, local prompt suppression |
| `approval-delivery-helpers.ts` | `approval-delivery-runtime` | Delivery suppression, DM route detection, native delivery mode resolution |
| `approval-native-helpers.ts` | `approval-native-runtime` | Origin target resolution, approver DM target resolution, session-conversation binding |
| `approval-handler-runtime.ts` | `approval-handler-runtime` | Native runtime adapter factory (`createChannelApprovalNativeRuntimeAdapter`) |
| `approval-handler-adapter-runtime.ts` | `approval-handler-adapter-runtime` | Lazy adapter factory (`createLazyChannelApprovalNativeRuntimeAdapter`) |
| `approval-gateway-runtime.ts` | `approval-gateway-runtime` | Gateway-mediated approval resolution |
| `approval-reply-runtime.ts` | `approval-reply-runtime` | Reply metadata and payload helpers |
| `approval-renderers.ts` | - | Shared approval UI renderers |

Key shared factories:
- `createApproverRestrictedNativeApprovalCapability()` - builds a full `ChannelApprovalCapability` from per-channel adapter params (approver list, delivery mode, auth checks, origin/DM target resolvers)
- `splitChannelApprovalCapability()` - splits a capability into `{ auth, delivery, nativeRuntime, render, native, describeExecApprovalSetup }` facets
- `createChannelExecApprovalProfile()` - builds a reusable profile with `isClientEnabled`, `isApprover`, `isAuthorizedSender`, `resolveTarget`, `shouldHandleRequest`, `shouldSuppressLocalPrompt`
- `createChannelNativeOriginTargetResolver()` - generic origin target resolver using turn-source + session-target + fallback chain
- `createChannelApproverDmTargetResolver()` - generic approver DM target resolver

The `ChannelPlugin.approvals` field is replaced by `ChannelPlugin.approvalCapability` which merges auth (actor action authorization, availability state) with delivery/native/render surfaces.

**Centralized native approval lifecycle** (PR #62135): approval handler bootstrap, runtime, route coordination, and view models extracted to `src/infra/approval-handler-bootstrap.ts`, `src/infra/approval-handler-runtime.ts`, `src/infra/approval-native-route-coordinator.ts`, and `src/infra/approval-view-model.ts`. Discord's `exec-approvals.ts` reduced from ~800 to ~50 lines; rendering moved to `approval-handler.runtime.ts`.

Per-channel impact:
- **Discord**: approval rendering and delivery moved to `approval-handler.runtime.ts`; `exec-approvals.ts` now thin re-export + button interaction handler using `resolveApprovalOverGateway()`
- **Slack**: `shouldHandleSlackExecApprovalRequest()` and inline auth replaced with `createChannelExecApprovalProfile()`; imports from split modules
- **Telegram**: exec approval config now gates on `account.enabled && tokenSource !== "none"`; uses shared `createChannelExecApprovalProfile()` and `isChannelExecApprovalTargetRecipient()`

## Matrix Extension

Capabilities:

- **Thread binding commands**: `/acp spawn`, `/session spawn`, `/focus`, `/unfocus` wired through binding compilation
- **Persistent sync state**: `FileBackedMatrixSyncStore` with debounced writes and `cleanShutdown` tracking
- **Startup migration**: legacy Matrix state migration wired into `openclaw doctor` and gateway startup; doctor migration previews restored
- **Poll vote alias**: `messageId` accepted as alias for `pollId` parameter in poll votes
- **Onboarding**: runtime-safe status checks
- **Thread-isolated sessions**: per-chat-type `threadReplies` config for thread-based session isolation
- **Group chat history context**: agent triggers include room history context
- **Draft streaming**: edit-in-place partial replies via message edits
- **Explicit proxy config**: `channels.matrix.proxy` for HTTP proxy support
- **Sender allowlist filtering**: fetched room context filtered by sender allowlist

## Slack Channel

- **Delivery-mirror guard**: embedded Pi session subscriber now filters `provider: "openclaw"` + `model: "delivery-mirror"` synthetic transcript entries via `isDeliveryMirrorAssistantMessage()`, preventing duplicate re-delivery to Slack ~3.6s after original
- **Chunk limit raised**: `SLACK_TEXT_LIMIT` raised from 4000 to 8000 (`extensions/slack/src/limits.ts`); passed as `fallbackLimit` to `resolveTextChunkLimit()`; config override via `textChunkLimit` still works
- **Thread explicit mention config**: `channels.slack.thread.requireExplicitMention` (default: `false`) - when `true`, suppresses implicit thread-participant mention detection so the bot only responds in threads when explicitly @mentioned (PR #58276) (NEW)
- **Guarded media transport**: fixes dispatcher leak to `globalThis.fetch` that caused media download failures in Slack (PR #62239) (NEW)

## Diagnostics

- **Cache-trace credential redaction**: credentials are now redacted from cache-trace diagnostic output to prevent accidental token exposure in diagnostic dumps

## Account Inspection & Credential Status

Pattern for safe credential projection without exposing tokens:

```typescript
type CredentialStatus = "available" | "configured_unavailable" | "missing";

// Used across Discord, Telegram, Slack for UI/status renderers
type InspectedAccount = {
  name: string;
  tokenStatus: CredentialStatus;
  // channel-specific fields...
};
```

Key files:
- `extensions/discord/src/account-inspect.ts`
- `extensions/telegram/src/account-inspect.ts`
- `extensions/slack/src/account-inspect.ts`
- `src/channels/account-snapshot-fields.ts` - shared utilities for credential status projection (supports `tokenStatus`, `botTokenStatus`, `appTokenStatus`, `signingSecretStatus`, `userTokenStatus`)

Read-only account inspect modules (`src/channels/read-only-account-inspect.discord.runtime.ts`, `slack.runtime.ts`, `telegram.ts`) have been removed; inspection is now handled through the plugin adapter `config.describeAccount` and the bundled plugin bootstrap registry.

## Channel Status

```bash
openclaw channels status            # quick status
openclaw channels status --probe    # deep probe (tests connections)
openclaw channels status --all      # all channels (read-only)
openclaw channels status --json     # JSON output
```

Falls back to config-only status when gateway unreachable. Reports "secret unavailable in this command path" for unresolvable SecretRef-backed tokens.

## Outbound Target & Action Resolution

Target display and action fallbacks moved to channel plugins:
- `messaging.formatTargetDisplay` - plugin callbacks for target display
- `messaging.inferTargetChatType` - plugin delegates for target kind inference
- `threading.resolveAutoThreadId` - auto-thread ID resolution moved to plugins
- Channel-specific message action fallbacks removed from core outbound

## Block Streaming

- Newline chunk mode no longer flushes per paragraph; `flushOnEnqueue` is opt-in only
- Envelope timestamp formatting honors timezone via `formatEnvelopeTimestamp`

## Adding a New Channel

When adding a new channel (built-in or extension):
1. Implement channel plugin interface
2. Register via `api.registerChannel()`
3. Add config type to `src/config/types.openclaw.ts`
4. Add Zod schema to `src/config/zod-schema.providers.ts`
5. Update docs: `docs/channels/`
6. Update `.github/labeler.yml` + create matching label
7. Update all UI surfaces (macOS app, web UI, mobile if applicable)
8. Add status + configuration forms

Note: `openclaw channels remove` now installs optional plugins before removal.

## Extension Directory Pattern

```
extensions/my-channel/
  ├── package.json
  ├── openclaw.plugin.json
  ├── index.ts              # exports plugin with registerChannel()
  ├── tsconfig.json          # package boundary tsconfig (now required)
  ├── src/
  │   ├── channel.ts        # channel implementation
  │   ├── types.ts          # channel-specific types
  │   └── ...
  └── skills/               # optional channel-specific skills
      └── my-channel/
          └── SKILL.md
```

Package boundary `tsconfig.json` files are now added to all extensions via bulk rollout for consistent type-checking isolation.

## Channel Plugin Bootstrap Registry

New `src/channels/plugins/bootstrap-registry.ts` provides a cached bootstrap plugin registry that merges runtime and setup plugin sections. Used for early startup operations (config presence detection, secret resolution) before the full plugin runtime is available. Merges `getBundledChannelPlugin` + `getBundledChannelSetupPlugin` entries, caching sorted IDs and resolved secrets.

## Channel Plugin Module Loader

New `src/channels/plugins/module-loader.ts` provides a centralized Jiti-based module loader for channel plugins. Caches loader instances per config key, handles native TypeScript loading with proper alias maps, and centralizes Windows-compatible Jiti dist loading policy.

## Config Presence Detection

Channel config presence detection (`src/channels/config-presence.ts`) was refactored:
- Hardcoded `CHANNEL_ENV_PREFIXES` array removed; env var prefixes now resolved from plugin manifests via `listBundledChannelPluginIds()`
- Auth state detection delegates to `hasBundledChannelPersistedAuthState()` from `src/channels/plugins/persisted-auth-state.ts` instead of per-channel filesystem probes
- `ChannelPresenceOptions.includePersistedAuthState` controls whether persisted auth state contributes to channel presence

## Config Write Policy

New shared config write policy (`src/channels/plugins/config-write-policy-shared.ts`) provides reusable authorization logic for channel config writes:
- `ConfigWriteTargetLike` - global, channel-scoped, account-scoped, or ambiguous targets
- `ConfigWriteAuthorizationResultLike` - allowed or blocked with reason (`ambiguous-target`, `origin-disabled`, `target-disabled`)
- Reusable across all channels that support chat-driven config writes

## Routing & Message Flow

Key paths:
- `src/routing/` - message routing between channels
- `src/channels/` - shared channel infrastructure (summary, snapshot fields)
- `src/channels/native-command-session-targets.ts` - native command session target resolution
- `src/channels/plugins/helpers.ts` - plugin helpers (default account resolution, pairing hints, DM security policy builder)
- `src/channels/plugins/group-policy-warnings.ts` - group policy warning collectors (open policy, allowlist provider, missing route allowlist)
- `src/channels/plugins/session-conversation.ts` - session conversation resolution; resolves thread IDs, base conversation IDs, and parent conversation candidates per channel via plugin messaging hooks or bundled fallback modules
- `src/channels/model-overrides.ts` - per-channel model overrides via `channels.modelByChannel` config; resolves group/channel/thread candidates with wildcard fallback
- `src/channels/chat-meta-shared.ts` - channel metadata factory that builds `ChatChannelMeta` from plugin catalog entries instead of hardcoded maps (NEW)
- `src/auto-reply/` - auto-reply logic, skill commands

Messages flow: Channel -> Routing -> Agent -> Response -> Channel

WebChat replies now stay on WebChat instead of being rerouted by persisted delivery routing.

## Extension-Specific Fixes

**MS Teams** (`extensions/msteams/`):
- Uses General channel conversation ID as team key for Bot Framework compatibility; Bot Framework sends `channelData.team.id` as the conversation ID, not the Graph API group GUID (`src/resolve-allowlist.ts`)

**Mattermost** (`extensions/mattermost/`):
- Reads `replyTo` param as fallback when `replyToId` is blank in plugin `handleAction` send paths (`src/channel.ts`)
- Fixes DM media upload for unprefixed 26-char user IDs (`extensions/mattermost/src/send.ts`)
- Sanitized `actionId` to alphanumeric-only with empty-id and collision guards

**Feishu/Lark** (`extensions/feishu/`):
- Passes `mediaLocalRoots` in `sendText` local-image auto-convert shim so local path images resolve correctly (`src/outbound.ts`)
- `@_all` no longer treated as bot-specific mention
- Bot-menu event keys mapped to slash commands
- Config schema uses `name` (not `botName`) for account display name
- **Drive comment event handling**: new `comment-handler.ts` / `comment-dispatcher.ts` flow processes Feishu Drive comment notice events; resolves comment turn, applies allowlist/pairing policy, dispatches to agent with `Surface: "feishu-comment"` context; supports dynamic agent creation for comment flows
- **Drive tool expanded with comment CRUD**: `feishu_drive` tool now supports `list_comments`, `list_comment_replies`, `add_comment`, and `reply_comment` actions alongside existing file operations (`extensions/feishu/src/drive.ts`)
- **Workspace-only localRoots in docx upload**: `docx-upload` action enforces workspace-scoped `localRoots` to prevent path traversal (NEW)

**ACPX** (`extensions/acpx/`):
- Embedded ACP runtime backend plugin: registers via `api.registerService()`, not `registerChannel()`
- Major runtime rewrite: `runtime.ts` reduced from ~1100 to ~150 lines; subprocess management, JSON-RPC, event parsing, and process spawning extracted or removed
- Config: `cwd`, `stateDir` (NEW), `permissionMode` (`approve-all`/`approve-reads`/`deny-all`), `nonInteractivePermissions` (`deny`/`fail`), `strictWindowsCmdWrapper`, `timeoutSeconds`, `queueOwnerTtlSeconds` (now compat-only, logged as ignored), `mcpServers`
- `agents` config: per-agent `command` overrides via `agents.<name>.command` (NEW)
- `command` and `expectedVersion` removed from top-level config; the plugin uses its bundled runtime
- MCP server injection: named MCP server definitions injected into ACP session bootstrap; `mcp-command-line.mjs` replaces old `mcp-agent-command.ts`
- Pinned version: bumped from `0.1.15` to **`0.5.1`**
- Spawned processes receive `OPENCLAW_SHELL=acp` env marker
- `pluginToolsMcpBridge` config option - when enabled, injects the built-in OpenClaw plugin-tools MCP server into ACP sessions so ACP agents can call plugin-registered tools
- Config schema now requires `minLength: 1` for `cwd` field
- `configContracts.dangerousFlags` added for `permissionMode: approve-all`; `secretInputs` scoped to `mcpServers.*.env.*`
- ACP Discord recovery and reset flow hardened (PR #62132) (NEW)

**WhatsApp** (`extensions/whatsapp/`):
- Now npm-publishable (`private: true` removed from package.json)
- Uses Baileys library for WhatsApp Web connection
- Test harness stabilized: session exports preserved in login coverage and shared workers, media test module exports preserved, CI extension checks stabilized
- **Reaction guidance levels**: `resolveWhatsAppReactionLevel()` resolves per-account reaction level from config with `defaultLevel: "minimal"` and `invalidFallback: "minimal"`; levels control ack reaction behavior (`extensions/whatsapp/src/reaction-level.ts`)

**BlueBubbles** (`extensions/bluebubbles/`):
- **Localhost probe respects private-network opt-out**: `allowPrivateNetwork` config is now honored during localhost health probes; account-level overrides preserved (PR #59373) (NEW)

**QQ Bot** (`extensions/qqbot/`):
- `/bot-logs` requires explicit allowlist: `hasExplicitCommandAllowlist()` checks that `allowFrom` contains non-wildcard entries before permitting log export; keeps `/bot-logs` closed when setup leaves allowFrom in permissive mode (`extensions/qqbot/src/slash-commands.ts`)

**Webhooks** (`extensions/webhooks/`) (NEW):
- Authenticated inbound webhooks that bind external automation to OpenClaw TaskFlows
- Config: named `routes` with `sessionKey`, `secret` (string or SecretRef), optional `path`, `controllerId`, `description`, `enabled`
- HTTP handler: `createTaskFlowWebhookRequestHandler()` with per-route auth validation
- Registers via `definePluginEntry()` with resolved route config

**QA Channel** (`extensions/qa-channel/`) (NEW):
- Internal testing channel using bus-based transport protocol
- Registers as a full channel plugin via `createChatChannelPlugin()`
- Supports accounts, config schema, setup wizard, status, and channel actions

## SearXNG Extension

Bundled self-hosted web search provider plugin (`extensions/searxng/`):
- Registers via `api.registerWebSearchProvider()` using `definePluginEntry()` (`extensions/searxng/index.ts`)
- **No API key required** - only needs a SearXNG base URL (`SEARXNG_BASE_URL` env var or `plugins.entries.searxng.config.webSearch.baseUrl` config)
- Configurable `categories` (comma-separated: general, news, science, etc.) and `language` (en, de, fr, etc.) via config or per-query tool parameters
- Auto-detect order: 200 (lower priority than commercial providers)
- Web search provider contract: `webSearchProviders: ["searxng"]` in `openclaw.plugin.json`
- Tool: `searxng_search` - search query with optional `count` (1-10), `categories`, `language` parameters
- Config resolution: `resolveSearxngBaseUrl()` checks plugin config, inline env SecretRef, then `SEARXNG_BASE_URL` env var (`extensions/searxng/src/config.ts`)

## Allowlists & Pairing

- Each channel supports allowlists for authorized users
- Pairing: some channels support device/user pairing flows
- Pairing setup codes now include shared auth (bootstrap token) via `src/pairing/setup-code.ts`
- Command gating: channels can restrict which commands are available
- Onboarding: channel-specific onboarding prompts


path: references/cli-commands.md
content: # OpenClaw CLI Commands

## Gateway

```bash
openclaw gateway run [--bind loopback|lan|auto|custom|tailnet] [--port N] [--force]
openclaw gateway run [--auth-mode none|token|password|trusted-proxy]
openclaw gateway run [--ws-log] [--dev] [--reset]
openclaw gateway run [--password-file <path>]
openclaw gateway run [--allow-unconfigured]          # allow start without gateway.mode=local
openclaw gateway stop
openclaw gateway status [--json]
openclaw gateway install                    # install as system service
openclaw gateway uninstall                  # remove system service
openclaw gateway start                      # start installed service
openclaw gateway restart                    # restart installed service
openclaw gateway call <method> [args]       # call gateway RPC method
openclaw gateway usage-cost                 # show usage/cost summary
openclaw gateway health                     # check gateway health
openclaw gateway probe                      # probe gateway connectivity
openclaw gateway discover                   # discover gateways on network
```

## Config

```bash
openclaw config get <key>                  # get a config value
openclaw config set <key> <value>          # set a config value
openclaw config set <key> --ref-provider <p> --ref-source 
 --ref-id <id>  # set SecretRef
openclaw config set --batch-json '<json>'  # batch set from JSON
openclaw config set --batch-file <path>    # batch set from file
openclaw config unset <key>                # remove a config key
openclaw config file                       # print config file path
openclaw config validate                   # validate current config
```

## Plugins

```bash
openclaw plugins install <spec>              # npm, clawhub, path, or archive
openclaw plugins install --link <path>       # symlink local
openclaw plugins install --force             # overwrite existing installed plugin or hook pack
openclaw plugins disable <id>
openclaw plugins enable <id>
openclaw plugins list [--json] [--verbose] [--enabled]
openclaw plugins info <id> [--json]
openclaw plugins inspect [id] [--all] [--json]
openclaw plugins uninstall <id|clawhub-spec> [--keep-files] [--force] [--dry-run]
openclaw plugins update [--all] [--dry-run] [--dangerously-force-unsafe-install]
openclaw plugins doctor
openclaw plugins install <spec> --dangerously-force-unsafe-install  # bypass security scan
openclaw plugins marketplace list <source> [--json]  # list plugins from marketplace source
```

## Channels

```bash
openclaw channels list                       # list configured channels
openclaw channels status                     # quick status
openclaw channels status --probe             # deep probe
openclaw channels status --all               # all channels
openclaw channels status --json              # JSON output
openclaw channels status --timeout <ms>      # custom timeout
openclaw channels capabilities               # list channel capabilities
openclaw channels resolve                    # resolve channel config
openclaw channels logs                       # show channel logs
openclaw channels add                        # add a channel
openclaw channels remove                     # remove a channel (installs optional plugins first)
openclaw channels login                      # channel-specific login
openclaw channels logout                     # channel-specific logout
```

## Agents

```bash
openclaw agent --message "<text>"            # send message to agent (single turn)
openclaw agent --message "<text>" --thinking low
openclaw agents list                         # list configured agents
openclaw agents bindings                     # list agent bindings
openclaw agents bind                         # create agent binding
openclaw agents unbind                       # remove agent binding
openclaw agents add <name>                   # create agent
openclaw agents set-identity                 # set agent identity
openclaw agents delete <name>                # delete agent
```

## Skills

```bash
openclaw skills list                         # list loaded skills
openclaw skills info <name>                  # show skill details
openclaw skills check                        # check skill status
```

Note: `skills add`/`skills remove` are via `npx skills`, not `openclaw skills`.

## Models

```bash
openclaw models list [--all] [--local] [--provider <name>] [--json] [--plain]
openclaw models status                       # show model/provider status
openclaw models set <model>                  # set default model
openclaw models set-image <model>            # set default image model
openclaw models scan                         # scan for available models
openclaw models aliases list                 # list model aliases
openclaw models aliases add                  # add model alias
openclaw models aliases remove               # remove model alias
openclaw models fallbacks list               # list fallback chain
openclaw models fallbacks add                # add fallback model
openclaw models fallbacks remove             # remove fallback model
openclaw models image-fallbacks list|add|remove  # image model fallbacks
openclaw models auth add                     # interactive token provider setup
openclaw models auth login                   # OAuth/token login flow
openclaw models auth setup-token             # Anthropic setup-token paste
openclaw models auth paste-token [--expires-in <duration>]  # token paste with optional expiration
openclaw models auth login-github-copilot    # GitHub Copilot auth
openclaw models auth order get|set|clear     # auth profile order management
```

## Sessions

```bash
openclaw sessions list                       # list active sessions
openclaw sessions info <key>                 # show session details
openclaw sessions --agent <id>               # list sessions for one agent
openclaw sessions --all-agents               # aggregate across agents
openclaw sessions --active <minutes>         # only sessions updated within N minutes
openclaw sessions cleanup [--dry-run] [--enforce] [--fix-missing]
openclaw sessions cleanup [--agent <id>] [--all-agents] [--store <path>]
openclaw sessions cleanup [--active-key <key>] [--json]
```

Session cleanup previews stale/cap pruning (`--dry-run`), applies maintenance (`--enforce`), and can prune entries with missing transcript files (`--fix-missing`).

## Tasks

```bash
openclaw tasks [--json] [--runtime <name>] [--status <name>]  # list tasks (default action)
openclaw tasks list [--json] [--runtime <name>] [--status <name>]  # explicit list
openclaw tasks show <lookup> [--json]                         # show one task by id, run id, or session key
openclaw tasks notify <lookup> <policy>                       # set notify policy (done_only|state_changes|silent)
openclaw tasks cancel <lookup>                                # cancel a running task
openclaw tasks audit [--json] [--severity <level>] [--code <name>] [--limit <n>]  # audit findings
openclaw tasks maintenance [--json] [--apply]                 # preview or apply tasks and TaskFlow maintenance
openclaw tasks flow list [--json] [--status <name>]           # list tracked TaskFlows
openclaw tasks flow show <lookup> [--json]                    # show one TaskFlow by flow id or owner key
openclaw tasks flow cancel <lookup>                           # cancel a running TaskFlow
```

Runtime filter values: `subagent`, `acp`, `cron`, `cli`. Status filter values: `queued`, `running`, `succeeded`, `failed`, `timed_out`, `cancelled`, `lost`. Flow status values: `queued`, `running`, `waiting`, `blocked`, `succeeded`, `failed`, `cancelled`, `lost`. Audit severity: `warn`, `error`. Audit codes: `stale_queued`, `stale_running`, `lost`, `delivery_failed`, `missing_cleanup`, `inconsistent_timestamps`, `restore_failed`, `stale_waiting`, `stale_blocked`, `cancel_stuck`, `missing_linked_tasks`, `blocked_task_missing`. Maintenance without `--apply` is a dry run; `--apply` runs reconciliation, cleanup stamping, and pruning (covers both tasks and TaskFlows).

Chat command: `/tasks` - shows task status for the current session (active + total counts, up to 5 visible tasks with timing and detail). Falls back to agent-scoped view when no session-scoped tasks exist.

## Infer (Capability CLI)

Stable CLI surface for provider-backed inference. Registered as both `openclaw infer` and `openclaw capability` (fallback alias).

```bash
# Capability catalog
openclaw infer list [--json]                 # list canonical capability ids and transports
openclaw infer inspect --name <id> [--json]  # inspect one capability

# Model inference
openclaw infer model run --prompt <text> [--model <p/m>] [--local] [--gateway] [--json]
openclaw infer model list [--json]           # list known models from catalog
openclaw infer model inspect --model <p/m> [--json]
openclaw infer model providers [--json]      # list model providers

# Model auth
openclaw infer model auth login --provider <id>
openclaw infer model auth logout --provider <id> [--json]
openclaw infer model auth status [--json]

# Image generation and description
openclaw infer image generate --prompt <text> [--model <p/m>] [--count N] [--size WxH] [--aspect-ratio R] [--resolution 1K|2K|4K] [--output <path>] [--json]
openclaw infer image edit --file <path> --prompt <text> [--model <p/m>] [--output <path>] [--json]
openclaw infer image describe --file <path> [--model <p/m>] [--json]
openclaw infer image describe-many --file <path> [--file <path>...] [--model <p/m>] [--json]
openclaw infer image providers [--json]

# Audio transcription
openclaw infer audio transcribe --file <path> [--language <code>] [--prompt <text>] [--model <p/m>] [--json]
openclaw infer audio providers [--json]

# Text to speech
openclaw infer tts convert --text <text> [--channel <id>] [--voice <id>] [--model <p/m>] [--output <path>] [--local] [--gateway] [--json]
openclaw infer tts voices [--provider <id>] [--json]
openclaw infer tts providers [--local] [--gateway] [--json]
openclaw infer tts status [--gateway] [--json]
openclaw infer tts enable [--local] [--gateway] [--json]
openclaw infer tts disable [--local] [--gateway] [--json]
openclaw infer tts set-provider --provider <id> [--local] [--gateway] [--json]

# Video generation and description
openclaw infer video generate --prompt <text> [--model <p/m>] [--output <path>] [--json]
openclaw infer video describe --file <path> [--model <p/m>] [--json]
openclaw infer video providers [--json]

# Web search and fetch
openclaw infer web search --query <text> [--provider <id>] [--limit N] [--json]
openclaw infer web fetch --url <url> [--provider <id>] [--format <fmt>] [--json]
openclaw infer web providers [--json]

# Embeddings
openclaw infer embedding create --text <text> [--text <text>...] [--provider <id>] [--model <p/m>] [--json]
openclaw infer embedding providers [--json]
```

All subcommands also work under `openclaw capability ...`.

## Daemon (legacy alias)

```bash
openclaw daemon install [--port N] [--runtime node|bun] [--force]
openclaw daemon uninstall
openclaw daemon start
openclaw daemon stop
openclaw daemon restart
openclaw daemon status [--json]
```

Install/manage the gateway as a system service (launchd on macOS, systemd on Linux, schtasks on Windows). Restart checks for gateway token drift (service token vs config token) before proceeding. On macOS, `start` attempts launchd agent recovery if the service is not loaded. Disabled in Nix mode.

## Onboarding

```bash
openclaw onboard                             # interactive
openclaw onboard --non-interactive --accept-risk  # automated
openclaw onboard --mode local --flow quick
openclaw onboard --reset --reset-scope full
```

Note: `onboard` avoids persisting talk fallback API key on fresh setup.

## Messages

```bash
openclaw message send "<text>"               # send to default channel
openclaw message send "<text>" --channel telegram
```

## Status

```bash
openclaw status                              # basic status
openclaw status --all                        # full status report
openclaw status --deep                       # gateway health probes
openclaw status --usage                      # provider usage snapshot
openclaw status --verbose                    # gateway connection details
openclaw status --json                       # JSON output
openclaw status --timeout <ms>               # custom timeout
openclaw status --debug                      # debug connection info
```

## Secrets

```bash
openclaw secrets reload                      # reload secret references
openclaw secrets audit                       # audit secret resolution
openclaw secrets configure                   # configure secrets provider
openclaw secrets apply                       # apply secrets to config
```

Secret resolution modes: `"strict"`, `"summary"`, `"operational_readonly"`.
Falls back to local resolution when gateway unavailable.
Target registry groups: `memory`, `qrRemote`, `channels`, `models`, `agentRuntime`, `status` (`src/cli/command-secret-targets.ts`).

## ACP

```bash
openclaw acp [--url <url>] [--token <token>] [--token-file <path>]
openclaw acp [--password <password>] [--password-file <path>]
openclaw acp [--session <key>] [--session-label <label>]
openclaw acp [--require-existing] [--reset-session]
openclaw acp [--no-prefix-cwd] [--provenance off|meta|meta+receipt]
openclaw acp [-v|--verbose]
openclaw acp client                          # run ACP client bridge
```

Runs an ACP bridge backed by the Gateway. Supports `--token-file`/`--password-file` for secret-safe credential passing (prefer over `--token`/`--password`).

## Cron

```bash
openclaw cron status [--json]                # cron scheduler status
openclaw cron list [--all] [--json]          # list jobs (--all includes disabled)
openclaw cron add|create --name <name>       # add a cron job (see full options below)
openclaw cron edit <id>                      # patch a cron job (see full options below)
openclaw cron rm|remove|delete <id> [--json] # remove a cron job
openclaw cron enable <id>                    # enable a cron job
openclaw cron disable <id>                   # disable a cron job
openclaw cron runs --id <id> [--limit <n>]   # show run history (default limit 50)
openclaw cron run <id> [--due]               # manually trigger (--due = only when due)
```

### `cron add` full options

```
Schedule (exactly one required):
  --at <when>              One-shot time (ISO with offset, or +duration; use --tz for offset-less)
  --every <duration>       Interval (e.g. 10m, 1h)
  --cron <expr>            5-field or 6-field cron expression
  --tz <iana>              Timezone for cron/offset-less --at (IANA)
  --stagger <duration>     Cron stagger window (e.g. 30s, 5m)
  --exact                  Disable cron staggering (stagger=0)

Identity:
  --name <name>            Job name (required)
  --description <text>     Optional description
  --disabled               Create job disabled
  --agent <id>             Agent id
  --session <target>       Session target (main|isolated|current|session:<id>)
  --session-key <key>      Session key for routing
  --wake <mode>            Wake mode (now|next-heartbeat, default: now)

Lifecycle:
  --delete-after-run       Delete one-shot job after success
  --keep-after-run         Keep one-shot job after success

Payload (exactly one required):
  --system-event <text>    systemEvent payload (main session)
  --message <text>         agentTurn payload (isolated/current/custom session)
  --thinking <level>       Thinking level (off|minimal|low|medium|high|xhigh)
  --model <model>          Model override (provider/model or alias)
  --timeout-seconds <n>    Timeout seconds for agent jobs
  --light-context          Lightweight bootstrap context
  --tools <csv>            Tool allow-list (comma-separated, e.g. exec,read,write)

Delivery:
  --announce               Announce summary to a chat (subagent-style)
  --deliver                Deprecated alias for --announce
  --no-deliver             Disable announce delivery and skip main-session summary
  --channel <channel>      Delivery channel (default: last)
  --to <dest>              Destination (E.164, Telegram chatId, Discord channel/user)
  --account <id>           Channel account id (multi-account setups)
  --best-effort-deliver    Do not fail job if delivery fails
```

### `cron edit` full options

Accepts all `cron add` options as patch fields, plus:

```
  --enable / --disable              Toggle job enabled state
  --clear-agent                     Unset agent (use default)
  --clear-session-key               Unset session key
  --no-light-context                Disable lightweight bootstrap
  --clear-tools                     Remove tool allow-list (use all tools)
  --no-best-effort-deliver          Fail job when delivery fails

Failure alerts:
  --failure-alert / --no-failure-alert           Enable/disable failure alerts
  --failure-alert-after <n>                      Alert after N consecutive errors
  --failure-alert-channel <channel>              Alert channel
  --failure-alert-to <dest>                      Alert destination
  --failure-alert-cooldown <duration>            Min time between alerts (e.g. 1h, 30m)
  --failure-alert-mode <mode>                    Alert mode (announce|webhook)
  --failure-alert-account-id <id>                Account ID for alert channel
```

Run `openclaw doctor --fix` to normalize legacy cron job storage.

## Diagnostics

```bash
openclaw doctor                              # diagnose issues
openclaw doctor --fix                        # auto-repair (see repair list below)
openclaw doctor --repair                     # alias for --fix
openclaw doctor --force                      # force repair without confirmation
openclaw doctor --deep                       # deep diagnostics (scan extra gateway services)
openclaw doctor --yes                        # accept defaults without prompting
openclaw doctor --non-interactive            # run without prompts (safe migrations only)
openclaw doctor --generate-gateway-token     # generate new gateway token
openclaw doctor --no-workspace-suggestions   # suppress workspace suggestions
openclaw dashboard                           # open web dashboard
openclaw reset                               # reset config/sessions
openclaw uninstall                           # uninstall openclaw
```

### Doctor `--fix` repairs

Repair contributions run in order:

1. **Gateway config** - checks gateway.mode and auth.mode ambiguity
2. **Auth profiles** - repairs legacy OAuth profile IDs
3. **Claude CLI** - notes Claude CLI binary, headless auth, OpenClaw auth profile, workspace, and project dir health (informational, no auto-repair)
4. **Gateway auth** - generates gateway token when missing (unless SecretRef-managed)
5. **Legacy state** - migrates legacy sessions/agent/WhatsApp auth state
6. **Legacy plugin manifests** - repairs legacy plugin manifest contracts
7. **Bundled plugin runtime deps** - installs missing npm dependencies required by bundled plugins (`npm install --omit=dev --no-save`)
8. **State integrity** - checks state directory structure
9. **Session locks** - checks for stale session locks
10. **Legacy cron normalization** - normalizes cron store: `jobId` to `id`, bare-string schedules to `{kind,expr}`, `schedule.cron` to `schedule.expr`, payload kind casing, legacy top-level payload/delivery fields, `deliver` mode to `announce`, legacy `notify: true` webhook fallback migration, stagger defaults, sessionTarget inference
11. **Sandbox** - repairs sandbox images, warns about scope
12. **Gateway services** - scans extra gateway installs, repairs service config, checks launchd overrides
13. **Startup channel maintenance** - runs channel plugin startup maintenance via plugin-driven doctor adapters (replaces per-provider Matrix/Telegram/Discord sequences)
14. **Security** - notes security warnings, reports effective exec policy scope snapshots
15. **Browser** - checks Chrome MCP browser readiness (delegated to plugin-driven detection)
16. **OAuth TLS** - checks OpenAI OAuth TLS prerequisites (deep mode)
17. **Hooks model** - checks hook model references
18. **systemd linger** - checks systemd linger for Linux service persistence
19. **Workspace status** - workspace health and suggestions
20. **Bootstrap size** - checks bootstrap context size
21. **Shell completion** - checks/installs shell completion
22. **Gateway health** - runs gateway health checks
23. **Memory search** - checks memory backend config, embedding providers, QMD binary; repairs short-term promotion artifacts when fixable
24. **Gateway daemon** - checks daemon service health
25. **Write config** - persists config changes when repairs applied
26. **Workspace suggestions** - offers workspace-level configuration hints

Config repair sequence (runs during config flow when `--fix` active):

- **Channel doctor repairs** - plugin-driven per-channel config normalization, compatibility migration, stale config cleanup, and allowlist repair (replaces hardcoded Telegram/Discord/Matrix repairs)
- **Open-policy allowFrom normalization** - adds missing `"*"` wildcard to `allowFrom` when `dmPolicy="open"` (top-level or nested `dm.allowFrom` depending on channel)
- **Bundled plugin load path migration** - rewrites legacy `dist/extensions/` or `dist-runtime/extensions/` paths to current `extensions/` paths in `plugins.load.paths`
- **Stale plugin config** - prunes `plugins.allow` and `plugins.entries` refs to plugins no longer installed; removes legacy web search config migration paths
- **Allowlist policy allowFrom** - repairs allowlist policy allowFrom entries
- **Channel plugin blockers** - warns when configured channels have their backing plugin disabled (`plugins.entries.<id>.enabled=false` or `plugins.enabled=false`)
- **Legacy tools-by-sender migration** - migrates untyped `toolsBySender` keys to typed `id:` prefix entries (deprecated bare keys become `id:<key>`)
- **Exec safe-bin coverage** - scaffolds missing `safeBinProfiles` entries for custom safeBins, warns about interpreter/runtime bins without profiles and risky semantics entries
- **Expanded cron normalization** - `delivery.mode` normalization, `sessionTarget` inference/normalization, legacy delivery input hoisting from payload to delivery object, stagger defaults for cron-type schedules, `schedule.anchorMs` coercion for every-type schedules
- **Legacy web search config migration** - migrates legacy `tools.web.search.<provider>.*` config to plugin-owned config paths
- **Legacy web fetch config migration** - migrates legacy `tools.web.fetch.firecrawl.*` config to plugin-owned config paths
- **Legacy X search config migration** - migrates legacy `tools.web.x_search.*` config to `plugins.entries.xai.config.webSearch.*`
- **Legacy internal hooks handler warning** - warns about `hooks.internal.handlers` (no auto-migration; must be manually converted to managed hook directories)
- **Plugin-contributed legacy config rules** - detects and migrates additional legacy config paths declared by plugin manifest contracts

Non-interactive cron repair is properly gated (requires `--fix` flag in non-interactive mode). Uninstall accepts plugin IDs, names, installed specs, resolved specs, marketplace plugin names, and `clawhub:<package>` specs (versionless match supported).

## Approvals

```bash
openclaw approvals get [--node <node>] [--gateway] [--json]
openclaw approvals set [--node <node>] [--gateway] [--file <path>] [--stdin]
openclaw approvals allowlist <pattern> [--node <node>] [--gateway] [--agent <id>]
```

`approvals get` now includes an effective policy report showing per-scope exec policy snapshots (requested vs host vs effective security/ask values) when config is available. JSON output includes the `effectivePolicy` field.

## Exec Environment

Child commands spawned via `exec` receive `OPENCLAW_CLI=1` in their environment (`src/infra/openclaw-exec-env.ts`). Shell-like runtimes also receive `OPENCLAW_SHELL` markers:
- `exec` - commands run through the exec tool
- `acp-client` - `openclaw acp client` bridge process
- `tui-local` - local TUI `!` shell commands
- `acp` - ACPX runtime backend child processes (`extensions/acpx/src/runtime-internals/process.ts`)

Shell-wrapper positional-argv allowlist matching (`src/infra/exec-approvals-allowlist.ts`) only permits direct carrier invocations: rejects single-quoted `$0`/`$n` tokens and newline-separated exec to prevent payload smuggling, while still accepting `exec -- carrier` forms.

`exec.host` default changed from `"sandbox"` to `"auto"` - picks the best available exec target at runtime.

`awk` and `sed` are excluded from the safeBins fast path to prevent injection via their expression arguments.

## Chat-Native Plugin Commands

When `commands.plugins: true` is enabled in config, plugin management is available directly in chat. Uses the same resolver as the CLI.

```
/plugin install clawhub:<pkg>    # install from ClawHub
/plugin install <spec>           # install (ClawHub first, npm fallback)
/plugin show <id>                # inspect plugin details
/plugin enable <id>              # enable a plugin
/plugin disable <id>             # disable a plugin
/plugin list                     # list loaded plugins
```

## Sub-CLIs (additional)

```bash
openclaw tui                                 # terminal UI
openclaw logs                                # view logs (falls back to local log file when gateway pairing required)
openclaw system                              # system info
openclaw approvals                           # manage approvals
openclaw nodes                               # manage browser nodes
openclaw devices                             # manage paired devices
openclaw node                                # node management
openclaw sandbox                             # sandbox configuration
openclaw dns                                 # DNS-SD discovery
openclaw docs                                # open documentation
openclaw hooks                               # manage hooks
openclaw webhooks                            # manage webhooks
openclaw qr                                  # QR code pairing
openclaw pairing                             # device pairing
openclaw directory                           # agent directory
openclaw security                            # security audit
openclaw update                              # check for updates
openclaw completion                          # shell completions
openclaw clawbot                             # legacy clawbot command aliases
```

## Dev Commands

```bash
pnpm install                                 # install deps
pnpm build                                   # type-check + build
pnpm tsgo                                    # TypeScript checks only
pnpm check                                   # lint + format check
pnpm format:fix                              # auto-format
pnpm test                                    # vitest
pnpm test:coverage                           # with coverage
pnpm openclaw ...                            # run CLI in dev mode
```


path: references/configuration.md
content: # OpenClaw Configuration

## Config File

- **Path**: `~/.openclaw/openclaw.json` (parsed as JSON5)
- **Override**: `OPENCLAW_CONFIG_PATH` env var (supports `file:///absolute/path`)
- **Format**: JSON5 (comments, trailing commas OK)
- **Key files**: `src/config/config.ts`, `src/config/io.ts`, `src/config/paths.ts`

## Config Resolution Pipeline

1. Load `.env` file if present
2. Load raw JSON5 from disk
3. Resolve `$include` directives (file composition)
4. Substitute `${ENV_VAR}` references (with `onMissing` warning mode for graceful degradation)
5. Apply environment overrides (`OPENCLAW_CONFIG_*` env vars via `env-vars.ts`)
6. Detect duplicate agent dirs (pre-validation)
7. Validate against Zod schema (including plugin schemas)
8. Apply runtime defaults chain: `applyMessageDefaults` -> `applyLoggingDefaults` -> `applySessionDefaults` -> `applyAgentDefaults` -> `applyContextPruningDefaults` -> `applyCompactionDefaults` -> `applyModelDefaults` -> `applyTalkConfigNormalization`
9. Normalize paths (`normalizeConfigPaths`) and exec safe-bin profiles (`normalizeExecSafeBinProfilesInConfig`)
10. Apply `env.vars` / `env.*` into `process.env` (skipping already-set and unresolved `${VAR}` refs)
11. Optionally load shell env fallback (`env.shellEnv.enabled`)
12. Ensure `commands.ownerDisplaySecret` exists (auto-generate + persist if missing)
13. Return validated `OpenClawConfig` (with TTL-based caching)

## Full Config Structure (`OpenClawConfig`)

```typescript
type OpenClawConfig = {
  meta?: {
    lastTouchedVersion?: string;
    lastTouchedAt?: string | number;  // ISO timestamp or Unix epoch (coerced to ISO)
  };
  auth?: AuthConfig;
  acp?: AcpConfig;
  env?: {
    shellEnv?: { enabled?: boolean; timeoutMs?: number };  // import from login shell
    vars?: Record<string, string>;                         // inline env overrides
    [key: string]: string | ... | undefined;               // sugar: direct env vars
  };
  wizard?: {                      // setup wizard state tracking
    lastRunAt?: string;
    lastRunVersion?: string;
    lastRunCommit?: string;
    lastRunCommand?: string;
    lastRunMode?: "local" | "remote";
  };
  diagnostics?: DiagnosticsConfig;
  logging?: LoggingConfig;
  cli?: CliConfig;
  update?: {                      // update channel + auto-update policy
    channel?: "stable" | "beta" | "dev";
    checkOnStart?: boolean;
    auto?: { enabled?: boolean; stableDelayHours?: number; stableJitterHours?: number; betaCheckIntervalHours?: number };
  };
  browser?: BrowserConfig;
  ui?: {
    seamColor?: string;           // accent color (hex)
    assistant?: { name?: string; avatar?: string };
  };
  secrets?: SecretsConfig;
  skills?: SkillsConfig;
  plugins?: PluginsConfig;       // see plugin-system.md
  models?: ModelsConfig;
  nodeHost?: NodeHostConfig;      // browser proxy settings for node hosts
  agents?: AgentsConfig;
  tools?: ToolsConfig;
  bindings?: AgentBinding[];      // top-level agent route/ACP bindings
  broadcast?: BroadcastConfig;
  audio?: AudioConfig;
  media?: {                       // inbound media handling
    preserveFilenames?: boolean;
    ttlHours?: number;
  };
  messages?: MessagesConfig;
  commands?: CommandsConfig;
  approvals?: ApprovalsConfig;
  session?: SessionConfig;
  web?: WebConfig;
  channels?: ChannelsConfig;
  cron?: CronConfig;
  hooks?: HooksConfig;
  discovery?: DiscoveryConfig;    // mDNS + wide-area DNS-SD
  canvasHost?: CanvasHostConfig;  // embedded canvas HTTP server
  talk?: TalkConfig;              // NOTE: top-level, not nested under gateway
  gateway?: GatewayConfig;
  memory?: MemoryConfig;
  mcp?: McpConfig;                // MCP server definitions
};
```

Key types: `src/config/types.openclaw.ts`, `src/config/types.plugins.ts`, `src/config/types.mcp.ts`

### Notable structural changes since last refresh

- Branded config state types: `SourceConfig`, `ResolvedSourceConfig`, `RuntimeConfig` distinguish config lifecycle stages
- `ConfigFileSnapshot` now has `sourceConfig` (pre-defaults) and `runtimeConfig` (post-defaults); `config` deprecated in favor of `runtimeConfig`
- `src/config/materialize.ts` module with profiles: `load`, `missing`, `snapshot` for config default application
- `hooks.internal.enabled` now defaults to `true` (was `false`) so bundled hooks load on fresh installs
- `exec.host` default changed from `"sandbox"` to `"auto"` (picks best available target at runtime)
- `exec.applyPatch.enabled` default changed from `false` to `true`
- Auth cooldowns: `overloadedProfileRotations` (default 1), `overloadedBackoffMs` (default 0), `rateLimitedProfileRotations` (default 1), new `authPermanentBackoffMinutes` (default 10), `authPermanentMaxMinutes` (default 60)
- MCP server config: `transport` (`"sse"` | `"streamable-http"`), `headers`, `connectionTimeoutMs` fields
- Web search config rewritten: legacy per-provider shapes (`brave`, `firecrawl`, etc.) replaced with generic `Record<string, unknown>`; `openaiCodex` and `x_search` scoped blocks
- `web.fetch.maxResponseBytes` added (default 2000000); `web.fetch.provider` added for fallback provider id
- Memory search: `provider`/`fallback` changed from union to generic `string`; `qmd.extraCollections` for cross-agent search; `store.fts.tokenizer` (`"unicode61"` | `"trigram"`) for CJK
- Auth profile: new `displayName` field
- `subagents.requireAgentId` blocks sessions_spawn without explicit agentId
- SecretRef: unsupported policies now hard-fail; gateway restart token drift fixed
- `awk` and `sed` excluded from exec safeBins fast path (injection prevention)
- Gateway: `webchat.chatHistoryMaxChars` controls max chars per text field in `chat.history` responses (default: 12000, max: 500000)
- WhatsApp: `reactionLevel` field (`"off" | "ack" | "minimal" | "extensive"`) controls agent reaction behavior per account
- Agent defaults compaction: `truncateAfterCompaction` boolean (default: false); `notifyUser` boolean (default: false); `provider` for plugin compaction providers
- Model aliases updated: `gpt-mini` -> `openai/gpt-5.4-mini`, new `gpt-nano` -> `openai/gpt-5.4-nano`
- Streaming config unified: Telegram/Discord/Slack legacy flat streaming keys replaced with structured `ChannelPreviewStreamingConfig` / `SlackChannelStreamingConfig`
- `ReplyToMode` expanded: now includes `"batched"` mode across all channels
- `ContextVisibilityMode` (`"all" | "allowlist" | "allowlist_quote"`) added to Telegram, Discord, and Slack channel configs
- `TalkProviderConfig` simplified: legacy ElevenLabs fields (`voiceId`, `voiceAliases`, `modelId`, `outputFormat`, `apiKey`) removed; providers use generic `[key: string]: unknown` extensibility
- Context pruning defaults now delegated to `applyProviderConfigDefaultsWithPlugin` (plugin-driven per-provider defaults)
- New agent defaults: `videoGenerationModel`, `musicGenerationModel`, `mediaGenerationAutoProviderFallback`, `skills`, `systemPromptOverride`, `contextInjection`, `subagents.allowAgents`
- Heartbeat: `includeSystemPromptSection` boolean; `target` type simplified to `ChannelId`
- New `ModelDefinitionConfig.contextTokens` for effective runtime cap separate from native `contextWindow`
- `ModelProviderConfig.request` added for provider-level HTTP transport overrides (auth, proxy, TLS)
- `ModelsConfig` discovery toggles: `copilotDiscovery`, `huggingfaceDiscovery`, `ollamaDiscovery` added
- Exec approvals: `enabled` field changed from `boolean` to `boolean | "auto"` (`NativeExecApprovalEnableMode`) across Discord, Telegram, and Slack
- Telegram: `errorPolicy`/`errorCooldownMs` at account, group, topic, and direct levels; `trustedLocalFileRoots`; `dangerouslyAllowPrivateNetwork`; `ingest` on topics/groups
- Slack: `thread.requireExplicitMention` for suppressing implicit thread mentions
- `tools.experimental.planTool` enables structured `update_plan` tool
- `media.tools.asyncCompletion.directSend` for direct channel sends on async media generation
- `MediaProviderRequestConfig.request` for provider HTTP transport overrides
- Config IO refactored: audit logging, write-prepare, observe-recovery, owner-display-secret, invalid-config, and runtime-snapshot extracted into separate modules
- Shell env expected keys now dynamically resolved from registered provider/channel env var lists

### MCP Config

```typescript
type McpConfig = {
  servers?: Record<string, McpServerConfig>;
};

type McpServerConfig = {
  command?: string;
  args?: string[];
  env?: Record<string, string | number | boolean>;
  cwd?: string;
  workingDirectory?: string;
  url?: string;
  transport?: "sse" | "streamable-http";
  headers?: Record<string, string | number | boolean>;
  connectionTimeoutMs?: number;
  [key: string]: unknown;          // extensible for future fields
};
```

## Gateway Config

```typescript
type GatewayConfig = {
  port?: number;                  // default: 18789
  mode?: "local" | "remote";
  bind?: "auto" | "lan" | "loopback" | "custom" | "tailnet";  // default: loopback
  customBindHost?: string;        // for bind="custom"
  controlUi?: GatewayControlUiConfig;
  auth?: GatewayAuthConfig;       // mode: "none" | "token" | "password" | "trusted-proxy"
  tailscale?: GatewayTailscaleConfig;
  remote?: GatewayRemoteConfig;   // remote.enabled field added (default: true when absent)
  reload?: GatewayReloadConfig;   // mode: "off" | "restart" | "hot" | "hybrid"
  tls?: GatewayTlsConfig;
  push?: GatewayPushConfig;       // APNs relay config (baseUrl, timeoutMs)
  http?: {
    endpoints?: {
      chatCompletions?: GatewayHttpChatCompletionsConfig;
      responses?: GatewayHttpResponsesConfig;      // /v1/responses (OpenResponses API)
    };
    securityHeaders?: GatewayHttpSecurityHeadersConfig;
  };
  nodes?: GatewayNodesConfig;     // browser routing + allowed/denied commands
  trustedProxies?: string[];
  allowRealIpFallback?: boolean;  // default: false
  tools?: GatewayToolsConfig;
  webchat?: GatewayWebchatConfig; // WebChat display/history settings
  channelHealthCheckMinutes?: number;  // default: 5, 0 to disable
  channelStaleEventThresholdMinutes?: number;  // default: 30, must be >= healthCheck interval
  channelMaxRestartsPerHour?: number;  // default: 10, rolling window cap
};
```

### Gateway WebChat Config

```typescript
type GatewayWebchatConfig = {
  /** Max characters per text field in chat.history responses before truncation (default: 12000). */
  chatHistoryMaxChars?: number;   // int, positive, max 500000
};
```

- Configurable via `gateway.webchat.chatHistoryMaxChars`
- Prevents large transcript payloads from overwhelming WebChat clients
- Validation: positive integer, capped at 500000

### Gateway Auth

- Auth modes: `none`, `token`, `password`, `trusted-proxy`
- Token/password support `SecretInput` (plaintext string or provider ref object)
- Trusted-proxy mode: identity-aware reverse proxy (Pomerium, Caddy + OAuth) passes user via `trustedProxy.userHeader`
- Rate limiting: per-IP sliding window with lockout (`gateway.auth.rateLimit`)
- Lockout expired entries now correctly reset attempt counters (prevents infinite escalation)
- Tailscale: Whois-verified header auth for WS Control UI surface

### Gateway Reload

```typescript
type GatewayReloadConfig = {
  mode?: GatewayReloadMode;       // "off" | "restart" | "hot" | "hybrid" (default: hybrid)
  debounceMs?: number;            // default: 300
  deferralTimeoutMs?: number;     // max wait for in-flight ops before SIGUSR1 restart (default: 300000)
};
```

- Watches config file via chokidar, debounced (default 300ms)
- `hybrid` (default): hot-reload channels, restart for structural changes
- Missing config file retried up to 2 times before skip
- `diffConfigPaths` compares old/new config structurally (handles arrays via `isDeepStrictEqual`)
- `deferralTimeoutMs`: caps how long the gateway waits for in-flight operations (e.g. active subagent LLM calls) before forcing a restart; lower values risk aborting active calls

## WhatsApp Config

```typescript
type WhatsAppConfig = WhatsAppConfigCore & WhatsAppSharedConfig & {
  accounts?: Record<string, WhatsAppAccountConfig>;
  defaultAccount?: string;
  actions?: WhatsAppActionConfig;   // reactions, sendMessage, polls (default: true for all)
};

// Key shared fields (available at top-level and per-account):
type WhatsAppSharedConfig = {
  enabled?: boolean;
  dmPolicy?: DmPolicy;             // default: "pairing"
  selfChatMode?: boolean;
  allowFrom?: string[];             // E.164 allowlist
  defaultTo?: string;               // default delivery target (E.164 or group JID)
  groupAllowFrom?: string[];
  groupPolicy?: GroupPolicy;        // default: "allowlist"
  contextVisibility?: ContextVisibilityMode;
  historyLimit?: number;
  dmHistoryLimit?: number;
  dms?: Record<string, DmConfig>;
  textChunkLimit?: number;          // default: 4000
  chunkMode?: "length" | "newline";
  mediaMaxMb?: number;              // default: 50
  blockStreaming?: boolean;
  blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
  groups?: Record<string, WhatsAppGroupConfig>;
  ackReaction?: WhatsAppAckReactionConfig;
  reactionLevel?: "off" | "ack" | "minimal" | "extensive";  // agent reaction guidance
  debounceMs?: number;              // batching window, default: 0
  heartbeat?: ChannelHeartbeatVisibilityConfig;
  healthMonitor?: ChannelHealthMonitorConfig;
};
```

### WhatsApp Reaction Levels

The `reactionLevel` field controls how the agent uses message reactions on WhatsApp:

- `"off"` - No reactions at all (ack and agent reactions disabled)
- `"ack"` - Only automatic ack reactions (e.g. eyes emoji on receipt); no agent-initiated reactions
- `"minimal"` (default) - Agent can react sparingly with brief guidance
- `"extensive"` - Agent can react liberally with richer guidance

Validated via `z.enum(["off", "ack", "minimal", "extensive"])`. Resolved at runtime through `resolveReactionLevel()` in `src/utils/reaction-level.ts`.

## Telegram Config

```typescript
type TelegramAccountConfig = {
  name?: string;
  capabilities?: TelegramCapabilitiesConfig;
  execApprovals?: TelegramExecApprovalConfig;  // enabled: boolean | "auto"
  markdown?: MarkdownConfig;
  commands?: ProviderCommandsConfig;
  customCommands?: TelegramCustomCommand[];  // custom menu commands
  configWrites?: boolean;
  dmPolicy?: DmPolicy;
  enabled?: boolean;
  botToken?: string;
  tokenFile?: string;               // path to token file; symlinks rejected
  replyToMode?: ReplyToMode;        // off|first|all|batched
  groups?: Record<string, TelegramGroupConfig>;
  direct?: Record<string, TelegramDirectConfig>;  // per-chat-id DM config
  allowFrom?: Array<string | number>;
  defaultTo?: string | number;
  groupAllowFrom?: Array<string | number>;
  groupPolicy?: GroupPolicy;
  contextVisibility?: ContextVisibilityMode;  // all|allowlist|allowlist_quote
  historyLimit?: number;
  dmHistoryLimit?: number;
  dms?: Record<string, DmConfig>;
  textChunkLimit?: number;          // default: 4000
  streaming?: ChannelPreviewStreamingConfig;  // unified streaming config
  mediaMaxMb?: number;
  timeoutSeconds?: number;
  retry?: OutboundRetryConfig;
  network?: TelegramNetworkConfig;  // IPv4/IPv6 prioritization + dangerouslyAllowPrivateNetwork
  proxy?: string;
  webhookUrl?: string;
  webhookSecret?: string;
  webhookPath?: string;
  webhookHost?: string;             // default: 127.0.0.1
  webhookPort?: number;             // default: 8787
  webhookCertPath?: string;
  actions?: TelegramActionConfig;
  threadBindings?: TelegramThreadBindingsConfig;
  reactionNotifications?: "off" | "own" | "all";
  reactionLevel?: "off" | "ack" | "minimal" | "extensive";  // default: ack
  heartbeat?: ChannelHeartbeatVisibilityConfig;
  healthMonitor?: ChannelHealthMonitorConfig;
  linkPreview?: boolean;            // default: true
  silentErrorReplies?: boolean;     // default: false
  errorPolicy?: "always" | "once" | "silent";
  errorCooldownMs?: number;
  responsePrefix?: string;
  ackReaction?: string;             // unicode emoji (e.g. "eyes")
  apiRoot?: string;                 // custom Bot API root URL
  trustedLocalFileRoots?: string[]; // for self-hosted Bot API file_path values
  autoTopicLabel?: AutoTopicLabelConfig;
};
```

- Thread bindings: persistent thread-session mapping with idle/max-age expiration and auto-sweep
- Custom commands: validated via `normalizeTelegramCommandName` (pattern: `/^[a-z0-9_]{1,32}$/`)
- Topics now support `agentId` for topic-specific agent routing
- `errorPolicy`/`errorCooldownMs`: per-account, per-group, per-topic, and per-direct error reporting control
- `ingest` field on `TelegramGroupConfig` and `TelegramTopicConfig`: emit internal message hooks for mention-skipped messages
- `dangerouslyAllowPrivateNetwork` on `TelegramNetworkConfig`: opt-in for trusted fake-IP/transparent-proxy environments
- `trustedLocalFileRoots`: filesystem roots for self-hosted Telegram Bot API absolute file_path values
- Exec approvals: `enabled` now accepts `boolean | "auto"` (`NativeExecApprovalEnableMode`)

## Discord Config

```typescript
type DiscordAccountConfig = {
  name?: string;
  capabilities?: string[];
  markdown?: MarkdownConfig;
  commands?: ProviderCommandsConfig;
  configWrites?: boolean;
  enabled?: boolean;
  token?: SecretInput;
  proxy?: string;
  allowBots?: boolean | "mentions";
  dangerouslyAllowNameMatching?: boolean;
  groupPolicy?: GroupPolicy;
  contextVisibility?: ContextVisibilityMode;  // all|allowlist|allowlist_quote
  textChunkLimit?: number;          // default: 2000
  streaming?: ChannelPreviewStreamingConfig;  // unified streaming config
  maxLinesPerMessage?: number;      // default: 17
  mediaMaxMb?: number;
  historyLimit?: number;
  dmHistoryLimit?: number;
  dms?: Record<string, DmConfig>;
  retry?: OutboundRetryConfig;
  actions?: DiscordActionConfig;
  replyToMode?: ReplyToMode;        // off|first|all|batched
  dmPolicy?: DmPolicy;
  allowFrom?: string[];
  defaultTo?: string;
  dm?: DiscordDmConfig;
  guilds?: Record<string, DiscordGuildEntry>;
  heartbeat?: ChannelHeartbeatVisibilityConfig;
  healthMonitor?: ChannelHealthMonitorConfig;
  execApprovals?: DiscordExecApprovalConfig;  // enabled: boolean | "auto"
  agentComponents?: DiscordAgentComponentsConfig;
  ui?: DiscordUiConfig;
  slashCommand?: DiscordSlashCommandConfig;
  threadBindings?: DiscordThreadBindingsConfig;
  intents?: DiscordIntentsConfig;
  voice?: DiscordVoiceConfig;
  pluralkit?: DiscordPluralKitConfig;
  responsePrefix?: string;
  ackReaction?: string;
  ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all" | "off" | "none";
  activity?: string;
  status?: "online" | "dnd" | "idle" | "invisible";
  autoPresence?: DiscordAutoPresenceConfig;
  activityType?: 0 | 1 | 2 | 3 | 4 | 5;
  activityUrl?: string;
  inboundWorker?: { runTimeoutMs?: number };   // default: 30 minutes
  eventQueue?: { listenerTimeout?: number; maxQueueSize?: number; maxConcurrency?: number };
};
```

### Discord Auto Presence

```typescript
type DiscordAutoPresenceConfig = {
  enabled?: boolean;              // default: false
  intervalMs?: number;            // poll interval for runtime state (default: 30000)
  minUpdateIntervalMs?: number;   // min spacing between presence updates (default: 15000)
  healthyText?: string;           // custom status while healthy
  degradedText?: string;          // custom status while degraded
  exhaustedText?: string;         // custom status on quota exhaustion
};
```

### Discord Agent Components

```typescript
type DiscordAgentComponentsConfig = {
  enabled?: boolean;              // default: true - agent-controlled buttons, select menus
};

type DiscordUiConfig = {
  components?: {
    accentColor?: string;         // hex color for Discord component containers
  };
};
```

### Discord Exec Approvals

```typescript
type DiscordExecApprovalConfig = {
  enabled?: boolean | "auto";     // NativeExecApprovalEnableMode; default: auto when approvers can be resolved
  approvers?: string[];           // Discord user IDs; falls back to commands.ownerAllowFrom
  agentFilter?: string[];         // only forward for these agent IDs
  sessionFilter?: string[];       // session key patterns (substring or regex)
  cleanupAfterResolve?: boolean;  // delete approval DMs after resolution (default: false)
  target?: "dm" | "channel" | "both";  // where to send prompts (default: "dm")
};
```

## Slack Config

```typescript
type SlackAccountConfig = {
  name?: string;
  mode?: "socket" | "http";       // connection mode (default: socket)
  signingSecret?: string;         // required for HTTP mode
  webhookPath?: string;           // default: /slack/events
  capabilities?: SlackCapabilitiesConfig;
  execApprovals?: SlackExecApprovalConfig;  // enabled: boolean | "auto"
  markdown?: MarkdownConfig;
  commands?: ProviderCommandsConfig;
  configWrites?: boolean;
  enabled?: boolean;
  botToken?: string;
  appToken?: string;
  userToken?: string;
  userTokenReadOnly?: boolean;    // default: true
  allowBots?: boolean;
  dangerouslyAllowNameMatching?: boolean;
  requireMention?: boolean;       // default: true
  groupPolicy?: GroupPolicy;
  contextVisibility?: ContextVisibilityMode;  // all|allowlist|allowlist_quote
  historyLimit?: number;
  dmHistoryLimit?: number;
  dms?: Record<string, DmConfig>;
  textChunkLimit?: number;
  streaming?: SlackChannelStreamingConfig;  // mode, chunkMode, preview, block, nativeTransport
  mediaMaxMb?: number;
  reactionNotifications?: SlackReactionNotificationMode;
  reactionAllowlist?: Array<string | number>;
  replyToMode?: ReplyToMode;      // off|first|all|batched
  replyToModeByChatType?: Partial<Record<"direct" | "group" | "channel", ReplyToMode>>;
  thread?: SlackThreadConfig;     // thread session behavior
  actions?: SlackActionConfig;
  slashCommand?: SlackSlashCommandConfig;
  dmPolicy?: DmPolicy;
  allowFrom?: Array<string | number>;
  defaultTo?: string;
  dm?: SlackDmConfig;
  channels?: Record<string, SlackChannelConfig>;
  heartbeat?: ChannelHeartbeatVisibilityConfig;
  healthMonitor?: ChannelHealthMonitorConfig;
  responsePrefix?: string;
  ackReaction?: string;           // shortcodes (e.g. "eyes")
  typingReaction?: string;        // reaction emoji while processing (e.g. "hourglass_flowing_sand")
};
```

- `typingReaction`: emoji shortcode added while processing a reply, removed when done. Useful as a typing indicator fallback when assistant mode is not enabled.
- `thread.requireExplicitMention`: when true, suppresses implicit thread mentions - only explicit @bot mentions trigger replies in threads where the bot has previously participated.
- Slack streaming unified into `SlackChannelStreamingConfig` (includes `nativeTransport` toggle for `chat.startStream`/`appendStream`/`stopStream`)

## Session Config

```typescript
type SessionConfig = {
  scope?: "per-sender" | "global";
  dmScope?: "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
  identityLinks?: Record<string, string[]>;
  mainKey?: string;               // always normalized to "main" (custom values ignored with warning)
  threadBindings?: SessionThreadBindingsConfig;  // shared defaults for thread routing
  reset?: SessionResetConfig;
  resetByType?: SessionResetByTypeConfig;
  resetByChannel?: Record<string, SessionResetConfig>;
  sendPolicy?: SessionSendPolicyConfig;
  maintenance?: SessionMaintenanceConfig;  // pruning, capping, file rotation
  parentForkMaxTokens?: number;            // max parent transcript tokens for thread/session forking (0 = disable guard)
  agentToAgent?: {
    maxPingPongTurns?: number;             // cap on requester/target agent turns (0-5, default: 5)
  };
  // ...
};
```

- `normalizeExplicitSessionKey`: provider-aware key normalization (Discord guild+channel normalization)
- `session.mainKey` is forcefully set to `"main"` regardless of config value

## Auth Config

```typescript
type AuthConfig = {
  profiles?: Record<string, AuthProfileConfig>;
  order?: Record<string, string[]>;
  cooldowns?: {
    billingBackoffHours?: number;          // default: 5
    billingBackoffHoursByProvider?: Record<string, number>;
    billingMaxHours?: number;              // default: 24
    authPermanentBackoffMinutes?: number;  // base backoff for permanent-auth failures (default: 10)
    authPermanentMaxMinutes?: number;      // cap for permanent-auth backoff (default: 60)
    failureWindowHours?: number;           // default: 24
    overloadedProfileRotations?: number;   // max same-provider rotations for overloaded errors (default: 1)
    overloadedBackoffMs?: number;          // fixed delay before retry (default: 0)
    rateLimitedProfileRotations?: number;  // max same-provider rotations for rate-limit errors (default: 1)
  };
};

type AuthProfileConfig = {
  provider: string;
  mode: "api_key" | "oauth" | "token";
  email?: string;
  displayName?: string;
};
```

## Agent Defaults - Compaction Config

```typescript
type AgentCompactionConfig = {
  mode?: "default" | "safeguard";
  reserveTokens?: number;
  keepRecentTokens?: number;
  reserveTokensFloor?: number;         // 0 disables the floor
  maxHistoryShare?: number;            // 0.1-0.9, default 0.5
  customInstructions?: string;
  recentTurnsPreserve?: number;
  identifierPolicy?: "strict" | "off" | "custom";
  identifierInstructions?: string;
  qualityGuard?: { enabled?: boolean; maxRetries?: number };
  postIndexSync?: "off" | "async" | "await";
  postCompactionSections?: string[];   // defaults to ["Session Startup", "Red Lines"]
  model?: string;                      // override compaction model
  timeoutSeconds?: number;             // default: 900
  provider?: string;                   // plugin compaction provider id
  truncateAfterCompaction?: boolean;   // truncate session JSONL after compaction (default: false)
  notifyUser?: boolean;                // send compaction notice to user (default: false)
  memoryFlush?: AgentCompactionMemoryFlushConfig;
};
```

- `truncateAfterCompaction`: When enabled, removes summarized entries from the session JSONL file after compaction to prevent unbounded file growth in long-running sessions. Default: false (preserves existing behavior).
- `notifyUser`: When enabled, sends a compaction notice to the user when compaction starts. Default: false (silent).
- `provider`: When set, delegates compaction summarization to a registered plugin provider instead of the built-in `summarizeInStages()`. Falls back to built-in on failure.

## Agent Defaults - New Fields

```typescript
type AgentDefaultsConfig = {
  // ... existing fields ...
  videoGenerationModel?: AgentModelConfig;
  musicGenerationModel?: AgentModelConfig;
  mediaGenerationAutoProviderFallback?: boolean;  // default: true; auto-append cross-provider fallbacks
  skills?: string[];                  // default skill allowlist for agents without explicit agents.list[].skills
  systemPromptOverride?: string;      // full system prompt replacement (debugging/experiments)
  contextInjection?: "always" | "continuation-skip";  // bootstrap file injection policy (default: always)
  subagents?: {
    allowAgents?: string[];           // default allowlist for sessions_spawn target agent ids
    // ... existing subagent fields ...
  };
  heartbeat?: {
    // ... existing heartbeat fields ...
    includeSystemPromptSection?: boolean;  // include ## Heartbeats section for default agent (default: true)
    target?: ChannelId;               // simplified from "last" | "none" | ChannelId
  };
  cliBackends?: Record<string, CliBackendConfig>;  // includes new imagePathScope: "temp" | "workspace"
};
```

## Memory Search Configuration

All settings live under `agents.defaults.memorySearch` in `openclaw.json` unless noted otherwise.

### Provider selection

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `provider` | `string` | auto-detected | `openai`, `gemini`, `voyage`, `mistral`, `ollama`, `local` |
| `model` | `string` | provider default | Embedding model name |
| `fallback` | `string` | `"none"` | Fallback adapter when primary fails |
| `enabled` | `boolean` | `true` | Enable/disable memory search |

Auto-detection order (first available wins): `local` -> `openai` -> `gemini` -> `voyage` -> `mistral`. `ollama` must be set explicitly.

### Remote endpoint - `memorySearch.remote`

| Key | Type | Description |
| --- | --- | --- |
| `baseUrl` | `string` | Custom API base URL |
| `apiKey` | `string` | Override API key |
| `headers` | `object` | Extra HTTP headers |
| `batch.enabled` | `boolean` | Enable batch API for embedding indexing (OpenAI/Gemini; default: true) |
| `batch.wait` | `boolean` | Wait for batch completion (default: true) |
| `batch.concurrency` | `number` | Max concurrent batch jobs (default: 2) |
| `batch.pollIntervalMs` | `number` | Poll interval in ms (default: 5000) |
| `batch.timeoutMinutes` | `number` | Timeout in minutes (default: 60) |

### Hybrid search - `memorySearch.query.hybrid`

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | Enable hybrid BM25 + vector search |
| `vectorWeight` | `number` | `0.7` | Vector score weight (0-1) |
| `textWeight` | `number` | `0.3` | BM25 score weight (0-1) |
| `candidateMultiplier` | `number` | `4` | Candidate pool size multiplier |
| `mmr.enabled` | `boolean` | `false` | Enable MMR diversity re-ranking |
| `mmr.lambda` | `number` | `0.7` | 0 = max diversity, 1 = max relevance |
| `temporalDecay.enabled` | `boolean` | `false` | Enable recency boost |
| `temporalDecay.halfLifeDays` | `number` | `30` | Score halves every N days |

### Local embedding - `memorySearch.local`

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `modelPath` | `string` | auto-downloaded | Path to GGUF model file |
| `modelCacheDir` | `string` | node-llama-cpp default | Cache dir for downloaded models |

Default model: `embeddinggemma-300m-qat-Q8_0.gguf` (~0.6 GB, auto-downloaded).

### Multimodal memory (Gemini) - `memorySearch.multimodal`

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `false` | Enable multimodal indexing |
| `modalities` | `string[]` | - | `["image"]`, `["audio"]`, or `["all"]` |
| `maxFileBytes` | `number` | `10000000` | Max file size for indexing |

Requires `gemini-embedding-2-preview`. Only applies to files in `extraPaths`.

### Extra paths - `memorySearch.extraPaths`

`string[]` of additional directories or files to index. Paths can be absolute or workspace-relative; directories are scanned recursively for `.md` files.

### Embedding cache - `memorySearch.cache`

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | Cache chunk embeddings in SQLite |
| `maxEntries` | `number` | `50000` | Max cached embeddings |

### Session memory (experimental)

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `experimental.sessionMemory` | `boolean` | `false` | Enable session indexing |
| `sources` | `string[]` | `["memory"]` | Add `"sessions"` to include transcripts |

### SQLite vector - `memorySearch.store.vector`

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `boolean` | `true` | Use sqlite-vec for vector queries |
| `extensionPath` | `string` | bundled | Override sqlite-vec path |

Falls back to in-process cosine similarity when sqlite-vec is unavailable.

### QMD backend - `memory.backend = "qmd"`, `memory.qmd.*`

Set `memory.backend` to `"qmd"` to enable. Settings live under `memory.qmd`:

| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `command` | `string` | `qmd` | QMD executable path |
| `searchMode` | `string` | `search` | `search`, `vsearch`, or `query` |
| `includeDefaultMemory` | `boolean` | `true` | Auto-index `MEMORY.md` + `memory/**/*.md` |
| `paths[]` | `array` | - | Extra paths: `{ name, path, pattern? }` |
| `sessions.enabled` | `boolean` | `false` | Index session transcripts |

**Update schedule** (`memory.qmd.update`): `interval` (default `"5m"`), `debounceMs` (default `15000`), `onBoot` (default `true`), `waitForBootSync`, `embedInterval`, `commandTimeoutMs`.

**Limits** (`memory.qmd.limits`): `maxResults` (default `6`), `maxSnippetChars`, `maxInjectedChars`, `timeoutMs` (default `4000`).

**Scope** (`memory.qmd.scope`): Same schema as `session.sendPolicy` - controls which sessions receive QMD results. Default is DM-only.

**Citations** (`memory.citations`): `"auto"` (default), `"on"`, `"off"` - controls `Source: <path#line>` footer in snippets.

## Env Variable Handling

### `env-vars.ts` - Config env var application

- `env.vars` and direct `env.*` string values are collected and applied to `process.env`
- Only applied when the key is not already set in `process.env`
- Dangerous host env vars blocked via `isDangerousHostEnvVarName` / `isDangerousHostEnvOverrideVarName`
- Values containing unresolved `${VAR}` references are skipped to prevent literal placeholders from being accepted as valid credentials
- Key normalization: `normalizeEnvVarKey(key, { portable: true })`

### `env-substitution.ts` - `${VAR}` resolution

- Pattern: `[A-Z_][A-Z0-9_]*` (uppercase only)
- Escape: `$${VAR}` outputs literal `${VAR}`
- `onMissing` callback mode: preserves placeholder + emits warning instead of throwing
- `containsEnvVarReference(value)`: check without substituting

### `shell-env-expected-keys.ts` - Dynamic key resolution

Shell env expected keys are now dynamically resolved from registered provider/channel env var lists via `resolveShellEnvExpectedKeys()` instead of a hardcoded list. Core keys always included: `OPENCLAW_GATEWAY_TOKEN`, `OPENCLAW_GATEWAY_PASSWORD`.

## Config Paths

```
~/.openclaw/
  +-  openclaw.json             # main config
  +-  extensions/              # global plugin installs
  +-  skills/                  # managed skills
  +-  credentials/             # web provider creds
  +-  agents/
  |   +-- <agent-id>/
  |       +-  workspace/       # agent workspace
  |       +-- sessions/        # session storage
  +-- agentbox/                # agentbox-specific data (if applicable)
```

Key file: `src/config/config-paths.ts`

## Config Gateway Methods

```typescript
// Available via gateway protocol
"config.get"       // Get current config with redaction
"config.schema"    // Get schema with plugin integration
"config.schema.lookup"  // Find schema for specific config path (#37266)
"config.set"       // Full config replacement (requires base hash)
"config.patch"     // Merge patch application (requires base hash)
"config.apply"     // Apply full config with restart coordination
```

- `config.schema.lookup` returns only the relevant schema subset for a given config path
- Config mutations include audit logging (actor, device ID, client IP, changed paths)
- Restart sentinel support for post-restart notifications

## Programmatic Config Operations

```typescript
import { loadConfig, writeConfigFile } from "openclaw/config";

// Read
const config = loadConfig();

// Modify
config.channels = {
  ...config.channels,
  telegram: { [REDACTED_SECRET]" }
};

// Write (with backup rotation + audit log)
await writeConfigFile(config);
```

## Environment Variable Substitution

Config values can reference env vars:
```json5
{
  "gateway": {
    "auth": { "token": "${OPENCLAW_GATEWAY_TOKEN}" }
  }
}
```

Resolved during config loading (step 4 of pipeline).

## `$include` Directive

Compose config from multiple files:
```json5
{
  "$include": "./base-config.json5",
  "gateway": { "port": 8080 }
}
```

Included file is merged, then local keys override.

## Defaults Applied at Runtime

Key file: `src/config/defaults.ts`

- **Model aliases**: `opus` -> `anthropic/claude-opus-4-6`, `sonnet` -> `anthropic/claude-sonnet-4-6`, `gpt` -> `openai/gpt-5.4`, `gpt-mini` -> `openai/gpt-5.4-mini`, `gpt-nano` -> `openai/gpt-5.4-nano`, `gemini` -> `google/gemini-3.1-pro-preview`, `gemini-flash` -> `google/gemini-3-flash-preview`, `gemini-flash-lite` -> `google/gemini-3.1-flash-lite-preview`
- **Agent defaults**: `maxConcurrent` (from `DEFAULT_AGENT_MAX_CONCURRENT`), `subagents.maxConcurrent` (from `DEFAULT_SUBAGENT_MAX_CONCURRENT`)
- **Context pruning**: delegated to `applyProviderConfigDefaultsWithPlugin` for plugin-driven per-provider defaults (Anthropic cache-ttl + heartbeat interval auto-applied when auth is configured)
- **Anthropic cache retention**: `cacheRetention: "short"` auto-applied for Anthropic/Bedrock models when using API key auth
- **Compaction**: defaults to `mode: "safeguard"`
- **Logging**: defaults `redactSensitive: "tools"`
- **Messages**: defaults `ackReactionScope: "group-mentions"`
- **Talk**: normalized via `normalizeTalkConfig`; legacy ElevenLabs fields removed; provider-specific config via `providers` map
- **Model defaults**: provider-specific normalization via `normalizeProviderSpecificConfig`; Mistral safe max tokens per model; cost/input/contextWindow/maxTokens defaults applied per model

## Models Config

```typescript
type ModelsConfig = {
  mode?: "merge" | "replace";
  providers?: Record<string, ModelProviderConfig>;
  bedrockDiscovery?: BedrockDiscoveryConfig;
  copilotDiscovery?: DiscoveryToggleConfig;    // new
  huggingfaceDiscovery?: DiscoveryToggleConfig; // new
  ollamaDiscovery?: DiscoveryToggleConfig;      // new
};

type ModelProviderConfig = {
  baseUrl: string;
  apiKey?: SecretInput;
  auth?: "api-key" | "aws-sdk" | "oauth" | "token";
  api?: ModelApi;
  injectNumCtxForOpenAICompat?: boolean;
  headers?: Record<string, SecretInput>;
  authHeader?: boolean;
  request?: ConfiguredModelProviderRequest;  // new: provider-level HTTP transport overrides
  models: ModelDefinitionConfig[];
};

type ModelDefinitionConfig = {
  id: string;
  name: string;
  api?: ModelApi;
  reasoning: boolean;
  input: Array<"text" | "image">;
  cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
  contextWindow: number;
  contextTokens?: number;          // new: effective runtime cap separate from native contextWindow
  maxTokens: number;
  headers?: Record<string, string>;
  compat?: ModelCompatConfig;      // includes new requiresStringContent field
};
```

- `ModelApi` values: `"openai-completions"`, `"openai-responses"`, `"openai-codex-responses"`, `"anthropic-messages"`, `"google-generative-ai"`, `"github-copilot"`, `"bedrock-converse-stream"`, `"ollama"`, `"azure-openai-responses"`
- `contextTokens`: optional effective runtime cap used for compaction/session budgeting, keeps native `contextWindow` intact
- `ModelProviderConfig.request`: `ConfiguredProviderRequest` with auth (bearer/header), proxy (env/explicit), and TLS overrides

## Validation

Zod schemas in `src/config/zod-schema*.ts`:
- `zod-schema.core.ts` - shared primitives
- `zod-schema.agents.ts` - agent definitions
- `zod-schema.providers.ts` - channel configs
- `zod-schema.providers-core.ts` - core provider type schemas
- `zod-schema.providers-whatsapp.ts` - WhatsApp-specific validation (reactionLevel, dmPolicy/allowFrom cross-checks)
- `zod-schema.session.ts` - session/message rules
- `zod-schema.hooks.ts` - hook definitions
- `zod-schema.installs.ts` - plugin install records
- `zod-schema.sensitive.ts` - sensitive field schemas
- `zod-schema.agent-runtime.ts` - agent runtime schemas
- `zod-schema.agent-defaults.ts` - agent defaults with compaction/heartbeat/subagents
- `zod-schema.ts` - main entry, composes all schemas

Malformed config breaks gateway startup. Schema cache key is hashed to prevent RangeError with many channels (#36603).

## Config CLI

```bash
openclaw config get <key>                  # get a config value
openclaw config set <key> <value>          # set a config value (also supports SecretRef builder)
openclaw config unset <key>                # remove a config key
openclaw config file                       # print config file path
openclaw config validate                   # validate current config
```

`config set` supports three modes: direct value, SecretRef builder (`--ref-provider`/`--ref-source`/`--ref-id`), and batch (`--batch-json`/`--batch-file`).

## Tools Config - Notable Changes

```typescript
type ToolsConfig = {
  // ... standard allow/deny/profile ...
  web?: {
    search?: {
      enabled?: boolean;
      provider?: string;
      apiKey?: SecretInput;
      maxResults?: number;
      timeoutSeconds?: number;
      cacheTtlMinutes?: number;
      openaiCodex?: {              // native Codex web search
        enabled?: boolean;
        mode?: "cached" | "live";
        allowedDomains?: string[];
        contextSize?: "low" | "medium" | "high";
        userLocation?: { country?: string; region?: string; city?: string; timezone?: string };
      };
    } & Record<string, unknown>;   // generic extensible search config
    x_search?: {                   // xAI Grok X search (apiKey removed - uses plugin config or XAI_API_KEY)
      enabled?: boolean;
      model?: string;
      inlineCitations?: boolean;
      maxTurns?: number;
      timeoutSeconds?: number;
      cacheTtlMinutes?: number;
    };
    fetch?: {
      enabled?: boolean;
      provider?: string;           // new: fallback provider id
      maxChars?: number;
      maxCharsCap?: number;        // default: 50000
      maxResponseBytes?: number;   // default: 2000000
      timeoutSeconds?: number;
      cacheTtlMinutes?: number;
      maxRedirects?: number;       // default: 3
      userAgent?: string;
      readability?: boolean;       // default: true
    };
  };
  media?: MediaToolsConfig;        // includes asyncCompletion.directSend
  experimental?: {
    planTool?: boolean;            // structured update_plan tool
  };
};
```

- Legacy Firecrawl-specific fetch config removed; replaced by generic `provider` field
- `x_search.apiKey` removed; auth resolved via plugin config or `XAI_API_KEY` env var
- `tools.experimental.planTool`: enables the structured `update_plan` tool for all providers (OpenAI-family runs auto-enable it)

## Streaming Config (Unified)

Legacy per-channel flat streaming keys (`streaming`, `blockStreaming`, `blockStreamingCoalesce`, `streamMode`, `draftChunk`) replaced with structured types:

```typescript
type ChannelPreviewStreamingConfig = {
  mode?: "off" | "partial" | "block" | "progress";
  chunkMode?: "length" | "newline";
  preview?: { chunk?: BlockStreamingChunkConfig };
  block?: { enabled?: boolean; coalesce?: BlockStreamingCoalesceConfig };
};

// Slack extends with nativeTransport
type SlackChannelStreamingConfig = ChannelPreviewStreamingConfig & {
  nativeTransport?: boolean;      // Slack chat.startStream/appendStream/stopStream
};
```

Used by Telegram, Discord, and Slack account configs via `streaming` field.

## AgentBox Config Pattern

AgentBox serves a base config via API and merges per-instance values at boot:

```typescript
// Backend: OPENCLAW_BASE_CONFIG (constants.ts)
{
  gateway: { mode: "local", port: 18789, bind: "loopback" },
  models: { mode: "replace", providers: { agentbox: {...}, blockrun: {...} } },
  plugins: { entries: { "openclaw-x402": { enabled: true }, telegram: { enabled: true } } },
  agents: { defaults: { model: "...", compaction: {...} } }
}

// Init script merges per-instance:
// - rpcUrl, telegramBotToken, dashboardUrl
// - Written to ~/.openclaw/openclaw.json via jq
```

## Config IO Architecture

The config IO module (`src/config/io.ts`) has been refactored with functionality extracted into focused submodules:

- `io.audit.ts` - Config write/observe audit logging (JSONL records with file metadata, suspicious change detection)
- `io.write-prepare.ts` - Config write preparation (merge patch creation, env ref restoration, validation formatting, path collection)
- `io.observe-recovery.ts` - Suspicious config read detection and backup recovery
- `io.owner-display-secret.ts` - Auto-generation and persistence of `commands.ownerDisplaySecret`
- `io.invalid-config.ts` - Structured invalid config error handling
- `runtime-snapshot.ts` - Runtime config snapshot state management (get/set/clear/refresh/write-notification listeners)


path: references/github-context.md
content: # OpenClaw GitHub Context
> Last refreshed: 2026-04-07T12:00:00Z

## Open Bugs (critical for development)

### Plugin System
- **#62511** - External plugins fail to load on 2026.4.5: normalizeAccountId is not a function (REGRESSION)
- **#62372** - RangeError: Maximum call stack size exceeded - google/minimax plugins fail to load on Windows (REGRESSION)
- **#62320** - CLI Stack Overflow in loadOpenClawPlugins / recordPluginError on 2026.4.5 (REGRESSION)
- **#62347** - memory-core plugin fails to load during onboard: "Unable to load plugin public-surface runtime" (REGRESSION)
- **#62277** - Feishu bundled plugin fails to load: missing register/activate export (REGRESSION)
- **#58427** - Plugin subagent calls intermittently fail with "Plugin runtime subagent methods are only available during a gateway request"
- **#53046** - Missing extension runtime API entry points in package.json in extensions (v2026.3.22 regression)
- **#52633** - openclaw-mem0 plugin loads but agent never calls memory_store tool, autoCapture not triggered
- **#52433** - ACPX plugin always reverts to 0.1.16, ignores npm/manual upgrade (0.3.1 never sticks)
- **#52250** - Plugin-backed channels become unknown in subagent flows when OPENCLAW_STATE_DIR is relative
- **#50441** - Slack channel crashes: "App is not a constructor" (@slack/bolt ESM interop)
- **#50382** - Duplicate plugin loading warning for installed extensions on gateway startup
- **#50131** - Plugin tools loaded in chat/agent do not inherit gateway subagent runtime/scope
- **#41757** - Custom plugin tool not surfaced to sandboxed agent sessions
- **#41229** - `plugins install` fails for local plugins when `plugins.allow` already exists
- **#39878** - `plugins uninstall` fails when channel config references the plugin being removed
- **#40232** - Plugin context engines can resolve before plugin registration
- **#36754** - Extension discovery silently skips symlinked plugin directories
- **#28122** - pnpm global install: bundled plugin manifests rejected as 'unsafe path'
- **#12854** - CJS dependencies fail with "require is not defined" under jiti loader
- **#36377** - Native bindings (sqlite3) broken under jiti

### Config Validation
- **#62472** - Context token / context window persisted incorrectly when multiple providers share the same model id
- **#62470** - google-vertex/gemini-3.1-pro-preview empty replies due to incorrect thinkingConfig disable path
- **#62405** - Control UI dropdown uses wrong value for OpenRouter models (missing openrouter/ prefix)
- **#62390** - buildQualifiedChatModelValue ignores provider when model already contains slash (REGRESSION)
- **#62388** - Comfy config path mismatch: plugins.entries.comfy.config accepted, runtime requires models.providers.comfy
- **#58932** - normalizeHyphenSlug strips all CJK characters, breaking group display names for non-Latin languages
- **#58880** - OpenRouter: "400 Reasoning is mandatory for this endpoint and cannot be disabled" (REGRESSION)
- **#58355** - Failed to set model for Ollama - model not allowed
- **#58302** - /reset and /new commands do not reset model to default
- **#58289** - Non-main agent may miss models.json and fail with "Unknown model" when provider plan is empty
- **#58087** - SecretRef-backed model provider headers sent as literal "secretref-managed" in API requests (REGRESSION)
- **#52551** - Dashboard model selector concatenates wrong provider ID
- **#49666** - Telegram proxy config wiped on gateway restart
- **#24008** - Unrecognized config keys silently wipe and reset entire section on restart
- **#36792** - Gateway promises "best-effort config" on invalid config but exits instead

### Gateway Startup
- **#62691** - `message send` crashes with `ERR_INTERNAL_ASSERTION` in v2026.4.5 (REGRESSION)
- **#62564** - ERR_UNSUPPORTED_ESM_URL_SCHEME on Windows (REGRESSION in v2026.4.5)
- **#62502** - ERR_UNSUPPORTED_ESM_URL_SCHEME on Windows non-C drives (REGRESSION)
- **#62446** - Install and first configuration fails: Cannot find module '@buape/carbon'
- **#62430** - openclaw tui crashes with Maximum call stack size exceeded during schema compilation (REGRESSION)
- **#62410** - Maximum call stack size exceeded during JSON Schema validation (REGRESSION)
- **#62408** - openclaw onboard --install-daemon ERR_UNSUPPORTED_ESM_URL_SCHEME on Windows (REGRESSION)
- **#62400** - CLI Local Commands Hang Indefinitely in OpenClaw 4.5 (REGRESSION)
- **#62356** - CLI enters CPU 100% infinite loop on unknown/unregistered subcommands (v2026.4.5)
- **#62342** - Agent listener invoked outside active run causes Gateway crash
- **#62329** - Gateway segfaults on every message when session history contains a cached PNG image (REGRESSION)
- **#62326** - openclaw devices command unresponsive (REGRESSION)
- **#62285** - openclaw command hang and gateway service not working after upgrading to 2026.4.5 (REGRESSION)
- **#62302** - RangeError: Maximum call stack size exceeded on startup due to talk-voice invoking JITI AST compilation (REGRESSION)
- **#58956** - Pre-compaction memory flush leaks into chat UI and blocks the user's active turn in 2026.3.31 (REGRESSION)
- **#58807** - falling back to HTTP: ENETUNREACH (REGRESSION)
- **#58415** - ws-stream WebSocket connect fails with 500 and always falls back to HTTP
- **#58306** - Gateway process storm: KeepAlive + ThrottleInterval=1 causes 30+ zombie processes when port not released
- **#53168** - CLI commands crash gateway via WebSocket handshake timeout (v2026.3.22)
- **#52766** - Gateway WebSocket handshake timeout on local loopback breaks all CLI RPC commands
- **#51987** - Local gateway websocket handshake times out on 127.0.0.1:18789
- **#51879** - CLI gateway handshake timeout on WSL2
- **#51438** - CLI to Gateway WebSocket connections timeout in v2026.3.12+ (regression)
- **#50074** - Gateway restart fails - stale process misdetection
- **#41804** - Gateway persistent stale processes on Windows
- **#36585** - TLS null dereference crash during socket reconnection
- **#34539** - structuredClone FATAL ERROR in orphaned subagent reconciliation
- **#24178** - Doctor check blocks indefinitely without TTY
- **#22972** - Health check times out on slow startups

### Secrets/Auth
- **#62546** - Canvas returns 401 Unauthorized on Windows despite correct token (v2026.4.5) (REGRESSION)
- **#62380** - Windows Node system.run completely non-functional - exec-approvals.sock never created (REGRESSION)
- **#58768** - Incompatible security approval config
- **#58687** - Security prompt is too long to approve, clicking "Allow All" is ineffective
- **#52488** - `openclaw status --all` shows "missing scope: operator.read" even after full pairing
- **#51911** - Anthropic setup-token onboarding path has multiple failure modes
- **#49885** - google-vertex fails with "No credentials found for profile" even when ADC is valid
- **#49138** - OAuth flow missing api.responses.write scope
- **#37303** - Onboarding crashes with exec secret provider
- **#12517** - [SEC] All 31 plugins share single Node.js process - lateral movement across plugins
- **#12518** - [SEC] All plugin credentials in shared `process.env` - no isolation
- **#11829** - [SEC] API keys leak to LLM via tool outputs, error messages, system prompts

### Channels
- **#62699** - ACP/acpx persistent sessions die with `queue owner unavailable`; Telegram topic-bound ACP issues
- **#62690** - Telegram 401 Unauthorized Error - Valid Token Not Accepted (REGRESSION)
- **#62670** - Agent listener invoked outside active run - concurrent WhatsApp + Telegram sessions
- **#62623** - v2026.4.5 regression: Slack DM file attachments no longer download to inbound (REGRESSION)
- **#62610** - Telegram channel missing from npm installs on macOS
- **#62580** - Response Contamination / Cross-Session Leakage
- **#62576** - Telegram approval/system messages broadcast to multiple authorized users (REGRESSION)
- **#62558** - QQBot channel - Framework slash commands (/new, /reset, /status) not working
- **#62551** - Agent is not being able to read files that were sent on Slack
- **#62498** - Media download failed in v2026.4.5 (re-occurrence of #32326) (REGRESSION)
- **#62324** - v2026.4.5 regression: non-main agents in Telegram multi-agent stop replying unless contextEngine=legacy (REGRESSION)
- **#62318** - Slack inbound images broken in 2026.4.5 - speech-core/runtime-api.js resolution failure (REGRESSION)
- **#62311** - Control UI completely blank on main branch: "process is not defined" (REGRESSION)
- **#58905** - Feishu(Lark) Webhook URL verification fails with 401 Invalid signature when encryptKey is set
- **#58884** - Numerous calls to statsig.anthropic.com when not using Claude/Anthropic models
- **#58781** - include_usage not auto-added to LLM calls, console stats broken (REGRESSION)
- **#58738** - ClawBot WeCom service account unresponsive after pairing (REGRESSION)
- **#58737** - Agent display name/avatar reverts to bot default on edited Slack messages (REGRESSION)
- **#58639** - Gmail to Telegram/Discord notifications stopped: /gmail-pubsub push endpoint broken
- **#58535** - Discord announce removing fields from input
- **#58523** - Slack multi-workspace: outbound works on second workspace but inbound DM replies never reach OpenClaw
- **#58514** - Google Chat: Space/Group messages silently ignored
- **#58408** - WhatsApp Inbound Works But Outbound CLI Fails With "No Active Listener"
- **#58402** - message tool "Operation aborted" + WhatsApp reconnect storm + xai-auth log spam
- **#58268** - Slack socket-mode mentions silently dropped as no-mention after stale-socket reconnect
- **#58249** - Teams webhook broken in 2026.3.24+: publicUrl removed breaks JWT validation (REGRESSION)
- **#58107** - Multiple Feishu group agents - only main reply delivered, others silently dropped (REGRESSION)
- **#52778** - WeCom DM message isolation failure
- **#52763** - WhatsApp groupPolicy "allowlist" bypassed
- **#52729** - hooks.internal.enabled causes LINE webhook 404
- **#52615** - Discord `allowFrom` Web UI double-escaping IDs
- **#52454** - Telegram:direct does not send thread back when you talk in the GUI
- **#52167** - WhatsApp media sends fail on old-style group JIDs
- **#51716** - Signal group messages silently dropped on Node 24
- **#51190** - Discord channel running=true while connected=false
- **#51158** - Matrix plugin syncs but never routes inbound messages to agent
- **#50999** - Telegram polling enters repeated stall/restart loop on macOS
- **#50450** - Telegram duplicate message sending bug (restart causes resend)
- **#50326** - Telegram `replyToMode: all` does not reply to triggering message in topics
- **#50174** - Windows: Telegram polling stalls every ~107s + Discord disconnects
- **#41581** - Telegram DM partial streaming regressed to choppy editMessageText
- **#41576** - Channel leaks `[[reply_to:ID]]` tags into visible message text
- **#36687** - `session.dmScope` reset causes cross-channel reply leakage
- **#57731** - Discord plugin aggressively drops connections (stale-socket) causing delayed and duplicated messages (REGRESSION)
- **#57660** - Telegram polling stall detector fires too aggressively (110s), causes message delivery failures

### Agent Runtime
- **#62706** - Chrome MCP attach fails when OpenClaw passes both --browserUrl and --userDataDir
- **#62684** - Local Ollama agent pipeline times out with no provider logs on 2026.4.5
- **#62672** - Fallback chain propagates primary model's 429 error to secondary providers
- **#62614** - Retry storm can repeatedly resend oversized session context after timeouts/rate limits
- **#62604** - Control UI image paste injects massive base64 text into composer
- **#62580** - Cross-Session Leakage / Response Contamination
- **#62564** - ERR_UNSUPPORTED_ESM_URL_SCHEME on Windows (REGRESSION)
- **#62561** - Inline image attachments dropped before imageModel fallback fires
- **#62550** - browser snapshot --format ai returns role refs instead of numeric AI refs on 2026.4.5
- **#62541** - Cannot create new agent after enable dreaming config (REGRESSION)
- **#62533** - Model "Not Found" (404) errors when using a remote Ollama provider as a fallback
- **#62517** - edit tool fails with tilde paths, error broadcasts to Telegram (REGRESSION)
- **#62505** - Coding Agent never completes anything (worked in 2026.4.2) (REGRESSION)
- **#62282** - Ollama embedding broke in 2026.4.5 (REGRESSION)
- **#58962** - pdf tool / model not being used
- **#58880** - OpenRouter reasoning mandatory, cannot be disabled (REGRESSION)
- **#58842** - model hanging
- **#58631** - Haiku outputs with formatted tables/structured text crash webchat session
- **#58628** - Browser enabled and running, but browser tool is missing from agent tool list (REGRESSION)
- **#58599** - Edit tool schema mismatch: "edits" array in schema vs flat "oldText"/"newText" in implementation
- **#58553** - Long sessions (600+ messages) break with "invalid function call parameters"
- **#58442** - Model failover fails on Coding Plan quota 429 errors - infinite loop
- **#58363** - kimi web_search run error
- **#58358** - OpenClaw mishandles message_stop for KimiCodingPlan Anthropic format
- **#58235** - Gemini 3.1 Pro Preview via OpenAI-compat API missing thought_signature in tool_calls
- **#58189** - VLLM-deployed QWEN3-32B gets stuck in a loop
- **#57826** - v2026.3.28 agent systematically omits required tool parameters (REGRESSION)
- **#57821** - Cannot read properties of undefined (reading 'push') with third-party Anthropic-compatible provider
- **#57774** - Sub-agent session not properly exited, causing death loop
- **#57760** - Automatic model failover does not work when primary model hits rate limits
- **#57753** - anthropic-messages SSE state machine fails to reset between tool-use loop iterations (REGRESSION)
- **#57663** - MiniMax M2.7 persistent 408 timeouts not surfaced to user; fallback chain causes 4+ minute delays (REGRESSION)
- **#57654** - Unexpected event order: message_start before message_stop
- **#57626** - Model switch to unresponsive Ollama model cascades into infinite [object Object] loop, corrupts session
- **#52708** - ACP `mode: "run"` sessions leave orphan processes
- **#52677** - web_search/web_fetch tools not available with built-in Gemini provider
- **#52612** - Full cacheWrite after using subagents (Anthropic)
- **#52604** - repairToolUseResultPairing misses orphaned tool IDs from MiniMax/OpenAI-compat
- **#52559** - Agent writes do not persist to host workspace even with sandbox off
- **#52317** - System prompt completely missing after /new
- **#51774** - Google provider adapter: streaming flag causes 400
- **#51743** - Google provider: usageMetadata not mapped - all Gemini calls record 0 tokens
- **#51593** - HTTP 400: "tool call id is duplicated" with moonshot/kimi-k2.5
- **#50401** - DeepSeek-V3 (SiliconFlow): 400 error on tool calls
- **#50178** - 400 thinking enabled but reasoning_content missing
- **#50094** - Compactor preserves orphaned tool_use blocks causing 400 on model switch
- **#50017** - Automatic model fallback does not recover cleanly from invalid primary model
- **#41707** - After upgrade, tools enter infinite repeat loop
- **#41291** - Agent enters infinite retry loop when tool calls fail
- **#36520** - Runtime events mutate system prompt every turn, invalidating prompt cache

### Skills
- **#58712** - memos-memory-guide skill shows disabled but works correctly - WSL path issue (REGRESSION)
- **#58319** - Support non-JSON SecretRef in skills entries apiKey
- **#52073** - Agent becomes completely unresponsive during Skill installation
- **#35272** - Sub-agents cannot access shared skills from `~/.openclaw/skills/`
- **#29122** - Workspace skills silently not loading
- **#21709** - `skills.allowBundled: []` allows all instead of blocking all

### Cron
- **#58927** - Cron jobs ignore payload.model and agentId, always resolving to default/main agent model
- **#58575** - --model flag on isolated cron jobs does not override session model (REGRESSION)
- **#58065** - Specified model for cron overridden by agent default (REGRESSION)
- **#57947** - Cron payload model override ignored - always resolves to agent default (REGRESSION)
- **#52975** - sessionTarget method of using custom sessionID in cron does not work
- **#52806** - Cron: Isolated sessions not executing
- **#52563** - Cron task configuration error causes Gateway crash
- **#52271** - sessions_send from cron/heartbeat deadlocks on nested lane
- **#51530** - Cron job agentTurn model override to Google/Gemini failing
- **#49258** - Cron job state inconsistency

### TUI / Control UI
- **#62604** - Control UI image paste injects massive base64 text into composer
- **#62418** - WebChat Control UI displays internal exec notifications in user-visible chat (REGRESSION)
- **#62411** - Gemma 4 thinking-off semantics broken in Control UI
- **#62371** - Control UI drag-and-drop images not attached to messages (REGRESSION)
- **#62311** - Control UI completely blank on main branch: "process is not defined" (REGRESSION)
- **#58694** - Can only chat in TUI mode
- **#58631** - Haiku outputs with formatted tables crash webchat session
- **#57964** - Repeated characters collapse when displayed in dashboard or TUI (REGRESSION)
- **#57822** - Web UI agent dropdown shows agent.id instead of identity.name
- **#58479** - Approval dialog succeeds in Control UI, but exec never consumes the approval (REGRESSION)
- **#58560** - Browser tools fail with AJV "no schema with key or ref" 2020-12 error (REGRESSION)
- **#53096** - Control UI assets not found on v2026.3.22
- **#52565** - Control UI event listener registration failure in 2026.3.13
- **#52511** - Control UI session intermittently loses browser control permission
- **#52282** - Windows pnpm ui:build fails (path with spaces)
- **#51685** - Control UI freezes with high CPU when switching sessions
- **#51507** - WebChat/Control-UI context calculation inaccurate
- **#37243** - TUI replies routed to Telegram instead of TUI

### Critical / Data Loss
- **#62339** - Unknown phone number appeared in OpenClaw allowFrom without user authorization (WhatsApp)
- **#57827** - OpenClaw 2026.3.28 `config.openFile` / OPENCLAW_CONFIG_PATH command injection vulnerability
- **#58140** - Running official install command triggers Windows Defender - malicious batch file and registry keys added

### Exec Approvals (new in v2026.3.31)
- **#62380** - Windows Node system.run completely non-functional - exec-approvals.sock never created (REGRESSION)
- **#58768** - Incompatible security approval config
- **#58687** - Security prompt too long to approve, "Allow All" ineffective
- **#58479** - Approval dialog succeeds but exec never consumes it (REGRESSION)
- **#58356** - system.run.prepare broken after update to v2026.3.28

## Breaking Changes & Regressions

### Breaking in v2026.4.5 (current)
- External plugins fail to load: normalizeAccountId is not a function (#62511)
- Maximum call stack size exceeded on Windows - plugins fail to load (#62372, #62320, #62302)
- ERR_UNSUPPORTED_ESM_URL_SCHEME on Windows for all non-file:// paths (#62564, #62502, #62374, #62408)
- CLI commands hang indefinitely consuming 90%+ CPU (#62400, #62356)
- Gateway segfaults on sessions containing cached PNG images (#62329)
- `openclaw devices` command unresponsive (#62326)
- Slack DM file attachments no longer download to inbound (#62623)
- Slack inbound images broken - speech-core/runtime-api.js resolution failure (#62318)
- Coding Agent never completes anything (worked in 2026.4.2) (#62505)
- Ollama embedding broke (#62282)
- Canvas returns 401 Unauthorized on Windows (#62546)
- Cannot create new agent after enable dreaming config (#62541)
- Edit tool fails with tilde paths (#62517)
- Telegram approval messages broadcast to multiple users (#62576)
- Non-main agents in Telegram multi-agent stop replying unless contextEngine=legacy (#62324)
- memory-core plugin fails to load: "Unable to load plugin public-surface runtime" (#62347)
- Feishu bundled plugin fails to load: missing register/activate export (#62277)
- Port 18789 in use when configuring new dreaming schedule (#62276)
- Control UI completely blank: "process is not defined" (#62311)
- Media download failed (re-occurrence of #32326) (#62498)

### Breaking in v2026.3.31 (still relevant)
- Pre-compaction memory flush leaks into chat UI and blocks user turns (#58956)
- Exec approval system overhauled - excessive prompts, allow-always behaves as allow-once (fixed in #58792, #58860, #58745)

### Breaking in v2026.3.28+ (still relevant)
- Browser server removed from gateway - "openclaw browser start" not found (#58256, #58221)
- Teams webhook broken: publicUrl removed breaks JWT validation (#58249)
- Cron model override ignored (#58575, #58065, #57947, #58927)
- SecretRef-backed headers sent as literal "secretref-managed" (#58087)
- Agent systematically omits required tool parameters (#57826)

### Breaking in v2026.3.22 (still relevant)
- Control UI assets missing from npm package (#53096)
- Missing extension runtime API entries (#53046)

### Active Regressions
| Issue | Summary |
|-------|---------|
| #62691 | `message send` crashes with ERR_INTERNAL_ASSERTION in v2026.4.5 |
| #62623 | Slack DM file attachments no longer download in 2026.4.5 |
| #62564 | ERR_UNSUPPORTED_ESM_URL_SCHEME on Windows (2026.4.5) |
| #62541 | Cannot create new agent after enable dreaming config |
| #62517 | Edit tool fails with tilde paths, error broadcasts to Telegram |
| #62511 | External plugins fail to load: normalizeAccountId is not a function |
| #62505 | Coding Agent never completes (worked in 2026.4.2) |
| #62498 | Media download failed in v2026.4.5 |
| #62418 | WebChat Control UI displays internal exec notifications |
| #62410 | Maximum call stack size exceeded during JSON Schema validation |
| #62400 | CLI local commands hang indefinitely (2026.4.5) |
| #62390 | buildQualifiedChatModelValue ignores provider when model has slash |
| #62380 | Windows Node exec-approvals.sock never created |
| #62372 | RangeError: Maximum call stack size exceeded (plugin loading) |
| #62347 | memory-core plugin fails to load during onboard |
| #62329 | Gateway segfaults on cached PNG image in session history |
| #62326 | openclaw devices command unresponsive |
| #62324 | Non-main Telegram agents stop replying unless contextEngine=legacy |
| #62320 | CLI Stack Overflow in loadOpenClawPlugins |
| #62318 | Slack inbound images broken in 2026.4.5 |
| #62311 | Control UI blank: "process is not defined" |
| #62302 | RangeError: Maximum call stack on startup (talk-voice/JITI) |
| #62285 | Gateway not working after upgrading to 2026.4.5 |
| #62282 | Ollama embedding broke in 2026.4.5 |
| #62277 | Feishu bundled plugin fails to load |
| #62276 | Port 18789 in use when configuring dreaming |
| #62261 | Dev channel fails to build (preflight lint fails) |
| #58956 | Pre-compaction memory flush leaks into UI, blocks user turn |
| #58880 | OpenRouter reasoning mandatory, cannot be disabled |
| #58807 | WebSocket fallback to HTTP with ENETUNREACH |
| #58781 | include_usage not auto-added, console stats broken |
| #58738 | ClawBot WeCom unresponsive after pairing |
| #58737 | Slack agent display name/avatar reverts on edited messages |
| #58712 | Skills WSL path issue shows disabled but works |
| #58628 | Browser tool missing from agent tool list despite enabled |
| #58599 | Edit tool schema mismatch (edits[] vs flat) |
| #58575 | Cron --model flag does not override session model |
| #58560 | Browser tools AJV schema error |
| #58479 | Control UI exec approval never consumed |
| #58249 | Teams webhook broken (publicUrl removed) |
| #58256 | Browser server removed from gateway, command not found |
| #58221 | Chrome extension browser relay unavailable on macOS |
| #58107 | Multiple Feishu agents - only main reply delivered |
| #58087 | SecretRef headers sent as literal string |
| #58065 | Cron model override ignored |
| #57964 | Repeated characters collapse in dashboard/TUI |
| #57947 | Cron payload model override ignored |
| #57826 | Agent omits required tool parameters |
| #57753 | Anthropic SSE state machine fails to reset between tool-use loops |
| #57731 | Discord stale-socket drops causing delayed/duplicate messages |
| #57663 | MiniMax M2.7 408 timeouts not surfaced, 4+ min delays |

### Config Migration Hazards
- **#49666** - Telegram proxy config silently wiped on restart
- **#24008** - Unknown config keys silently wipe entire section on restart
- Browser control server removed - configs referencing `browser.server` need migration
- `hooks.internal.enabled` default changed to `true` - may affect setups relying on it being off
- `exec.host` default changed from `"sandbox"` to `"auto"`
- Exec approval system overhauled in 2026.3.31 - new approval flow can block all remote usage if not configured
- v2026.4.5 Windows ESM URL scheme handling is broadly broken - avoid upgrading Windows installs until patched

## Plugin/Extension/Config Known Issues

### Plugin Loading
- CJS dependencies fail under jiti ESM loader (#12854) - fundamental limitation
- Native bindings (sqlite3) broken under jiti (#36377)
- Symlinked plugin dirs silently skipped (#36754)
- pnpm global install rejects bundled plugin manifest paths (#28122)
- Plugin tools not inherited by subagent runtime scope (#50131)
- Custom plugin tools invisible to sandboxed sessions (#41757)
- Plugin subagent methods fail outside gateway request context (#58427)
- ACPX version pinning broken - always reverts to 0.1.16 (#52433)
- v2026.4.5: External plugins fail with normalizeAccountId is not a function (#62511)
- v2026.4.5: Maximum call stack size exceeded on plugin loading (#62372, #62320, #62302)
- v2026.4.5: memory-core plugin fails to load during onboard (#62347)
- v2026.4.5: Feishu bundled plugin missing register/activate export (#62277)

### Skills
- Sub-agents cannot see globally installed skills (#35272)
- Skills can silently fail to load with no error (#29122)
- `skills.allowBundled: []` semantics inverted (#21709)
- Agent becomes unresponsive during skill installation (#52073)
- Non-JSON SecretRef not supported in skills entries apiKey (#58319)
- WSL path resolution may show skill as disabled when it works (#58712)

### Channel Plugin Registration
- Slack ESM interop crash: "App is not a constructor" (#50441)
- Slack multi-workspace inbound DM replies missing (#58523)
- Slack socket-mode mentions dropped after stale-socket reconnect (#58268)
- Slack DM file attachments no longer download in 2026.4.5 (#62623)
- Slack inbound images broken in 2026.4.5 (#62318)
- WhatsApp outbound fails despite connected status (#58408)
- WhatsApp groupPolicy "allowlist" bypassed (#52763)
- Teams webhook broken in 2026.3.24+ (#58249)
- Feishu multi-group only main reply delivered (#58107)
- Feishu webhook verification fails with encryptKey (#58905)
- Feishu bundled plugin fails to load in 2026.4.5 (#62277)
- Google Chat space/group messages silently ignored (#58514)
- Matrix plugin syncs but never routes inbound messages (#51158)
- Signal group messages silently dropped on Node 24 (#51716)
- LINE webhook 404 with hooks.internal.enabled (#52729)
- Discord channel running=true while connected=false (#51190)
- Discord stale-socket aggressive drops (#57731)
- Telegram 401 Unauthorized in 2026.4.5 (#62690)
- Telegram approval messages broadcast to multiple users (#62576)
- Telegram non-main agents stop replying in multi-agent (#62324)

## Recently Closed / Fixed (removed from previous tracking)
- **#58931** - Matrix not sending intermediate messages since 2026.3.31 - CLOSED (fixed 2026-04-01, MiniMax-specific + Matrix blockStreaming added)
- **#58934** - Validation failure exec host=node cwd omitted - CLOSED (PR #58977)
- **#58723** - dangerouslyForceUnsafeInstall param not effective - CLOSED (PR #58879, confirmed fixed in 2026.4.1)
- **#58734** - Matrix bootstrap runtime broken in 2026.3.31 - CLOSED (fixed 2026-04-01)
- **#57956** - Anthropic token auth profiles broken in 2026.3.28 - CLOSED (Anthropic ended third-party subscription support April 4, 2026)
- **#57776** - Matrix device verification fails with mismatched_sas - CLOSED (fixed 2026-04-03)
- **#57746** - Telegram long messages truncated instead of split - CLOSED (fixed 2026-04-06)
- **#57618** - Mattermost channel config rejected after upgrade - CLOSED (fixed 2026-04-04)
- **#58347** - LiveSessionModelSwitchError defeats fallback chain - CLOSED (PR #60266, regression coverage added)
- **#58337** - MiniMax toolCall pre-text block completely dropped on Telegram - CLOSED (fixed 2026-04-04)
- **#58546** - Multi-agent routing session under wrong agent - CLOSED (fixed)
- **#58466** - Model switch blocks failover on overloaded_error - CLOSED (PR #58589)
- **#58409** - CRITICAL heartbeat session reset - CLOSED (PR #58605, #58447)
- **#58357** - HTTP API missing scope with --auth none - CLOSED (PR #58653)
- **#58456** - API key status "unknown" for some providers - CLOSED (PR #58503)
- **#58494** - Browser CDP stale cdpUrl preference - CLOSED (PR #58499)
- **#58217** - Missing scope: operator.write - CLOSED (PR #58653)
- **#52899** - Matrix Plugin API version mismatch - CLOSED
- **#58662** - allow-always behaves as allow-once - CLOSED (PR #58745)
- **#58620** - Gateway writes auth.token to config, triggering restart loop - CLOSED (PR #58678)
- **#58689** - All HTTP endpoints 500 after upgrade to 2026.3.31 - CLOSED
- **#58697** - Gateway panel Internal Server Error - CLOSED

## Recent Impactful PRs (last 30 days, since 2026-03-07)

| PR | Title | Area |
|---|---|---|
| #62658 | Require re-pairing for node reconnect command upgrades | Gateway |
| #62650 | feat(ios): improve gateway connection error ux | iOS |
| #62600 | fix: compaction after tool use abortion cause agent infinite loop calls | Agents |
| #62439 | fix(exec): detect cmd wrapper carriers | Exec |
| #62411 | fix(google): preserve Gemma 4 thinking-off semantics | Agents |
| #62401 | fix(exec): align node shell allowlist wrappers | Exec |
| #62383 | fix(allowlist): gate write commands behind owner check | Security |
| #62369 | fix(feishu): enforce workspace-only localRoots in docx upload | Feishu |
| #62357 | fix(fetch-guard): drop request body on cross-origin unsafe-method redirects | Security |
| #62355 | fix(browser): harden SSRF redirect guard against non-navigation document hops | Security |
| #62350 | fix(gateway): invalidate shared-token/password WS sessions on secret rotation | Gateway |
| #62316 | fix(docker): use built bundled plugins in runtime images | Docker |
| #62312 | fix(gateway): stop SSRF guard rejecting operator-configured proxy hostnames | Gateway |
| #62291 | fix: expand host-exec env blocklist for Java, Rust, and Cargo toolchains | Security |
| #62286 | fix(channels): keep bundled dist loads off native Jiti on Windows | Channels |
| #62275 | fix(memory): respect selected slot in dreaming config | Memory |
| #62264 | Add lightContext support for spawned subagents | Agents |
| #62263 | Fix Telegram doctor migration for multi-account configs | Telegram |
| #62251 | Cron: normalize jobId to id when loading jobs.json | Cron |
| #62244 | Fix: HTTP 404 classification for model fallback chain | Agents |
| #62239 | fix(slack): prevent undici dispatcher leak to globalThis.fetch causing media download failure | Slack |
| #62237 | Mistral: send reasoning_effort for mistral-small-latest | Agents |
| #62234 | Plugins: allowlist compat for capability provider fallback | Plugins |
| #62227 | feat(memory-core): ingest session transcripts into dreaming corpus | Memory |
| #62193 | feat(ollama): detect vision capability from /api/show and set image input | Ollama |
| #62179 | feat: expose prompt-cache runtime context to context engines | Agents |
| #62168 | Matrix: prompt invite auto-join during onboarding | Matrix |
| #62148 | chore: bump bundled acpx to 0.5.1 | ACPX |
| #62146 | feat(gateway): add compaction checkpoints | Gateway |
| #62136 | fix: openclaw allows normal reply text to carry media | Security |
| #62135 | Refactor: centralize native approval lifecycle assembly | Exec |
| #62132 | ACP: harden Discord recovery and reset flow | ACP/Discord |
| #62129 | feat: Add first-class infer CLI for inference workflows | CLI |
| #62111 | fix: lower trust background runtime output injected into system events | Security |
| #62082 | fix: QQ Bot extension media URLs not guarded | QQ Bot/Security |
| #62079 | fix: Mercurial/Rust/Make env vars not sanitized in host exec | Security |
| #62078 | fix: timed-out exec approval can proceed when strictInlineEval enabled | Security |
| #62068 | feat: add Arcee AI provider plugin | Plugins |
| #62050 | stabilize media live coverage and add shared harness | Media |
| #62023 | fix(browser): guard interaction-driven navigations | Security |
| #62003 | fix(hooks): mark wake hook events untrusted | Security |
| #62001 | Protect gateway exec approval config paths | Security |
| #61985 | fix: suppress commentary history leaks | Agents |
| #61968 | fix(agents): keep phaseless OpenAI WS text buffered until phase resolves | Agents |
| #61935 | fix(gateway-status): use local TLS probe targets with fingerprint | Gateway |
| #61933 | fix(command-queue): crash on SIGUSR1 restart after upgrade from pre-4.5 | Gateway |
| #61925 | fix: lazily recreate bound ACP sessions after reset | ACP |
| #61924 | fix(memory): surface warning when sqlite-vec unavailable during index | Memory |
| #61921 | fix(browser): retry remote CDP websocket readiness before failing | Browser |
| #61909 | fix: preserve code block indentation in normalizeDirectiveWhitespace | Agents |
| #61907 | fix(matrix): compact loose list HTML for consistent Element rendering | Matrix |
| #61903 | fix(agents): prevent ReDoS in interpreter heuristic regexes | Security |
| #61892 | plugins: add bundled webhooks TaskFlow bridge | Plugins |
| #61873 | fix: approval card buttons off-screen on mobile | UI |
| #61872 | fix: prevent Canvas/Copy icons from overlapping chat bubble text | UI |
| #61871 | fix: recognize api.grok.x.ai as xAI-native endpoint | Agents |
| #61869 | fix: detect ?token= and suggest #[REDACTED_SECRET] syntax | UI |
| #58929 | fix(ci): allow plugin npm preview without publish token | Plugins |
| #58918 | fix: hide raw provider errors from external chat replies | Agents |
| #58916 | fix: preserve anthropic thinking blocks on replay | Agents |
| #58904 | fix(agent): treat webchat exec approvals as native UI | Agents/Exec |
| #58900 | feat(gateway): make chat history max chars configurable | Gateway |
| #58896 | Security: require explicit opt-in for internal startup hooks | Security |
| #58895 | qqbot: require explicit allowlist for /bot-logs | Security |
| #58879 | fix(plugins): pass dangerouslyForceUnsafeInstall through archive | Plugins |
| #58873 | fix: preserve bundled channel plugin compat | Plugins |
| #58872 | fix(cron): avoid busy-wait drift for recurring main jobs | Cron |
| #58869 | fix(subagents): harden task-registry lifecycle writes | Tasks |
| #58860 | fix(exec): resume agent session after approval completion | Exec |
| #58846 | fix(compaction): add circuit breaker for retry loops | Agents |
| #58844 | fix(compaction): prevent splitting mid tool-call pairs | Agents |
| #58828 | feat(tasks): add chat-native task board | Tasks |
| #58825 | fix(gateway): move probe ahead of channel stages for health checks | Gateway |
| #58810 | fix(status): filter stale task rows from status cards | Tasks |
| #58799 | fix(line): resolve dist runtime contract path | LINE |
| #58792 | fix(exec): resolve remote approval regressions | Exec |
| #58782 | fix: restore bundled runtime dependency provisioning | Plugins |
| #58746 | fix: catch per-stage errors in HTTP pipeline to prevent cascade 500s | Gateway |
| #58716 | fix: re-throw LiveSessionModelSwitchError instead of swallowing in fallback | Agents |
| #58707 | fix: escalate to model fallback after rate-limit profile rotation cap | Agents |
| #58706 | feat(slack): default allowBots to true | Slack |
| #58705 | fix(plugins): repair bundled runtime deps within plugin install dirs | Plugins |
| #58688 | fix(discord): polyfill Client.registerListener for carbon >=0.14 compat | Discord |
| #58678 | fix: treat gateway.auth and gateway.controlUi as no-op in reload plan | Gateway |
| #58670 | fix(tasks): prevent synchronous task registry sweep from blocking event loop | Tasks |
| #58653 | fix: grant default operator scopes when gateway auth mode is none | Gateway |
| #58641 | Webchat: preserve rewritten stream snapshots | WebChat |
| #58625 | feat(auth): WHAM-aware Codex cooldown for multi-profile setups | Auth |
| #58614 | fix(acpx): fall back to PATH node for shebang wrappers | ACPX |
| #58610 | fix(acpx): harden codex and gemini ACP runtime flows | ACPX |
| #58609 | routing: support wildcard peer bindings (peer.id="*") for multi-agent | Routing |
| #58605 | fix(session): prevent heartbeat/cron/exec from triggering session reset | Sessions |
| #58604 | plugins: suppress provenance warning for allowlisted local plugins | Plugins |
| #58593 | fix(discord): stop media downloads from hanging | Discord |
| #58589 | fix: live session model switch no longer blocks failover | Agents |
| #58586 | fix(voice-call): clear connection timeout on successful STT connect | Voice |
| #58573 | fix(sandbox): resolve pinned fs helper python without PATH | Sandbox |
| #58562 | fix: differentiate overloaded vs rate-limit user-facing error messages | Agents |
| #58555 | fix: harden embedded text normalization | Agents |
| #58548 | feat: add agents.defaults.params for global default model params | Agents |
| #58521 | fix(tasks): make task-store writes atomic | Tasks |
| #58516 | refactor(tasks): add owner-key task access boundaries | Tasks |
| #58504 | feat(cron): add --tools flag for per-job tool allow-list | Cron |
| #58503 | fix(auth): add qwen-dashscope and anthropic-openai to known API key env vars | Auth |
| #58502 | fix(gateway): skip restart when config.patch has no actual changes | Gateway |
| #58499 | fix: CDP profiles prefer cdpPort over stale WebSocket cdpUrl | Browser |
| #58497 | feat: feishu comment event | Feishu |
| #58489 | fix: preserve Telegram topic routing in announce and delivery context | Telegram |
| #58474 | fix(session-status): infer custom runtime providers from config | Gateway |
| #58461 | fix(agents): correct model resolution priority in live-model-switch | Agents |
| #58447 | fix(core): prevent silent session reset during heartbeat runs | Sessions |
| #58446 | feat: add thinkingByChannel - per-channel thinking level overrides | Agents |
| #58403 | feat(openresponses): add per-request skills override to /v1/responses | API |
| #58400 | fix: move bootstrap session grammar into plugin-owned session-key surfaces | Plugins |
| #58382 | fix(pairing): restore qr bootstrap onboarding handoff | Pairing |
| #58379 | fix(gateway): prevent session death loop on overloaded fallback | Gateway |
| #58376 | fix(matrix): filter fetched room context by sender allowlist | Matrix |
| #58375 | Plugins: preserve prompt build system prompt precedence | Plugins |
| #58371 | Gateway: reject mixed trusted-proxy token config | Gateway |
| #58369 | Exec approvals: reject shell init-file script matches | Security |
| #58368 | fix(browser): close SSRF bypass - redirect guard and follow-up action gates | Security |
| #58362 | fix(android): resolve TLS handshake failure on Android 15 | Android |
| #58350 | fix(zalo): isolate replay dedupe across webhook paths | Zalo |
| #58346 | fix(acp): use semantic approval classes | ACP |
| #58341 | fix: skip stale-socket restart when Discord channel is still connected | Discord |
| #58336 | ClawFlow: add runtime substrate | Tasks |
| #58325 | fix(sessions): clear model override on /reset and /new | Sessions |

## Dev Gotchas (synthesized)

- **v2026.4.5 is extremely unstable on Windows** - ERR_UNSUPPORTED_ESM_URL_SCHEME (#62564, #62502, #62374, #62408), Maximum call stack size exceeded (#62372, #62320, #62302, #62410, #62430), plugins fail to load (#62511, #62347, #62277). Avoid upgrading Windows installs to 2026.4.5 until a hotfix release. Multiple critical regressions affect onboarding, CLI, and plugin loading.
- **v2026.4.5 Slack file handling broken** - Both DM file attachments (#62623) and inbound images (#62318) are broken. Root cause is likely the undici dispatcher leak fix (#62239) or speech-core runtime-api.js resolution failure. Images shared in Slack are not forwarded to the model.
- **v2026.4.5 CLI hangs and crashes** - CLI local commands hang indefinitely consuming 90%+ CPU (#62400), unknown subcommands cause infinite loops (#62356), `openclaw devices` is unresponsive (#62326). These may share a root cause with the Maximum call stack size issues.
- **v2026.4.5 memory-core / dreaming config issues** - memory-core plugin fails to load during onboard (#62347), cannot create new agent after enabling dreaming config (#62541), Ollama embedding broke (#62282). The dreaming feature appears unstable.
- **v2026.4.5 Coding Agent regression** - Coding Agent never completes anything, only produces vague status updates (#62505). Worked in 2026.4.2.
- **v2026.3.31 exec approval overhaul** - The exec approval system was significantly reworked. Expect excessive approval prompts; allow-always may behave as allow-once. Multiple hotfixes landed (PRs #58792, #58860, #58745). Remote usage on Windows may be blocked without workaround. Additionally, timed-out approvals could bypass strictInlineEval (fixed in PR #62078).
- **v2026.3.31 pre-compaction memory flush** (#58956) - Memory flush leaks into chat UI and blocks the user's active turn. Still open.
- **v2026.3.28 removes browser control server from gateway** - "openclaw browser start" is gone (#58256, #58221). Chrome extension browser relay stops working. This is a structural change, not a bug.
- **Cron model override is still broken across 4 issues** (#58575, #58065, #57947, #58927) - --model flag, payload.model, and agentTurn model override all fail; always uses agent default. PRs #58992 and #58998 were submitted but issues remain open.
- **Anthropic subscription support ended April 4, 2026** - Anthropic ended third-party harness subscription access. Users must enable pay-as-you-go "extra usage" to continue using Anthropic models through OpenClaw. One-time credit available until April 17. (#57956)
- **Model failover improvements landed but gaps remain** - PRs #58589, #58716, #58707, #60266 fixed several failover issues (overloaded_error, rate limits, LiveSessionModelSwitchError). But #58442 (Coding Plan 429 infinite loop) remains open. PR #62244 adds HTTP 404 classification to the fallback chain. Compaction abort now correctly handles tool use (#62600).
- **Compaction circuit breaker added** (PR #58846) - Prevents infinite retry loops during compaction. Also prevents splitting mid tool-call pairs (#58844). Compaction checkpoints added (PR #62146) for better recovery.
- **Plugin installs now fail closed on dangerous code** - use [REDACTED_SECRET] to override. Plugin source scan failures also block install. The param was broken in 2026.3.31 (#58723) but fixed in PR #58879 (confirmed working in 2026.4.1).
- **Massive security hardening wave** - Multiple PRs since April 1: SSRF redirect guard hardened for browser (#62355, #62023, #62312), exec env blocklist expanded for Java/Rust/Cargo/Mercurial/Make (#62291, #62079), timed-out exec approvals fixed (#62078), wake hook events marked untrusted (#62003), exec approval config paths protected (#62001), reply text media path sanitized (#62136), QQ Bot media URLs guarded (#62082), cross-origin redirect body dropped (#62357), ReDoS prevented in interpreter heuristic (#61903).
- **Teams webhook broken since 2026.3.24** (#58249) - publicUrl removal breaks JWT validation. Still open.
- **hooks.internal.enabled now defaults to true** - bundled hooks load on fresh installs. Old configs relying on false default may see new behavior. PR #58896 now requires explicit opt-in for internal startup hooks.
- **exec.host defaults to "auto" instead of "sandbox"** - may change exec routing behavior for existing setups.
- **New features: infer CLI, TaskFlow webhooks, dreaming** - `openclaw infer` CLI for provider-backed inference (PR #62129), bundled webhooks TaskFlow bridge (PR #61892), memory-core dreaming corpus ingestion (PR #62227). Dreaming is still unstable on 2026.4.5.
- **New features: compaction checkpoints, lightContext subagents** - Compaction checkpoints for recovery (PR #62146), lightContext support for spawned subagents (PR #62264).
- **Prompt cache invalidated every turn** (#36520) - still open. PR #62179 exposes prompt-cache runtime context to context engines.
- **Shared process model = no plugin isolation** (#12517, #12518) - all plugins share one Node.js process.
- **Sub-agents can't see globally installed skills** (#35272) - still no fix.
- **Config keys still silently wipe sections** (#24008) - validate config carefully across upgrades.
- **Google provider usageMetadata not mapped** (#51743) - all Gemini calls record 0 tokens. PR #62193 adds Ollama vision capability detection.
- **SecretRef-backed headers resolve to literal string** (#58087) - regression affecting model providers using SecretRef for custom headers.
- **Long sessions break model inference** (#58553) - 600+ message sessions get "invalid function call parameters".
- **Edit tool schema mismatch** (#58599) - edits[] array in schema vs flat oldText/newText in implementation.
- **CJK character handling** (#58932) - normalizeHyphenSlug strips CJK characters, breaking non-Latin group display names.
- **Security: config.openFile command injection** (#57827) - OPENCLAW_CONFIG_PATH vulnerable in 2026.3.28.
- **Wildcard peer bindings** (PR #58609) - peer.id="*" now supported for multi-agent routing.
- **agents.defaults.params** (PR #58548) - New config key for global default model params.
- **thinkingByChannel** (PR #58446) - Per-channel thinking level overrides now available.
- **Cross-session leakage reported** (#62580) - Response contamination between sessions. Needs investigation.
- **WhatsApp allowFrom security concern** (#62339) - Unknown phone number appeared in allowFrom without user authorization. Cron job sent messages to unknown number. Potential security issue.


path: references/models-providers.md
content: # OpenClaw Models & Providers

## Models Config

```json
{
  "models": {
    "mode": "merge",
    "providers": {
      "provider-name": {
        "type": "openai-compatible",
        "baseUrl": "https://api.provider.com/v1",
        "apiKey": "${PROVIDER_API_KEY}",
        "models": {
          "model-id": {
            "name": "Display Name",
            "contextWindow": 128000,
            "reasoning": true,
            "costPerMillionInput": 3.0,
            "costPerMillionOutput": 15.0
          }
        }
      }
    },
    "defaults": {
      "model": "provider-name/model-id"
    }
  }
}
```

## Mode: merge vs replace

- **`merge`** (default): provider models added alongside 700+ built-in models
- **`replace`**: hides ALL built-in models, only shows configured providers

Use `replace` for controlled environments (e.g. agentbox VMs) where you want to expose only specific models.

## Provider Types

- `openai-compatible` - any OpenAI-compatible API
- `anthropic` - Anthropic API
- `google` - Google AI (Gemini)
- `google-vertex` - Google Cloud Vertex AI
- `x402` - pay-per-use via HTTP 402 micropayments

## Model APIs

Supported `api` values for model definitions:

| API | Description |
|-----|-------------|
| `openai-completions` | OpenAI Chat Completions API |
| `openai-responses` | OpenAI Responses API (/v1/responses) |
| `openai-codex-responses` | OpenAI Codex Responses API (new) |
| `anthropic-messages` | Anthropic Messages API (default for Anthropic) |
| `google-generative-ai` | Google Generative AI API |
| `github-copilot` | GitHub Copilot API |
| `bedrock-converse-stream` | AWS Bedrock Converse Stream |
| `azure-openai-responses` | Azure OpenAI Responses API |
| `ollama` | Ollama local inference |

## Model Aliases

Built-in aliases resolved in `src/config/defaults.ts`:

| Alias | Resolves to |
|-------|-------------|
| `opus` | `anthropic/claude-opus-4-6` |
| `sonnet` | `anthropic/claude-sonnet-4-6` |
| `gpt` | `openai/gpt-5.4` |
| `gpt-mini` | `openai/gpt-5.4-mini` |
| `gpt-nano` | `openai/gpt-5.4-nano` |
| `gemini` | `google/gemini-3.1-pro-preview` |
| `gemini-flash` | `google/gemini-3-flash-preview` |
| `gemini-flash-lite` | `google/gemini-3.1-flash-lite-preview` |

Anthropic within-provider aliases also supported (e.g. `opus-4.6` -> `claude-opus-4-6`, `sonnet-4.5` -> `claude-sonnet-4-5`).

## Model Compat Config

Extended `ModelCompatConfig` for provider quirks:

```typescript
type ModelCompatConfig = {
  requiresThinkingAsText?: boolean;
  requiresStringContent?: boolean;
  toolSchemaProfile?: string;
  unsupportedToolSchemaKeywords?: string[];
  nativeWebSearchTool?: boolean;
  toolCallArgumentsEncoding?: string;
  requiresMistralToolIds?: boolean;
  requiresOpenAiAnthropicToolPayload?: boolean;
  // ... other compat flags
};
```

## Model Definition Extensions

`ModelDefinitionConfig` now supports:

- `contextTokens?: number` - optional effective runtime cap for compaction/session budgeting. Keeps the provider's native `contextWindow` intact while preferring a smaller practical window.

`ModelProviderConfig` now supports:

- `request?: ConfiguredModelProviderRequest` - optional transport request overrides at the provider level.

## Discovery Toggle Config

Legacy discovery configs are deprecated but still in the type surface for migration compat:

- `bedrockDiscovery` - AWS Bedrock model discovery (existing, with region/filter/refresh options)
- `copilotDiscovery` - GitHub Copilot discovery (simple `{ enabled?: boolean }`)
- `huggingfaceDiscovery` - HuggingFace discovery (simple `{ enabled?: boolean }`)
- `ollamaDiscovery` - Ollama discovery (simple `{ enabled?: boolean }`)

## x402 Provider Pattern

Used in agentbox for pay-per-use inference:

```json
{
  "providers": {
    "agentbox": {
      "type": "x402",
      "baseUrl": "https://instance.agentbox.ai/v1",
      "models": {
        "claude-sonnet-4-20250514": {
          "name": "Claude Sonnet 4",
          "contextWindow": 200000,
          "reasoning": false,
          "costPerMillionInput": 3.0,
          "costPerMillionOutput": 15.0
        }
      }
    }
  }
}
```

x402 providers work with the `openclaw-x402` plugin that intercepts 402 responses and auto-signs Solana USDC payments.

## Model Studio (DashScope/Qwen) Provider

The `modelstudio` extension provides Qwen models via Alibaba Cloud Model Studio with four auth methods:

| Method | Endpoint | Type |
|--------|----------|------|
| `standard-api-key-cn` | `dashscope.aliyuncs.com/compatible-mode/v1` | Pay-as-you-go (China) |
| `standard-api-key` | `dashscope-intl.aliyuncs.com/compatible-mode/v1` | Pay-as-you-go (Global/Intl) |
| `api-key-cn` | `coding.dashscope.aliyuncs.com/v1` | Coding Plan subscription (China) |
| `api-key` | `coding-intl.dashscope.aliyuncs.com/v1` | Coding Plan subscription (Global/Intl) |

Group label: "Qwen (Alibaba Cloud Model Studio)". Default model: `modelstudio/qwen3.5-plus`. All four base URLs are registered as native Model Studio URLs (streaming usage compat applied). Env var: `MODELSTUDIO_API_KEY`. Coding Plan models include `qwen3.5-plus`, `glm-5`, `kimi-k2.5`, `MiniMax-M2.5`.

## StepFun Provider

The `stepfun` extension provides Step AI models with two surfaces:

| Surface | CN Base URL | Intl Base URL |
|---------|-------------|---------------|
| `standard` | `api.stepfun.com/v1` | `api.stepfun.ai/v1` |
| `plan` | `api.stepfun.com/step_plan/v1` | `api.stepfun.ai/step_plan/v1` |

Standard models: `step-3.5-flash`. Plan models: `step-3.5-flash`, `step-3.5-flash-2603`. Region auto-detected from API key endpoint. Env var: `STEPFUN_API_KEY`.

## Anthropic Plugin Architecture

The Anthropic provider is implemented as a bundled plugin (`extensions/anthropic/`). Stream wrappers (beta headers, service tier, fast mode) live in `extensions/anthropic/stream-wrappers.ts` and are composed via the `wrapStreamFn` provider hook.

Key stream wrappers:
- [REDACTED_SECRET] - injects `anthropic-beta` header (context-1m, fine-grained-tool-streaming, interleaved-thinking, OAuth betas)
- `createAnthropicServiceTierWrapper` - sets `service_tier` (`auto`|`standard_only`) on anthropic-messages requests
- `createAnthropicFastModeWrapper` - resolves `fastMode`/`fast_mode` extra param to `service_tier`

Context-1M beta (`context-1m-2025-08-07`) is auto-added for `claude-opus-4`/`claude-sonnet-4` prefixes when `extraParams.context1m === true`. Skipped for OAuth auth (Anthropic rejects it with OAuth tokens).

Default Anthropic betas: [REDACTED_SECRET], `interleaved-thinking-2025-05-14`. OAuth adds: `claude-code-20250219`, `oauth-2025-04-20`.

Thinking level defaults to `"adaptive"` for Opus 4.6 and Sonnet 4.6 models when the model is explicitly configured.

## SearXNG Web Search Provider

Bundled plugin (`extensions/searxng/`) providing self-hosted meta-search via SearXNG. Registered via `api.registerWebSearchProvider()`.

Config path: `plugins.entries.searxng.config.webSearch.baseUrl`. Env var: `SEARXNG_BASE_URL`. No API key required - only a base URL to a SearXNG instance.

Tool parameters: `query` (required), `count` (1-10), `categories` (comma-separated: general, news, science, etc.), `language` (e.g. en, de, fr).

Plugin manifest declares `contracts.webSearchProviders: ["searxng"]`. Auto-detect order: 200 (low priority - prefers other configured web search providers first).

## Provider Registration via Plugin

```typescript
// Plugin can register as auth provider
api.registerProviderAuth({
  id: "my-provider",
  authenticate: async (req) => {
    req.headers.set("Authorization", `Bearer ${apiKey}`);
    return req;
  }
});
```

Note: `registerProvider()` is auth-only. It does NOT populate the model catalog. Model metadata (names, context windows, costs) must be defined in config `models.providers`. Plugin-based missing-auth errors now include descriptive error messages.

### Provider Plugin Hooks

| Hook | Purpose |
|------|---------|
| `normalizeModelId` | Provider-owned model-id alias cleanup before resolution |
| `normalizeConfig` | Transform provider config before validation |
| `normalizeTransport` | Adjust request transport options per-provider |
| `resolveConfigApiKey` | Custom API key resolution from config/env |
| `createStreamFn` | Factory for provider-specific streaming implementation |
| `createEmbeddingProvider` | Memory embeddings via provider plugin (for memory plugins) |
| `wrapStreamFn` | Compose provider-specific stream wrappers (beta headers, service tier, etc.) |
| `buildReplayPolicy` | Provider-owned replay/compaction transcript policy |
| `sanitizeReplayHistory` | Provider-specific replay history rewrites after core cleanup |
| `validateReplayTurns` | Final replay-turn validation for strict turn ordering |
| `normalizeToolSchemas` | Rewrite tool schema keywords unsupported by provider transport |
| `inspectToolSchemas` | Inspect tool schemas without mutation |
| `resolveReasoningOutputMode` | Return `"native"` or `"tagged"` for reasoning block handling |
| `resolveTransportTurnState` | Provider transport turn state for replay |
| `resolveWebSocketSessionPolicy` | WebSocket session policy resolution |
| `matchesContextOverflowError` | Detect provider-specific context overflow errors |
| `classifyFailoverReason` | Classify errors into `FailoverReason` for fallback decisions |
| `resolveSystemPromptContribution` | Provider-owned system prompt contributions |
| `applyConfigDefaults` | Apply provider-specific config defaults (e.g. context pruning) |
| `resolveExternalAuthProfiles` | Resolve external auth profiles from provider plugins |
| `shouldDeferSyntheticProfileAuth` | Defer synthetic profile auth to env/config credentials |

### Bundled Provider Policy Surface

A new `provider-public-artifacts.ts` module provides a `BundledProviderPolicySurface` that loads `provider-policy-api.js` from bundled plugins. This surface is checked before plugin hooks for `normalizeConfig`, `applyConfigDefaults`, and `resolveConfigApiKey` - giving bundled plugins fast-path policy application without full plugin activation.

### Provider Plugin Lookup Key

`resolveProviderPluginLookupKey()` resolves the runtime plugin key used for hook dispatch. When a provider's `api` field is set to a non-generic API (anything not in `openai-completions`, `openai-responses`, `anthropic-messages`, `google-generative-ai`), the API value is used as the lookup key instead of the provider key. This lets providers using specialized APIs (like `bedrock-converse-stream` or `ollama`) route to the correct plugin.

## Plugin Auto-Enable for Provider Auth

Plugins with `autoEnableWhenConfiguredProviders` in their manifest are automatically enabled when the corresponding provider has auth configured (auth profiles, model providers, or model refs). For example, the MiniMax plugin declares `autoEnableWhenConfiguredProviders: ["minimax", "minimax-portal"]` and is auto-enabled when a MiniMax API key auth route is configured - even without an explicit `plugins.entries.minimax.enabled: true`.

Detection checks: auth profiles with matching provider, `models.providers` keys, and provider prefixes in model refs (e.g. `minimax/MiniMax-M2.7` in agent defaults).

## Plugin Model Auth API

Context-engine plugins can resolve model credentials via `runtime.modelAuth`:

```typescript
// Resolve auth for a specific model
const auth = await runtime.modelAuth.getApiKeyForModel({ model, cfg });

// Resolve auth for a provider by name
const auth = await runtime.modelAuth.resolveApiKeyForProvider({ provider: "anthropic", cfg });

// auth: { apiKey, profileId?, source, mode: "api-key"|"oauth"|"token"|"aws-sdk" }
```

This is the safe surface for plugins - it strips internal overrides (agentDir/store) that raw `model-auth` helpers expose. Plugins should never import `model-auth` directly.

## Local Provider Auth

`CUSTOM_LOCAL_AUTH_MARKER` for local servers (llama.cpp, vLLM, LocalAI, Ollama) that don't need authentication. These providers skip credential resolution entirely.

## Provider ID Normalization

`normalizeProviderIdForAuth` handles provider alias mappings and coding-plan variants. Provider IDs are normalized before credential lookup to handle cases like `openai-codex` -> `openai`.

Model ID normalization is split into static and runtime layers:
- `model-ref-shared.ts` - static normalization: Anthropic short aliases (`opus-4.6` -> `claude-opus-4-6`), Google preview IDs, xAI, HuggingFace prefix stripping, OpenRouter/Vercel AI Gateway prefixing
- `model-selection-normalize.ts` - runtime normalization: chains static normalization with plugin `normalizeModelId` hooks

## Provider Credential Resolution

`resolveApiKeyForProvider` now supports:
- `lockedProfile?: boolean` - treats profile as user-locked, prevents silent override by env/config credentials
- `credentialPrecedence?: "profile-first" | "env-first"` - controls whether env vars or auth profiles are tried first

Synthetic profile deferral: when a resolved key is a provider-owned synthetic profile marker and the caller hasn't locked the profile, auth resolution falls through to env/config credentials so real credentials take precedence over placeholders.

Explicit config API key preference: when `authHeader: true` is set on a provider config and the auth override resolves to `api-key`, the explicit config API key is preferred before iterating auth profiles.

`applyAuthHeaderOverride()` injects `Authorization: Bearer <apiKey>` headers for providers that set `authHeader: true`, so downstream SDKs (e.g. `@google/genai`) send credentials via standard HTTP Authorization rather than vendor-specific headers.

## Prompt Cache Stripping

`prompt_cache_key` and `prompt_cache_retention` fields are stripped from requests targeting non-OpenAI Responses API endpoints. This prevents 400 errors when models/providers don't support prompt caching metadata.

## Agent Model Defaults

```json
{
  "agents": {
    "defaults": {
      "model": "provider/model-id",
      "timeout": 120000,
      "compaction": { "enabled": true, "threshold": 0.8 },
      "contextPruning": { "enabled": true }
    }
  }
}
```

Per-agent fallback model override is supported via `agents.entries.<id>.model`.

Context pruning defaults are now applied via `applyProviderConfigDefaultsWithPlugin` dispatching to the Anthropic bundled plugin, rather than inline logic in `defaults.ts`.

## Model Selection Improvements

Model selection (`model-selection.ts`) refactored into three modules:
- `model-ref-shared.ts` - shared static `modelKey()`, `parseStaticModelRef()`, per-provider model ID normalization
- `model-selection-normalize.ts` - `parseModelRef()`, `normalizeModelRef()` with plugin hook support
- `model-selection-display.ts` - `resolveModelDisplayRef()`, `resolveModelDisplayName()` for UI rendering

Provider inference: when a model string has no provider prefix, `inferUniqueProviderFromConfiguredModels` checks the configured allowlist before defaulting to the session's current provider. This prevents provider prefix drift when switching models across providers.

Thinking default for 4.6 models: Anthropic Opus 4.6 and Sonnet 4.6 models default to `"adaptive"` thinking when the model is explicitly configured (in model config, primary selection, or per-model settings).

Synthetic catalog entries from allowlisted models now inherit `contextWindow`, `reasoning`, and `input` metadata from the provider's configured model definition, instead of using bare defaults.

## Model Fallback & Transient Errors

Transient HTTP status codes that trigger model fallback:
`499`, `500`, `502`, `503`, `504`, `521`, `522`, `523`, `524`, `529`

HTTP 499 (Client Closed Request) is classified as `timeout` for fallback purposes (or `overloaded` if the response body contains an `overloaded_error` payload). This handles proxies/load balancers that return 499 when upstream is slow.

### Plugin-Driven Failover Classification

Provider plugins can now contribute to failover classification via two new hooks:
- `matchesContextOverflowError` - detect provider-specific context overflow errors (checked across all plugins)
- `classifyFailoverReason` - classify errors into `FailoverReason` values (first plugin match wins)

These are consulted in addition to the built-in classifier.

### Rate-Limit Profile Rotation Cap

When a model hits rate-limit errors, the runner rotates through auth profiles. After exceeding `rateLimitProfileRotationLimit` rotations (configurable), the runner escalates to **cross-provider model fallback** instead of spinning forever across profiles that share the same model quota. This mirrors the existing overload rotation cap behavior.

Without this, per-model quota exhaustion (e.g. Anthropic Sonnet-only rate limits) causes infinite profile rotation when all profiles share the same underlying quota. The escalation throws a `FailoverError` with reason `rate_limit`, triggering the standard model fallback chain.

### Fallback Classifier Priority Order

`classifyFailoverReason()` evaluates in this order (first match wins):

1. `overloaded` - explicit overload signals
2. `timeout` - transient HTTP status codes (5xx), 529 -> `overloaded`
3. `billing` - billing/payment errors
4. `auth_permanent` - permanent auth failures
5. `auth` - transient auth errors
6. `timeout` - JSON `api_error` with transient signal (see below)
7. `format` - Cloud Code Assist format errors
8. `timeout` - generic timeout patterns

### Generalized `api_error` Detection

JSON `api_error` payloads (`"type":"api_error"`) are only classified as transient (`timeout`) when the message matches a transient signal pattern. This handles non-standard providers (e.g. MiniMax returning `"unknown error, 520 (1000)"`).

Transient signals: `internal server error`, `overload`, `temporarily unavailable`, `service unavailable`, `unknown error`, `server error`, `bad gateway`, `gateway timeout`, `upstream error`, `backend error`, `try again later`, `temporarily.*unable`.

Billing/auth errors inside `api_error` payloads are excluded from transient classification - they fall through to their specific classifiers.

### Failover Reasons

Full `FailoverReason` union: `auth`, `auth_permanent`, `format`, `rate_limit`, `overloaded`, `billing`, `timeout`, `model_not_found`, `session_expired`, `unknown`.

`session_expired` (HTTP 410 Gone) handles expired/deleted upstream sessions - not retried via profile rotation.

## Auth Profile Cooldowns

Failure reasons (priority order): `auth_permanent`, `auth`, `billing`, `format`, `model_not_found`, `overloaded`, `timeout`, `rate_limit`, `unknown`.

Cooldown backoff: 1min, 5min, 25min, max 1 hour (exponential). Billing/auth_permanent use separate `disabledUntil` with configurable backoff (default 5h base, 24h max, doubling).

**Cooldown expiry resets error counters.** When `cooldownUntil`/`disabledUntil` expires, `errorCount` and `failureCounts` are cleared so the profile gets a fresh backoff window. Without this, stale error counts from expired cooldowns cause the next transient failure to immediately escalate to a much longer cooldown.

**Single-provider billing probes.** When only one provider is configured (no fallback chain), billing cooldowns are probed on the standard 30s throttle so the setup can recover if the user fixes their balance - without requiring a restart. Multi-provider setups only probe near cooldown expiry so the fallback chain stays preferred.

### Cooldown Probe Policy by Reason

| Reason | Allow cooldown probe | Use transient probe slot | Preserve transient probe slot |
|--------|---------------------|--------------------------|-------------------------------|
| `rate_limit` | yes | yes | no |
| `overloaded` | yes | yes | no |
| `billing` | yes | no | no |
| `unknown` | yes | yes | no |
| `model_not_found` | no | no | yes |
| `format` | no | no | yes |
| `auth` / `auth_permanent` | no | no | yes |
| `session_expired` | no | no | yes |

## Error Sanitization for Chat Replies

Raw provider error payloads are never shown directly to users. `formatAssistantErrorText()` rewrites errors into safe user-facing messages:

- Context overflow -> suggests `/reset` or larger-context model
- Reasoning constraint -> suggests `/think minimal`
- Invalid streaming order -> generic retry message
- Role ordering conflicts -> retry + `/new` suggestion
- Rate-limit/overload -> transient retry message
- Transport errors (ECONNRESET, ETIMEDOUT, etc.) -> connectivity message
- Billing errors -> provider-specific billing message
- Raw HTTP/JSON API payloads -> `formatRawAssistantErrorForUi()` extracts status + message
- Long unhandled errors -> truncated to 600 chars

`sanitizeUserFacingText()` also catches provider error payloads that leak into non-error stream chunks (e.g. Codex error prefixes) and rewrites them.

## Provider Replay Policy

Provider plugins now own replay/compaction transcript policy via `buildReplayPolicy` hook. The `ProviderReplayPolicy` type controls:

- `sanitizeMode` - `"full"` or `"images-only"`
- `sanitizeToolCallIds` / `toolCallIdMode` - strict tool-call ID regeneration (`"strict"` or `"strict9"`)
- `preserveSignatures` - preserve byte-for-byte thinking block signatures
- `dropThinkingBlocks` - strip thinking blocks from prior turns
- `repairToolUseResultPairing` - fix orphaned tool use/result blocks
- `applyAssistantFirstOrderingFix` / `validateGeminiTurns` / `validateAnthropicTurns` - turn ordering fixes
- `allowSyntheticToolResults` - permit synthetic tool results in replay

Helper builders in `provider-replay-helpers.ts`:
- `buildOpenAICompatibleReplayPolicy()` - builds policy for OpenAI-compatible APIs (completions/responses/codex)
- `buildStrictAnthropicReplayPolicy()` - strict Anthropic replay with signature preservation
- `buildAnthropicReplayPolicyForModel()` - model-aware Anthropic policy (preserves thinking blocks for 4.5+ models)
- `shouldPreserveThinkingBlocks()` - returns true for Claude 4.5+ models (Opus 4.5+, Sonnet 4.5+, Haiku 4.5+) where dropping thinking blocks breaks prompt cache prefix matching

Additional replay hooks: `sanitizeReplayHistory` (post-core cleanup rewrites), `validateReplayTurns` (strict turn ordering validation).

## Anthropic Thinking Replay Preservation

Anthropic Claude endpoints can reject replayed `thinking` blocks unless original signatures are preserved byte-for-byte. The replay policy handles this per-provider:

- `preserveSignatures` in replay policy determines whether the provider preserves thinking block signatures
- `dropThinkingBlocks` policy strips thinking blocks at send-time for providers where replay would fail
- Claude 4.5+ models (Opus 4.5/4.6, Sonnet 4.5/4.6, Haiku 4.5) preserve thinking blocks natively - dropping them breaks prompt cache prefix matching
- Pre-4.5 models (claude-3-7-sonnet, claude-3-5-sonnet) require dropping thinking blocks

## Default Context Windows

Claude Opus/Sonnet 4.6 default context window updated to 1M tokens (PR #49941).

## Model Selection Priority

1. Agent-specific model (in `agents.entries.<id>.model`)
2. Agent defaults (`agents.defaults.model`)
3. Session-level override (via chat command)
4. Built-in default (Claude Sonnet)

## Plugin Activation State

Plugin enablement uses `resolveEffectivePluginActivationState()` (renamed from `resolveEffectiveEnableState()`), returning `.activated` (previously `.enabled`). The `enabledByDefault` flag from plugin manifests is now passed through to the activation resolver. Provider discovery uses `resolveDiscoveredProviderPluginIds()` which returns all plugins with registered providers (regardless of activation state), while `resolveEnabledProviderPluginIds()` filters to only activated plugins.

## Provider Plugin Loading

Provider plugin loading (`providers.runtime.ts`) refactored with a two-mode architecture:
- **runtime mode** (default): uses `resolveRuntimePluginRegistry()` with the shared plugin registry, returns `[]` if a registry load is already in-flight (prevents re-entrant plugin activation)
- **setup mode**: uses `loadOpenClawPlugins()` directly with `resolveDiscoveredProviderPluginIds()` for initial setup/discovery flows

Plugin resolution now supports `providerRefs` and `modelRefs` parameters for targeted plugin loading - only the plugins that own the specified providers/models are loaded, with their IDs auto-activated in the runtime config.


path: references/plugin-system.md
content: # OpenClaw Plugin System

## Discovery Sources

Plugins are discovered from (in order):
1. **Config load paths** - `plugins.load.paths[]` array
2. **Workspace extensions** - per-agent workspace dirs
3. **Bundled plugins** - built into OpenClaw core
4. **Global plugins dir** - `~/.openclaw/plugins/`

Key files: `src/plugins/discovery.ts`, `src/plugins/loader.ts`

## Plugin Manifest (`openclaw.plugin.json`)

```json
{
  "id": "plugin-id",
  "name": "Display Name",
  "description": "What it does",
  "kind": "memory",
  "channels": ["channel-id"],
  "providers": ["provider-id"],
  "autoEnableWhenConfiguredProviders": ["provider-id"],
  "providerAuthEnvVars": { "provider-id": ["PROVIDER_API_KEY"] },
  "skills": ["skill-id"],
  "configSchema": { "type": "object", "properties": { ... } },
  "uiHints": { "token": { "label": "API Token", "sensitive": true } },
  "contracts": {
    "webSearchProviders": ["provider-id"],
    "mediaUnderstandingProviders": ["provider-id"],
    "imageGenerationProviders": ["provider-id"],
    "videoGenerationProviders": ["provider-id"],
    "musicGenerationProviders": ["provider-id"],
    "webFetchProviders": ["provider-id"],
    "speechProviders": ["provider-id"],
    "tools": ["tool-id"]
  }
}
```

- `kind`: optional, set `"memory"` for memory backend plugins, or array `["memory", "context-engine"]` for multi-kind plugins (PR #57507)
- `channels`/`providers`: declare capabilities for UI/discovery
- `autoEnableWhenConfiguredProviders`: provider ids that trigger auto-enable when referenced in auth profiles, provider config, or model refs (see "Auto-Enable for API Key Auth" below)
- `providerAuthEnvVars`: cheap env-var lookup for provider auth without booting plugin runtime. Map of provider id to env var names (e.g. minimax manifest maps `"minimax" -> ["MINIMAX_API_KEY"]`)
- `contracts`: static capability ownership snapshot (`PluginManifestContracts`) for manifest-driven discovery and compat wiring without importing plugin runtime. Optional arrays: `speechProviders`, `mediaUnderstandingProviders`, `imageGenerationProviders`, `videoGenerationProviders` (NEW), `musicGenerationProviders` (NEW), `webFetchProviders` (NEW), `webSearchProviders`, `tools`
- `providerAuthChoices`: array of auth choice objects for provider onboarding UI. Fields: `provider`, `method`, `choiceId`, `choiceLabel`, `groupId`, `groupLabel`, `optionKey`, `cliFlag`, `cliOption`, `cliDescription`, `onboardingScopes`
- `configSchema`: validated against plugin's `entries.<id>.config`
- `uiHints`: drive config UI (labels, sensitive masking)

> **Deprecated**: top-level `speechProviders`, `mediaUnderstandingProviders`, `imageGenerationProviders` in the manifest are deprecated. Move them under `contracts`. Run `openclaw doctor --fix` to auto-migrate.

## Package.json Convention

```json
{
  "name": "@openclaw/plugin-name",
  "type": "module",
  "openclaw": {
    "extensions": ["./index.ts", "./other-entry.ts"]
  }
}
```

The `openclaw.extensions` array lists entry points. Each must export a default plugin definition.

## Plugin Registration Pattern

```typescript
import type { OpenClawPluginApi, OpenClawPluginDefinition } from "openclaw/plugin-sdk";

const plugin: OpenClawPluginDefinition = {
  id: "my-plugin",
  name: "My Plugin",
  configSchema: myZodSchema,
  register(api: OpenClawPluginApi) {
    api.registerChannel({ plugin: channelImpl });
    api.registerTool(toolDef);                 // singular, supports factory pattern
    api.registerCommand(commandDef);           // custom slash commands (bypass LLM)
    api.on("before_agent_start", handler);     // typed hook with priority
    api.registerHttpRoute(routeDef);           // HTTP endpoint
    api.registerGatewayMethod("method", handler); // gateway protocol method
    api.registerContextEngine("id", factory);  // exclusive slot for context engine
    api.registerSpeechProvider(speechImpl);    // TTS/STT provider
    api.registerMediaUnderstandingProvider(muImpl);  // media understanding
    api.registerImageGenerationProvider(igImpl);     // image generation
    api.registerWebSearchProvider(wsImpl);           // web search
    api.registerInteractiveHandler(handler);         // interactive message handler
    api.onConversationBindingResolved(handler);      // binding lifecycle hook
    api.registerVideoGenerationProvider(vigImpl);   // video generation (NEW)
    api.registerMusicGenerationProvider(mugImpl);   // music generation (NEW)
    api.registerWebFetchProvider(wfImpl);           // web fetch (NEW)
  }
};
export default plugin;
```

## Plugin API (`OpenClawPluginApi`)

```typescript
type OpenClawPluginApi = {
  id: string;                                  // plugin id
  name: string;                                // plugin display name
  version: string;                             // plugin version
  description: string;                         // plugin description
  source: string;                              // install source
  rootDir: string;                             // plugin root directory
  registrationMode: "full" | "setup-only" | "setup-runtime" | "cli-metadata";  // cli-metadata NEW
  runtime: PluginRuntime;
  pluginConfig: Record<string, unknown>;       // resolved plugin config
  registerChannel(opts: { plugin: ChannelPlugin }): void;
  registerTool(tool: AnyAgentTool | ToolFactory): void;  // singular, supports factory
  registerCommand(command: OpenClawPluginCommandDefinition): void;
  on<K extends PluginHookName>(hookName: K, handler: PluginHookHandlerMap[K], opts?: { priority?: number }): void;
  registerHook(hookName: string, handler: Function, opts?: { priority?: number }): void;  // alias for on()
  registerHttpRoute(route: HttpRouteDefinition): void;    // replaces old registerHttpHandler
  registerGatewayMethod(method: string, handler: GatewayMethodHandler): void;
  registerProvider(provider: ProviderDefinition): void;   // auth-only, no model catalog
  registerService(service: ServiceDefinition): void;
  registerCli(cli: CliDefinition): void;
  registerContextEngine(id: string, factory: ContextEngineFactory): void;  // exclusive slot
  registerSpeechProvider(provider: SpeechProviderDef): void;
  registerMediaUnderstandingProvider(provider: MediaUnderstandingProviderDef): void;
  registerImageGenerationProvider(provider: ImageGenerationProviderDef): void;
  registerVideoGenerationProvider(provider: VideoGenerationProviderDef): void;  // NEW
  registerMusicGenerationProvider(provider: MusicGenerationProviderDef): void;  // NEW
  registerWebSearchProvider(provider: WebSearchProviderDef): void;
  registerWebFetchProvider(provider: WebFetchProviderDef): void;                // NEW
  registerInteractiveHandler(handler: InteractiveHandlerDef): void;
  onConversationBindingResolved(handler: BindingResolvedHandler): void;
  resolvePath(...segments: string[]): string;
};
```

## Plugin Commands

```typescript
type OpenClawPluginCommandDefinition = {
  name: string;
  description: string;
  acceptsArgs?: boolean;
  requireAuth?: boolean;
  handler: PluginCommandHandler;
};

type PluginCommandContext = {
  senderId: string;
  channel: string;
  isAuthorizedSender: boolean;
  commandBody: string;
  // ...
};

type PluginCommandResult = ReplyPayload;
```

Plugin commands are processed before built-in commands and before agent invocation - they bypass LLM reasoning entirely.

## Plugin Hooks

26 typed hooks via `PluginHookHandlerMap`. Key prompt mutation hooks can return:
- `systemPrompt` - replace system prompt
- `prependContext` - dynamic content prepended to context
- `prependSystemContext` - static, cacheable content prepended to system prompt
- `appendSystemContext` - static, cacheable content appended to system prompt

Notable hooks:
- `subagent_delivery_target` - controls subagent message delivery routing
- `before_dispatch` - intercept inbound messages before agent dispatch (NEW). Returns `{ handled: boolean; text?: string }` to skip default dispatch and optionally reply directly.

New hook context fields:
- `PluginHookAgentContext.runId` - unique identifier for the agent run (NEW)
- `PluginHookBeforeToolCallResult.requireApproval` - plugin-driven approval gates on tool calls (NEW), with severity levels, timeout behavior, and `onResolution` callback
- `PluginCommandContext.threadParentId` - parent conversation id for thread-capable channels (NEW)

Approval resolution types: `allow-once`, `allow-always`, `deny`, `timeout`, `cancelled` (via `PluginApprovalResolutions` const).

Sync-only hooks: `before_message_write` and `tool_result_persist` reject async handlers at registration time.

Hook policy: `plugins.entries[pluginId].hooks.allowPromptInjection` (boolean) controls prompt mutation access. When `false`, `before_prompt_build` is blocked entirely and `before_agent_start` is constrained to non-prompt fields.

## Plugin Runtime

```typescript
type PluginRuntime = PluginRuntimeCore & {
  subagent: PluginRuntimeSubagent;             // gateway subagent (late-bound via Symbol)
  channel: PluginRuntimeChannels;
};

type PluginRuntimeCore = {
  version: string;
  config: OpenClawConfig;
  workspaceDir: string;
  channels: PluginRuntimeChannels;
  tools: PluginRuntimeTools;
  events: PluginRuntimeEvents;
  system: PluginRuntimeSystem;
  modelAuth: {                                 // #41090: safe model auth access
    getApiKeyForModel(modelId: string): Promise<string | undefined>;
    resolveApiKeyForProvider(providerId: string): Promise<string | undefined>;
  };
  mediaUnderstanding: PluginRuntimeMediaUnderstanding;
  imageGeneration: PluginRuntimeImageGeneration;
  webSearch: PluginRuntimeWebSearch;
  stt: PluginRuntimeStt;
  // media, whatsapp, config access...
};
```

Runtime is lazy-loaded via Proxy to avoid heavy dependencies in test/validation scenarios.

`modelAuth` safety wrappers strip `agentDir`/`store`/`profileId`/`preferredProfile` to prevent credential steering by plugins.

Gateway subagent runtime uses `Symbol.for("openclaw.plugin.gatewaySubagentRuntime")` for late binding.

### Local Runtime Module Loading

Key file: `src/plugins/runtime/local-runtime-module.ts`

`loadSiblingRuntimeModuleSync<T>()` loads a sibling module relative to a caller's `import.meta.url` using jiti. Resolution logic:

1. Compute `baseDir` from `fileURLToPath(moduleUrl)`
2. Search candidate directories: `[baseDir, baseDir/plugins/runtime]`
3. Try extensions in order: `.js`, `.ts`, `.mjs`, `.mts`, `.cjs`, `.cts`
4. First existing file wins; throws if none found

Jiti instances are cached by a composite key of `{ tryNative, aliasMap, moduleUrl }` to avoid re-creating loaders for the same configuration. Uses `buildPluginLoaderAliasMap()` and `buildPluginLoaderJitiOptions()` from `src/plugins/sdk-alias.ts`.

```typescript
function loadSiblingRuntimeModuleSync<T>(params: {
  moduleUrl: string;      // caller's import.meta.url
  relativeBase: string;   // e.g. "./my-module" (extension auto-resolved)
}): T;
```

### Bundled Plugin Runtime Dependencies

Key file: `src/plugins/bundled-dir.ts`

`resolveBundledPluginsDir()` locates the bundled extensions directory with this resolution priority:

1. `OPENCLAW_BUNDLED_PLUGINS_DIR` env override (falls back to argv-based package root for stale overrides in packaged installs)
2. Source checkout: `<packageRoot>/extensions/` (preferred when `.git` + `src/` + `extensions/` exist, or in VITEST)
3. Runtime staging: `<packageRoot>/dist-runtime/extensions/` - only used when the paired `dist/extensions/` also exists (prevents wrapper drift after partial builds, fix from #58782)
4. Built distribution: `<packageRoot>/dist/extensions/`
5. Bun `--compile` sibling: `<execDir>/dist/extensions/` or `<execDir>/extensions/`
6. Walk-up from module: traverse up to 6 parent dirs looking for `extensions/`

Package roots are resolved from three sources (deduplicated): `argv[1]`, `process.cwd()`, `import.meta.url`.

## Plugin Config Type

```typescript
type PluginsConfig = {
  enabled?: boolean;                             // master switch
  allow?: string[];                             // allowlist of trusted plugin IDs
  deny?: string[];                              // denylist (allow takes precedence)
  load?: { paths?: string[] };                  // additional search paths
  slots?: {
    memory?: string | null;                     // exclusive memory slot (null/"none" disables)
    contextEngine?: string;                     // exclusive context engine slot
  };
  entries?: Record<string, PluginEntryConfig>;  // per-plugin config
  installs?: Record<string, PluginInstallRecord>; // install tracking
};

type PluginEntryConfig = {
  enabled?: boolean;
  hooks?: { allowPromptInjection?: boolean };   // prompt mutation policy
  subagent?: {                                  // subagent policy
    allowModelOverride?: boolean;
    allowedModels?: string[];
  };
  config?: Record<string, unknown>;
};

type PluginInstallRecord = InstallRecordBase & {
  source: "npm" | "path" | "archive" | "marketplace" | "clawhub";
  spec?: string;
  sourcePath?: string;
  installPath?: string;
  version?: string;
  installedAt?: string;
  resolvedName?: string;
  resolvedVersion?: string;
  resolvedSpec?: string;
  integrity?: string;
  shasum?: string;
  resolvedAt?: string;
  marketplaceName?: string;                     // marketplace install metadata
  marketplaceSource?: string;
  marketplacePlugin?: string;
  clawhubUrl?: string;                          // clawhub install metadata
  clawhubPackage?: string;
  clawhubFamily?: string;
  clawhubChannel?: string;
};
```

## Plugin Loading Flow

1. `loadOpenClawPlugins()` entry point (from gateway/CLI)
2. Discovery: scan all sources for manifests
3. Config validation: each plugin's config checked against its schema
4. Module import via jiti (handles TS/JS, ESM/CJS)
5. Plugin SDK aliases resolved (100+ `openclaw/*` import paths)
6. Provenance tracking: validates plugins are from known/trusted install paths
7. Registry created with status tracking (loaded/disabled/error)
8. Global hook runner initialized

Options: `mode: "validate"` validates without executing plugins.
Untracked plugins (no provenance) generate diagnostics warnings.

Key file: `src/plugins/loader.ts`

### Bundled Channel Plugin Enable Resolution

Key file: `src/plugins/config-state.ts`

`resolveEffectiveEnableState()` wraps the base `resolveEnableState()` with an additional override for bundled channel plugins. When a bundled plugin is disabled by default (reason `"bundled (disabled by default)"` or `"not in allowlist"`), it calls `isBundledChannelEnabledByChannelConfig()` which checks if `channels.<channelId>.enabled === true` exists in the root config (#58873).

```typescript
function resolveEffectiveEnableState(params: {
  id: string;
  origin: PluginRecord["origin"];
  config: NormalizedPluginsConfig;
  rootConfig?: OpenClawConfig;
  enabledByDefault?: boolean;
}): { enabled: boolean; reason?: string };
```

The channel id is resolved via `normalizeChatChannelId(pluginId)`. If the root config has `channels.<id>.enabled === true`, the plugin is force-enabled regardless of allowlist or default-disabled state. This allows `applyPluginAutoEnable()` to set `channels.<id>.enabled: true` and have it take effect during loading.

### Auto-Enable for API Key Auth

Key file: `src/config/plugin-auto-enable.ts`

`applyPluginAutoEnable()` supports `mode: "api_key"` auth profiles (not just OAuth) for triggering auto-enable via [REDACTED_SECRET] (#57127). The `isProviderConfigured()` function checks three sources:

1. `auth.profiles` - matches `profile.provider` against the target provider id (works for both OAuth and API key mode profiles)
2. `models.providers` - matches keys in the provider config map
3. Model refs - extracts provider prefix from `agents.defaults.model`, `agents.list[].model`, and model fallbacks

`resolveAutoEnableProviderPluginIds()` merges the built-in `BUNDLED_AUTO_ENABLE_PROVIDER_PLUGIN_IDS` map with manifest-declared `autoEnableWhenConfiguredProviders` from the `PluginManifestRegistry`. Plugins that are explicitly disabled or denied are skipped. The `preferOver` mechanism prevents enabling superseded plugins when a preferred alternative is also configured.

### Uninstall Resolution

`resolvePluginUninstallId()` (`src/cli/plugins-cli.ts`) uses a multi-step fallback chain:
1. Match by plugin `id`/`name` in registry
2. Match by `spec`/`resolvedSpec`/`resolvedName`/`marketplacePlugin` in install records
3. Parse as ClawHub spec (`clawhub:<name>`) and match `clawhubPackage` (versionless)
4. Fall back to raw id

### Doctor: Stale Plugin Config Pruning

Key file: `src/commands/doctor/shared/stale-plugin-config.ts` (PR #53187)

`scanStalePluginConfig()` finds orphaned `plugins.allow` and `plugins.entries` refs pointing to plugins no longer installed. `maybeRepairStalePluginConfig()` auto-removes them via `openclaw doctor --fix`. Auto-repair is blocked when manifest discovery has errors (`isStalePluginAutoRepairBlocked()`).

`normalizePluginId()` (`src/plugins/config-state.ts`) is now exported for use by the doctor scanner.

## Process-Global Singleton Pattern

Six Symbol-based singletons ensure shared state across duplicated dist chunks:

| Symbol | Purpose |
|--------|---------|
| `openclaw.plugins.hook-runner-global-state` | Global hook runner (#40184) |
| `openclaw.contextEngineRegistryState` | Context engine registry |
| `openclaw.pluginRegistryState` | Plugin registry state (#50418) |
| `openclaw.pluginCommandsState` | Command registry across module graphs (#50431) |
| `openclaw.plugins.binding.global-state` | Conversation binding state |
| `openclaw.plugin.gatewaySubagentRuntime` | Gateway subagent late binding |

## Plugin Update

```typescript
type PluginUpdateStatus = "updated" | "unchanged" | "skipped" | "error";
type PluginUpdateOutcome = {
  pluginId: string;
  status: PluginUpdateStatus;
  message: string;
  currentVersion?: string;
  nextVersion?: string;
};
```

- `updateNpmInstalledPlugins()` - update npm-installed plugins with dry-run support
- `syncPluginsForUpdateChannel()` - sync plugins between bundled/npm based on update channel
- Unpinned version bumps no longer trigger false integrity-mismatch warnings (#37179)

## Plugin Installation

### CLI Commands

```bash
openclaw plugins install <npm-spec>       # from npm
openclaw plugins install <path>           # from local dir
openclaw plugins install --link <path>    # symlink local dir
openclaw plugins disable <id>
openclaw plugins enable <id>
openclaw plugins list [--json] [--verbose]
openclaw plugins info <id> [--json]
openclaw plugins uninstall <id> [--keep-files] [--force]
openclaw plugins update [--all] [--dry-run]
openclaw plugins doctor                   # diagnostics
```

### Install Flow (npm)

1. Validate npm spec
2. Download + extract tarball
3. Inspect `package.json` + `openclaw.plugin.json`
4. Validate `openclaw.extensions` array
5. Security scan for dangerous patterns
6. Install to `~/.openclaw/plugins/<plugin-id>/`
7. Run `npm install --omit=dev` in plugin dir
8. Update config: install record + allowlist + enable
9. Gateway restart required

## Plugin SDK (`src/plugin-sdk/`)

100+ scoped exports. Plugins import from `openclaw/plugin-sdk` or `openclaw/<subpath>`. Runtime resolves via jiti alias.

Key exports: types, config access, channel interfaces, tool builders, hook types, media pipeline, crypto/signing utilities, `requireApiKey`/`ResolvedProviderAuth` for model auth, `ContextEngineFactory` for context engine registration.

New SDK modules:
- `src/plugin-sdk/diffs.ts` - narrow facade for diff/artifact context routing
- `src/plugin-sdk/channel-actions.ts` - `createMessageToolButtonsSchema()` and `createMessageToolCardSchema()` moved from core to SDK
- `src/plugin-sdk/approval-delivery-helpers.ts` - `createApproverRestrictedNativeApprovalAdapter()` factory for channel-specific approval delivery. Produces an adapter with `auth` (authorize actor, availability state), `delivery` (DM route detection, forwarding suppression), and optional `native` (origin/approver-DM target resolution, delivery capabilities) sections. Used by discord, slack, and telegram extensions.
- `src/plugin-sdk/approval-native-helpers.ts` - `createChannelNativeOriginTargetResolver()` factory for resolving approval request origin targets across approval kinds (`exec`/`plugin`). Decouples target resolution from delivery logic. (NEW)
- `src/plugin-sdk/approval-client-helpers.ts` - client-side approval helper utilities for extension approval interactions. (NEW)
- `src/plugin-sdk/approval-auth-runtime.ts`, `approval-client-runtime.ts`, `approval-delivery-runtime.ts`, `approval-gateway-runtime.ts`, `approval-handler-adapter-runtime.ts`, `approval-handler-runtime.ts`, `approval-native-runtime.ts`, `approval-reply-runtime.ts` - granular runtime seams for the approval subsystem, enabling staged loading and extension-specific approval adapter composition. (NEW)
- `src/plugin-sdk/acp-binding-runtime.ts` - ACP binding runtime seam extracted from core. (NEW)
- Browser SDK split: `browser-bridge.ts`, `browser-cdp.ts`, `browser-config.ts`, `browser-config-runtime.ts`, `browser-config-support.ts`, `browser-control-auth.ts`, `browser-host-inspection.ts`, `browser-maintenance.ts`, `browser-node-host.ts` - browser subsystem split into fine-grained SDK modules for smaller import footprints. (NEW)

Channel-specific SDK subpaths:
- `openclaw/discord` - thread binding management (`autoBindSpawnedDiscordSubagent`, `listThreadBindingsBySessionKey`, `unbindThreadBindingsBySessionKey`)
- `openclaw/slack` - account resolution, inspection, onboarding adapters
- `openclaw/telegram` - account resolution, inspection, onboarding adapters

## Extension Shared Helpers

`extensions/shared/` directory with extracted modules from `src/plugin-sdk/extension-shared.ts` (boundary debt removal). Provides common utilities for channel extensions without coupling to core plugin-sdk internals.

## Extension Directory Structure

```
extensions/my-plugin/
  ├── package.json            # name, openclaw.extensions
  ├── openclaw.plugin.json    # manifest
  ├── index.ts                # default export: plugin definition
  ├── src/                    # implementation
  └── node_modules/           # runtime deps (npm install --omit=dev)
```

Keep plugin deps in the extension `package.json`, not root. Use `devDependencies` or `peerDependencies` for `openclaw` (resolved at runtime via jiti alias).

Removed `api.registerHttpHandler()` - plugins using it get a clear migration error pointing to `api.registerHttpRoute()` (#36794).

## Bundled Web Search Provider: SearXNG (#57317)

Key files: `extensions/searxng/`

Self-hosted meta-search provider with no API key required. Registered as a bundled web search provider plugin.

### Manifest

```json
{
  "id": "searxng",
  "contracts": { "webSearchProviders": ["searxng"] },
  "configSchema": {
    "type": "object",
    "properties": {
      "webSearch": {
        "type": "object",
        "properties": {
          "baseUrl": { "type": ["string", "object"] },
          "categories": { "type": "string" },
          "language": { "type": "string" }
        }
      }
    }
  }
}
```

### Registration

Uses `definePluginEntry()` + `api.registerWebSearchProvider()` with the `WebSearchProviderPlugin` interface:

```typescript
const provider: WebSearchProviderPlugin = {
  id: "searxng",
  label: "SearXNG Search",
  hint: "Self-hosted meta-search with no API key required",
  requiresCredential: true,            // base URL acts as credential
  credentialLabel: "SearXNG Base URL",
  envVars: ["SEARXNG_BASE_URL"],
  autoDetectOrder: 200,
  credentialPath: "plugins.entries.searxng.config.webSearch.baseUrl",
  // ...credential get/set helpers, tool factory
};
```

### Runtime Client (`src/searxng-client.ts`)

`runSearxngSearch()` performs the search:
1. Resolve config: `baseUrl` from plugin config, inline env secret ref, or `SEARXNG_BASE_URL` env var
2. SSRF validation: `validateSearxngBaseUrl()` uses `assertHttpUrlTargetsPrivateNetwork()` - HTTP URLs must target private/loopback network; HTTPS allowed for public hosts
3. Cache check via `normalizeCacheKey()` / `readCache()` with `DEFAULT_CACHE_TTL_MINUTES`
4. Build URL: `<baseUrl>/search?q=...&format=json&categories=...&language=...`
5. Fetch via `withTrustedWebSearchEndpoint()` with configurable timeout (default 20s) and 1MB response limit
6. Parse results: normalize each `SearxngResult` (`url`, `title`, `content`), cap at `count`
7. Wrap results with `wrapWebContent()` (untrusted external content markers) and `writeCache()`

### Config Resolution Chain (`src/config.ts`)

Base URL resolution order:
1. `plugins.entries.searxng.config.webSearch.baseUrl` (string or secret-input object)
2. Inline env secret ref (`{ source: "env", id: "VAR_NAME" }`)
3. `SEARXNG_BASE_URL` environment variable

Categories and language from `plugins.entries.searxng.config.webSearch.categories`/`language`.

## New Provider Plugin Types

Provider plugins gained several new hook contexts for decoupling provider-specific logic from core:

```typescript
// Provider-owned model-id normalization (plugin-side alias cleanup)
type ProviderNormalizeModelIdContext = { provider: string; modelId: string };

// Provider-owned config normalization for models.providers.<id> entries
type ProviderNormalizeConfigContext = { provider: string; providerConfig: ModelProviderConfig };

// Provider-owned transport normalization (API/baseUrl-based, not provider-id-based)
type ProviderNormalizeTransportContext = { provider: string; api?: string | null; baseUrl?: string };

// Provider-owned env/config auth marker resolution
type ProviderResolveConfigApiKeyContext = { provider: string; env: NodeJS.ProcessEnv };

// Provider-owned transport creation (custom StreamFn replacing pi-ai default)
type ProviderCreateStreamFnContext = { config?; agentDir?; workspaceDir?; provider; modelId; model };

// Provider-owned embedding transport creation (memory embeddings via provider plugin)
type ProviderCreateEmbeddingProviderContext = {
  config; agentDir?; workspaceDir?; provider; model;
  remote?: { baseUrl?; apiKey?; headers? };
  providerApiKey?; outputDimensionality?; taskType?;
};
```

`ProviderWrapStreamFnContext` now includes optional `model?: ProviderRuntimeModel`.

`ProviderAuthContext` now includes optional `env?: NodeJS.ProcessEnv`.

## Plugin Tool Context Additions

`OpenClawPluginToolContext` gained new fields:
- `runtimeConfig?: OpenClawConfig` - active runtime-resolved config snapshot
- `browser?: { sandboxBridgeUrl?; allowHostControl? }` - browser sandbox bridge info
- `deliveryContext?: DeliveryContext` - trusted ambient delivery route for active session

## Speech Provider Plugin Additions

`SpeechProviderPlugin` gained new hooks:
- `autoSelectOrder?: number` - priority for auto-selection
- `resolveConfig?` - provider-owned config resolution
- `parseDirectiveToken?` - provider-owned directive token parsing
- `resolveTalkConfig?` / `resolveTalkOverrides?` - talk config resolution

## Web Search Provider Additions

`WebSearchProviderPlugin` gained `runSetup?` hook for interactive provider setup.

## Video & Music Generation Providers (NEW)

`VideoGenerationProviderPlugin` and `MusicGenerationProviderPlugin` follow the same pattern as image generation providers. Register via `api.registerVideoGenerationProvider()` and `api.registerMusicGenerationProvider()`. Declare in manifest under `contracts.videoGenerationProviders` and `contracts.musicGenerationProviders`.

## Web Fetch Provider (NEW)

`WebFetchProviderPlugin` - custom web fetch implementation. Register via `api.registerWebFetchProvider()`. Declare in manifest under `contracts.webFetchProviders`. SDK contract artifacts available via the `web-fetch` contract subpath.

## Memory Plugin State (`src/plugins/memory-state.ts` - NEW)

Centralized module for memory plugin exclusive slots:

```typescript
type MemoryPromptSectionBuilder = (params: { availableTools: Set<string>; citationsMode? }) => string[];
type MemoryFlushPlanResolver = (params: { cfg?; nowMs? }) => MemoryFlushPlan | null;
type MemoryPluginRuntime = {
  getMemorySearchManager(params: { cfg; agentId; purpose? }): Promise<{ manager; error? }>;
  resolveMemoryBackendConfig(params: { cfg; agentId }): MemoryRuntimeBackendConfig;
  closeAllMemorySearchManagers?(): Promise<void>;
};
```

Three exclusive slots: prompt section builder, flush plan resolver, and runtime adapter.

## Memory Embedding Providers (`src/plugins/memory-embedding-providers.ts` - NEW)

Multiple adapters can coexist (unlike other exclusive slots):

```typescript
type MemoryEmbeddingProviderAdapter = {
  id: string;
  model: string;
  maxInputTokens?: number;
  embedQuery: (text: string) => Promise<number[]>;
  embedBatch: (texts: string[]) => Promise<number[][]>;
};
```

## Plugin Kind Slots (`src/plugins/slots.ts` - NEW)

Multi-kind plugin support. `kind` field can now be a single `PluginKind` or an array:

```typescript
type PluginKind = "memory" | "context-engine";
function hasKind(kind: PluginKind | PluginKind[] | undefined, target: PluginKind): boolean;
function normalizeKinds(kind?: PluginKind | PluginKind[]): PluginKind[];
```

## CLI Backend Plugin Type (NEW)

```typescript
type CliBackendPlugin = {
  id: string;                          // provider id (e.g. "claude-cli/opus")
  config: CliBackendConfig;            // default backend config
  bundleMcp?: boolean;                 // inject bundle MCP config file
  normalizeConfig?: (config: CliBackendConfig) => CliBackendConfig;
};
```

## CLI Command Descriptors (NEW)

```typescript
type OpenClawPluginCliCommandDescriptor = {
  name: string;
  description: string;
  hasSubcommands: boolean;
};
```

Used for lazy root CLI registration - when descriptors cover every top-level command, the plugin registrar stays lazy-loaded.

## Plugin Install Security

- `before_install` hook removed (commit `fcb802e`)
- Dangerous skill installs now fail closed by default (commit `0d7f1e2`)
- New override: `--dangerously-force-unsafe-install` bypasses built-in blocking
- `dangerouslyForceUnsafeInstall` is now threaded through all install paths including archive extraction (#58879), via `InstallSafetyOverrides` type in `PackageInstallCommonParams` and `FileInstallCommonParams` (`src/plugins/install.ts`). `FileInstallCommonParams` picks `dangerouslyForceUnsafeInstall` from `PackageInstallCommonParams`. All scan functions (`scanBundleInstallSource`, `scanPackageInstallSource`, `scanFileInstallSource`) accept `InstallSafetyOverrides`. Also threaded through marketplace (`src/plugins/marketplace.ts`), ClawHub (`src/plugins/clawhub.ts`), and CLI install command (`src/cli/plugins-install-command.ts`).
- Marketplace and Ollama network requests are guarded (commit `8deb952`)
- Plugin install blocked when source scan fails (commit `7a953a5`)

## Plugin Shapes

OpenClaw classifies every loaded plugin into a shape:

| Shape | Meaning |
|-------|---------|
| `plain-capability` | One capability type (e.g. provider-only like `mistral`) |
| `hybrid-capability` | Multiple capability types (e.g. `openai` owns text, speech, media, images) |
| `hook-only` | Only hooks, no capabilities/tools/commands/services |
| `non-capability` | Tools/commands/services but no capabilities |

Use `openclaw plugins inspect <id>` to see shape.

## Bundle Compatibility

OpenClaw can install plugins from three external ecosystems: Codex, Claude, Cursor. These are content packs mapped into native features.

- Bundle formats: `.codex-plugin/plugin.json`, `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`
- Auto-detected during install from local paths and archives
- Mapped features: skill roots, command roots (Claude/Cursor), hook packs (Codex), MCP tools, Claude `settings.json` defaults
- Show up as `Format: bundle` with subtype in `plugins list` / `plugins inspect`
- Bundles are NOT in-process native plugins - narrower trust boundary

## ClawHub Install Flow

- `openclaw plugins install <spec>` checks ClawHub first, falls back to npm
- Explicit: `openclaw plugins install clawhub:<package>`
- ClawHub downloads archive, checks plugin API / min gateway compat
- Marketplace support: `<plugin>@<marketplace>` shorthand, `--marketplace` flag
- Marketplace sources: known-marketplace name, local path, GitHub repo shorthand, git URL

## New Bundled Extensions (2026.4)

13 new extensions shipped since 2026.4.1:

| Extension | ID | Purpose |
|-----------|-----|---------|
| `extensions/alibaba/` | `alibaba` | Alibaba Model Studio (DashScope/Qwen) video generation provider |
| `extensions/amazon-bedrock-mantle/` | `amazon-bedrock-mantle` | Amazon Bedrock enhanced provider with mantle token generation |
| `extensions/arcee/` | `arcee` | Arcee AI inference provider |
| `extensions/comfy/` | `comfy` | ComfyUI workflow bridge - image, video, and music generation via local or cloud ComfyUI |
| `extensions/fireworks/` | `fireworks` | Fireworks AI inference provider |
| `extensions/memory-wiki/` | `memory-wiki` | Persistent wiki compiler and Obsidian-friendly knowledge vault |
| `extensions/qa-channel/` | `qa-channel` | QA harness channel for automated testing |
| `extensions/qa-lab/` | `qa-lab` | QA lab runner and frontier bakeoff infrastructure |
| `extensions/qwen/` | `qwen` | Qwen model provider |
| `extensions/runway/` | `runway` | Runway video generation provider |
| `extensions/stepfun/` | `stepfun` | StepFun (CN/Intl) inference provider |
| `extensions/vydra/` | `vydra` | Vydra provider |
| `extensions/webhooks/` | `webhooks` | Authenticated inbound webhooks binding external automation to TaskFlows |

**Comfy**: `mode: "local"` (connects to self-hosted ComfyUI) or `"cloud"`. Supports separate `image`, `video`, `music` workflow configs with `workflowPath`, `promptNodeId`, `outputNodeId`, `pollIntervalMs`, `timeoutMs`. Declares all three generation contracts: `imageGenerationProviders`, `videoGenerationProviders`, `musicGenerationProviders`.

**Webhooks**: `plugins.entries.webhooks.config.routes` is a record of named routes, each with `sessionKey` (required), `secret` (required, SecretInput), optional `path`, `enabled`, `controllerId`, `description`.

## Community Plugins

Community plugins are available on ClawHub and npm. Notable ones: Codex App Server Bridge, DingTalk, Lossless Claw (LCM), Opik, QQbot, WeCom.

## ExecApprovalManager Generics

`ExecApprovalManager` is now generic: `ExecApprovalManager<TPayload = ExecApprovalRequestPayload>`. All methods (`create`, `register`, `getSnapshot`) use the generic `TPayload` type.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)