diff --git a/.gitignore b/.gitignore
index 87751335a6d..6667c670952 100644
--- a/.gitignore
+++ b/.gitignore
@@ -69,6 +69,7 @@ apps/ios/*.mobileprovision
# Local untracked files
.local/
+docs/.local/
IDENTITY.md
USER.md
.tgz
diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc
index 0b6b8f0fb71..94035711053 100644
--- a/.markdownlint-cli2.jsonc
+++ b/.markdownlint-cli2.jsonc
@@ -1,6 +1,6 @@
{
"globs": ["docs/**/*.md", "docs/**/*.mdx", "README.md"],
- "ignores": ["docs/zh-CN/**", "docs/.i18n/**", "docs/reference/templates/**"],
+ "ignores": ["docs/zh-CN/**", "docs/.i18n/**", "docs/reference/templates/**", "**/.local/**"],
"config": {
"default": true,
diff --git a/docs/assets/install-script.svg b/docs/assets/install-script.svg
new file mode 100644
index 00000000000..78a6f975641
--- /dev/null
+++ b/docs/assets/install-script.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/docs/docs.json b/docs/docs.json
index 42dcf5e337e..4ef7baffbae 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -24,6 +24,14 @@
"dark": "#FF5A36",
"light": "#FF8A6B"
},
+ "styling": {
+ "codeblocks": {
+ "theme": {
+ "dark": "min-dark",
+ "light": "min-light"
+ }
+ }
+ },
"navbar": {
"links": [
{
@@ -1100,6 +1108,7 @@
"group": "Configuration and operations",
"pages": [
"gateway/configuration",
+ "gateway/configuration-reference",
"gateway/configuration-examples",
"gateway/authentication",
"gateway/health",
diff --git a/docs/gateway/configuration-examples.md b/docs/gateway/configuration-examples.md
index ac3f992930a..ca77eef132d 100644
--- a/docs/gateway/configuration-examples.md
+++ b/docs/gateway/configuration-examples.md
@@ -67,7 +67,11 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
// Auth profile metadata (secrets live in auth-profiles.json)
auth: {
profiles: {
- "anthropic:me@example.com": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
+ "anthropic:me@example.com": {
+ provider: "anthropic",
+ mode: "oauth",
+ email: "me@example.com",
+ },
"anthropic:work": { provider: "anthropic", mode: "api_key" },
"openai:default": { provider: "openai", mode: "api_key" },
"openai-codex:default": { provider: "openai-codex", mode: "oauth" },
@@ -375,7 +379,10 @@ Save to `~/.openclaw/openclaw.json` and you can DM the bot from that number.
to: "+15555550123",
thinking: "low",
timeoutSeconds: 300,
- transform: { module: "./transforms/gmail.js", export: "transformGmail" },
+ transform: {
+ module: "./transforms/gmail.js",
+ export: "transformGmail",
+ },
},
],
gmail: {
diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md
new file mode 100644
index 00000000000..9dc16e68c1f
--- /dev/null
+++ b/docs/gateway/configuration-reference.md
@@ -0,0 +1,2318 @@
+---
+title: "Configuration Reference"
+description: "Complete field-by-field reference for ~/.openclaw/openclaw.json"
+---
+
+# Configuration Reference
+
+Every field available in `~/.openclaw/openclaw.json`. For a task-oriented overview, see [Configuration](/gateway/configuration).
+
+Config format is **JSON5** (comments + trailing commas allowed). All fields are optional — OpenClaw uses safe defaults when omitted.
+
+---
+
+## Channels
+
+Each channel starts automatically when its config section exists (unless `enabled: false`).
+
+### DM and group access
+
+All channels support DM policies and group policies:
+
+| DM policy | Behavior |
+| ------------------- | --------------------------------------------------------------- |
+| `pairing` (default) | Unknown senders get a one-time pairing code; owner must approve |
+| `allowlist` | Only senders in `allowFrom` (or paired allow store) |
+| `open` | Allow all inbound DMs (requires `allowFrom: ["*"]`) |
+| `disabled` | Ignore all inbound DMs |
+
+| Group policy | Behavior |
+| --------------------- | ------------------------------------------------------ |
+| `allowlist` (default) | Only groups matching the configured allowlist |
+| `open` | Bypass group allowlists (mention-gating still applies) |
+| `disabled` | Block all group/room messages |
+
+
+`channels.defaults.groupPolicy` sets the default when a provider's `groupPolicy` is unset.
+Pairing codes expire after 1 hour. Pending DM pairing requests are capped at **3 per channel**.
+Slack/Discord have a special fallback: if their provider section is missing entirely, runtime group policy can resolve to `open` (with a startup warning).
+
+
+### WhatsApp
+
+WhatsApp runs through the gateway's web channel (Baileys Web). It starts automatically when a linked session exists.
+
+```json5
+{
+ channels: {
+ whatsapp: {
+ dmPolicy: "pairing", // pairing | allowlist | open | disabled
+ allowFrom: ["+15555550123", "+447700900123"],
+ textChunkLimit: 4000,
+ chunkMode: "length", // length | newline
+ mediaMaxMb: 50,
+ sendReadReceipts: true, // blue ticks (false in self-chat mode)
+ groups: {
+ "*": { requireMention: true },
+ },
+ groupPolicy: "allowlist",
+ groupAllowFrom: ["+15551234567"],
+ },
+ },
+ web: {
+ enabled: true,
+ heartbeatSeconds: 60,
+ reconnect: {
+ initialMs: 2000,
+ maxMs: 120000,
+ factor: 1.4,
+ jitter: 0.2,
+ maxAttempts: 0,
+ },
+ },
+}
+```
+
+
+
+```json5
+{
+ channels: {
+ whatsapp: {
+ accounts: {
+ default: {},
+ personal: {},
+ biz: {
+ // authDir: "~/.openclaw/credentials/whatsapp/biz",
+ },
+ },
+ },
+ },
+}
+```
+
+- Outbound commands default to account `default` if present; otherwise the first configured account id (sorted).
+- Legacy single-account Baileys auth dir is migrated by `openclaw doctor` into `whatsapp/default`.
+- Per-account override: `channels.whatsapp.accounts..sendReadReceipts`.
+
+
+
+### Telegram
+
+```json5
+{
+ channels: {
+ telegram: {
+ enabled: true,
+ botToken: "your-bot-token",
+ dmPolicy: "pairing",
+ allowFrom: ["tg:123456789"],
+ groups: {
+ "*": { requireMention: true },
+ "-1001234567890": {
+ allowFrom: ["@admin"],
+ systemPrompt: "Keep answers brief.",
+ topics: {
+ "99": {
+ requireMention: false,
+ skills: ["search"],
+ systemPrompt: "Stay on topic.",
+ },
+ },
+ },
+ },
+ customCommands: [
+ { command: "backup", description: "Git backup" },
+ { command: "generate", description: "Create an image" },
+ ],
+ historyLimit: 50,
+ replyToMode: "first", // off | first | all
+ linkPreview: true,
+ streamMode: "partial", // off | partial | block
+ draftChunk: {
+ minChars: 200,
+ maxChars: 800,
+ breakPreference: "paragraph", // paragraph | newline | sentence
+ },
+ actions: { reactions: true, sendMessage: true },
+ reactionNotifications: "own", // off | own | all
+ mediaMaxMb: 5,
+ retry: {
+ attempts: 3,
+ minDelayMs: 400,
+ maxDelayMs: 30000,
+ jitter: 0.1,
+ },
+ network: { autoSelectFamily: false },
+ proxy: "socks5://localhost:9050",
+ webhookUrl: "https://example.com/telegram-webhook",
+ webhookSecret: "secret",
+ webhookPath: "/telegram-webhook",
+ },
+ },
+}
+```
+
+- Bot token: `channels.telegram.botToken` or `channels.telegram.tokenFile`, with `TELEGRAM_BOT_TOKEN` as fallback for the default account.
+- `configWrites: false` blocks Telegram-initiated config writes (supergroup ID migrations, `/config set|unset`).
+- Draft streaming uses Telegram `sendMessageDraft` (requires private chat topics).
+- Retry policy: see [Retry policy](/concepts/retry).
+
+### Discord
+
+```json5
+{
+ channels: {
+ discord: {
+ enabled: true,
+ token: "your-bot-token",
+ mediaMaxMb: 8,
+ allowBots: false,
+ actions: {
+ reactions: true,
+ stickers: true,
+ polls: true,
+ permissions: true,
+ messages: true,
+ threads: true,
+ pins: true,
+ search: true,
+ memberInfo: true,
+ roleInfo: true,
+ roles: false,
+ channelInfo: true,
+ voiceStatus: true,
+ events: true,
+ moderation: false,
+ },
+ replyToMode: "off", // off | first | all
+ dm: {
+ enabled: true,
+ policy: "pairing",
+ allowFrom: ["1234567890", "steipete"],
+ groupEnabled: false,
+ groupChannels: ["openclaw-dm"],
+ },
+ guilds: {
+ "123456789012345678": {
+ slug: "friends-of-openclaw",
+ requireMention: false,
+ reactionNotifications: "own",
+ users: ["987654321098765432"],
+ channels: {
+ general: { allow: true },
+ help: {
+ allow: true,
+ requireMention: true,
+ users: ["987654321098765432"],
+ skills: ["docs"],
+ systemPrompt: "Short answers only.",
+ },
+ },
+ },
+ },
+ historyLimit: 20,
+ textChunkLimit: 2000,
+ chunkMode: "length", // length | newline
+ maxLinesPerMessage: 17,
+ retry: {
+ attempts: 3,
+ minDelayMs: 500,
+ maxDelayMs: 30000,
+ jitter: 0.1,
+ },
+ },
+ },
+}
+```
+
+- Token: `channels.discord.token`, with `DISCORD_BOT_TOKEN` as fallback for the default account.
+- Use `user:` (DM) or `channel:` (guild channel) for delivery targets; bare numeric IDs are rejected.
+- Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged name (no `#`). Prefer guild IDs.
+- Bot-authored messages are ignored by default. `allowBots: true` enables them (own messages still filtered).
+- `maxLinesPerMessage` (default 17) splits tall messages even when under 2000 chars.
+
+**Reaction notification modes:** `off` (none), `own` (bot's messages, default), `all` (all messages), `allowlist` (from `guilds..users` on all messages).
+
+### Google Chat
+
+```json5
+{
+ channels: {
+ googlechat: {
+ enabled: true,
+ serviceAccountFile: "/path/to/service-account.json",
+ audienceType: "app-url", // app-url | project-number
+ audience: "https://gateway.example.com/googlechat",
+ webhookPath: "/googlechat",
+ botUser: "users/1234567890",
+ dm: {
+ enabled: true,
+ policy: "pairing",
+ allowFrom: ["users/1234567890"],
+ },
+ groupPolicy: "allowlist",
+ groups: {
+ "spaces/AAAA": { allow: true, requireMention: true },
+ },
+ actions: { reactions: true },
+ typingIndicator: "message",
+ mediaMaxMb: 20,
+ },
+ },
+}
+```
+
+- Service account JSON: inline (`serviceAccount`) or file-based (`serviceAccountFile`).
+- Env fallbacks: `GOOGLE_CHAT_SERVICE_ACCOUNT` or `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE`.
+- Use `spaces/` or `users/` for delivery targets.
+
+### Slack
+
+```json5
+{
+ channels: {
+ slack: {
+ enabled: true,
+ botToken: "xoxb-...",
+ appToken: "xapp-...",
+ dm: {
+ enabled: true,
+ policy: "pairing",
+ allowFrom: ["U123", "U456", "*"],
+ groupEnabled: false,
+ groupChannels: ["G123"],
+ },
+ channels: {
+ C123: { allow: true, requireMention: true, allowBots: false },
+ "#general": {
+ allow: true,
+ requireMention: true,
+ allowBots: false,
+ users: ["U123"],
+ skills: ["docs"],
+ systemPrompt: "Short answers only.",
+ },
+ },
+ historyLimit: 50,
+ allowBots: false,
+ reactionNotifications: "own",
+ reactionAllowlist: ["U123"],
+ replyToMode: "off", // off | first | all
+ thread: {
+ historyScope: "thread", // thread | channel
+ inheritParent: false,
+ },
+ actions: {
+ reactions: true,
+ messages: true,
+ pins: true,
+ memberInfo: true,
+ emojiList: true,
+ },
+ slashCommand: {
+ enabled: true,
+ name: "openclaw",
+ sessionPrefix: "slack:slash",
+ ephemeral: true,
+ },
+ textChunkLimit: 4000,
+ chunkMode: "length",
+ mediaMaxMb: 20,
+ },
+ },
+}
+```
+
+- **Socket mode** requires both `botToken` and `appToken` (`SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` for default account env fallback).
+- **HTTP mode** requires `botToken` plus `signingSecret` (at root or per-account).
+- `configWrites: false` blocks Slack-initiated config writes.
+- Use `user:` (DM) or `channel:` for delivery targets.
+
+**Reaction notification modes:** `off`, `own` (default), `all`, `allowlist` (from `reactionAllowlist`).
+
+**Thread session isolation:** `thread.historyScope` is per-thread (default) or shared across channel. `thread.inheritParent` copies parent channel transcript to new threads.
+
+| Action group | Default | Notes |
+| ------------ | ------- | ---------------------- |
+| reactions | enabled | React + list reactions |
+| messages | enabled | Read/send/edit/delete |
+| pins | enabled | Pin/unpin/list |
+| memberInfo | enabled | Member info |
+| emojiList | enabled | Custom emoji list |
+
+### Mattermost
+
+Mattermost ships as a plugin: `openclaw plugins install @openclaw/mattermost`.
+
+```json5
+{
+ channels: {
+ mattermost: {
+ enabled: true,
+ botToken: "mm-token",
+ baseUrl: "https://chat.example.com",
+ dmPolicy: "pairing",
+ chatmode: "oncall", // oncall | onmessage | onchar
+ oncharPrefixes: [">", "!"],
+ textChunkLimit: 4000,
+ chunkMode: "length",
+ },
+ },
+}
+```
+
+Chat modes: `oncall` (respond on @-mention, default), `onmessage` (every message), `onchar` (messages starting with trigger prefix).
+
+### Signal
+
+```json5
+{
+ channels: {
+ signal: {
+ reactionNotifications: "own", // off | own | all | allowlist
+ reactionAllowlist: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
+ historyLimit: 50,
+ },
+ },
+}
+```
+
+**Reaction notification modes:** `off`, `own` (default), `all`, `allowlist` (from `reactionAllowlist`).
+
+### iMessage
+
+OpenClaw spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
+
+```json5
+{
+ channels: {
+ imessage: {
+ enabled: true,
+ cliPath: "imsg",
+ dbPath: "~/Library/Messages/chat.db",
+ remoteHost: "user@gateway-host",
+ dmPolicy: "pairing",
+ allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
+ historyLimit: 50,
+ includeAttachments: false,
+ mediaMaxMb: 16,
+ service: "auto",
+ region: "US",
+ },
+ },
+}
+```
+
+- Requires Full Disk Access to the Messages DB.
+- Prefer `chat_id:` targets. Use `imsg chats --limit 20` to list chats.
+- `cliPath` can point to an SSH wrapper; set `remoteHost` for SCP attachment fetching.
+
+
+
+```bash
+#!/usr/bin/env bash
+exec ssh -T gateway-host imsg "$@"
+```
+
+
+
+### Multi-account (all channels)
+
+Run multiple accounts per channel (each with its own `accountId`):
+
+```json5
+{
+ channels: {
+ telegram: {
+ accounts: {
+ default: {
+ name: "Primary bot",
+ botToken: "123456:ABC...",
+ },
+ alerts: {
+ name: "Alerts bot",
+ botToken: "987654:XYZ...",
+ },
+ },
+ },
+ },
+}
+```
+
+- `default` is used when `accountId` is omitted (CLI + routing).
+- Env tokens only apply to the **default** account.
+- Base channel settings apply to all accounts unless overridden per account.
+- Use `bindings[].match.accountId` to route each account to a different agent.
+
+### Group chat mention gating
+
+Group messages default to **require mention** (metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, Google Chat, and iMessage group chats.
+
+**Mention types:**
+
+- **Metadata mentions**: Native platform @-mentions. Ignored in WhatsApp self-chat mode.
+- **Text patterns**: Regex patterns in `agents.list[].groupChat.mentionPatterns`. Always checked.
+- Mention gating is enforced only when detection is possible (native mentions or at least one pattern).
+
+```json5
+{
+ messages: {
+ groupChat: { historyLimit: 50 },
+ },
+ agents: {
+ list: [{ id: "main", groupChat: { mentionPatterns: ["@openclaw", "openclaw"] } }],
+ },
+}
+```
+
+`messages.groupChat.historyLimit` sets the global default. Channels can override with `channels..historyLimit` (or per-account). Set `0` to disable.
+
+#### DM history limits
+
+```json5
+{
+ channels: {
+ telegram: {
+ dmHistoryLimit: 30,
+ dms: {
+ "123456789": { historyLimit: 50 },
+ },
+ },
+ },
+}
+```
+
+Resolution: per-DM override → provider default → no limit (all retained).
+
+Supported: `telegram`, `whatsapp`, `discord`, `slack`, `signal`, `imessage`, `msteams`.
+
+#### Self-chat mode
+
+Include your own number in `allowFrom` to enable self-chat mode (ignores native @-mentions, only responds to text patterns):
+
+```json5
+{
+ channels: {
+ whatsapp: {
+ allowFrom: ["+15555550123"],
+ groups: { "*": { requireMention: true } },
+ },
+ },
+ agents: {
+ list: [
+ {
+ id: "main",
+ groupChat: { mentionPatterns: ["reisponde", "@openclaw"] },
+ },
+ ],
+ },
+}
+```
+
+### Commands (chat command handling)
+
+```json5
+{
+ commands: {
+ native: "auto", // register native commands when supported
+ text: true, // parse /commands in chat messages
+ bash: false, // allow ! (alias: /bash)
+ bashForegroundMs: 2000,
+ config: false, // allow /config
+ debug: false, // allow /debug
+ restart: false, // allow /restart + gateway restart tool
+ allowFrom: {
+ "*": ["user1"],
+ discord: ["user:123"],
+ },
+ useAccessGroups: true,
+ },
+}
+```
+
+
+
+- Text commands must be **standalone** messages with leading `/`.
+- `native: "auto"` turns on native commands for Discord/Telegram, leaves Slack off.
+- Override per channel: `channels.discord.commands.native` (bool or `"auto"`). `false` clears previously registered commands.
+- `channels.telegram.customCommands` adds extra Telegram bot menu entries.
+- `bash: true` enables `! ` for host shell. Requires `tools.elevated.enabled` and sender in `tools.elevated.allowFrom.`.
+- `config: true` enables `/config` (reads/writes `openclaw.json`).
+- `channels..configWrites` gates config mutations per channel (default: true).
+- `allowFrom` is per-provider. When set, it is the **only** authorization source (channel allowlists/pairing and `useAccessGroups` are ignored).
+- `useAccessGroups: false` allows commands to bypass access-group policies when `allowFrom` is not set.
+
+
+
+---
+
+## Agent defaults
+
+### `agents.defaults.workspace`
+
+Default: `~/.openclaw/workspace`.
+
+```json5
+{
+ agents: { defaults: { workspace: "~/.openclaw/workspace" } },
+}
+```
+
+### `agents.defaults.repoRoot`
+
+Optional repository root shown in the system prompt's Runtime line. If unset, OpenClaw auto-detects by walking upward from the workspace.
+
+```json5
+{
+ agents: { defaults: { repoRoot: "~/Projects/openclaw" } },
+}
+```
+
+### `agents.defaults.skipBootstrap`
+
+Disables automatic creation of workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`).
+
+```json5
+{
+ agents: { defaults: { skipBootstrap: true } },
+}
+```
+
+### `agents.defaults.bootstrapMaxChars`
+
+Max characters per workspace bootstrap file before truncation. Default: `20000`.
+
+```json5
+{
+ agents: { defaults: { bootstrapMaxChars: 20000 } },
+}
+```
+
+### `agents.defaults.userTimezone`
+
+Timezone for system prompt context (not message timestamps). Falls back to host timezone.
+
+```json5
+{
+ agents: { defaults: { userTimezone: "America/Chicago" } },
+}
+```
+
+### `agents.defaults.timeFormat`
+
+Time format in system prompt. Default: `auto` (OS preference).
+
+```json5
+{
+ agents: { defaults: { timeFormat: "auto" } }, // auto | 12 | 24
+}
+```
+
+### `agents.defaults.model`
+
+```json5
+{
+ agents: {
+ defaults: {
+ models: {
+ "anthropic/claude-opus-4-6": { alias: "opus" },
+ "minimax/MiniMax-M2.1": { alias: "minimax" },
+ },
+ model: {
+ primary: "anthropic/claude-opus-4-6",
+ fallbacks: ["minimax/MiniMax-M2.1"],
+ },
+ imageModel: {
+ primary: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
+ fallbacks: ["openrouter/google/gemini-2.0-flash-vision:free"],
+ },
+ thinkingDefault: "low",
+ verboseDefault: "off",
+ elevatedDefault: "on",
+ timeoutSeconds: 600,
+ mediaMaxMb: 5,
+ contextTokens: 200000,
+ maxConcurrent: 3,
+ },
+ },
+}
+```
+
+- `model.primary`: format `provider/model` (e.g. `anthropic/claude-opus-4-6`). If you omit the provider, OpenClaw assumes `anthropic` (deprecated).
+- `models`: the configured model catalog and allowlist for `/model`. Each entry can include `alias` (shortcut) and `params` (provider-specific: `temperature`, `maxTokens`).
+- `imageModel`: only used if the primary model lacks image input.
+- `maxConcurrent`: max parallel agent runs across sessions (each session still serialized). Default: 1.
+
+**Built-in alias shorthands** (only apply when the model is in `agents.defaults.models`):
+
+| Alias | Model |
+| -------------- | ------------------------------- |
+| `opus` | `anthropic/claude-opus-4-6` |
+| `sonnet` | `anthropic/claude-sonnet-4-5` |
+| `gpt` | `openai/gpt-5.2` |
+| `gpt-mini` | `openai/gpt-5-mini` |
+| `gemini` | `google/gemini-3-pro-preview` |
+| `gemini-flash` | `google/gemini-3-flash-preview` |
+
+Your configured aliases always win over defaults.
+
+Z.AI GLM-4.x models automatically enable thinking mode unless you set `--thinking off` or define `agents.defaults.models["zai/"].params.thinking` yourself.
+
+### `agents.defaults.cliBackends`
+
+Optional CLI backends for text-only fallback runs (no tool calls). Useful as a backup when API providers fail.
+
+```json5
+{
+ agents: {
+ defaults: {
+ cliBackends: {
+ "claude-cli": {
+ command: "/opt/homebrew/bin/claude",
+ },
+ "my-cli": {
+ command: "my-cli",
+ args: ["--json"],
+ output: "json",
+ modelArg: "--model",
+ sessionArg: "--session",
+ sessionMode: "existing",
+ systemPromptArg: "--system",
+ systemPromptWhen: "first",
+ imageArg: "--image",
+ imageMode: "repeat",
+ },
+ },
+ },
+ },
+}
+```
+
+- CLI backends are text-first; tools are always disabled.
+- Sessions supported when `sessionArg` is set.
+- Image pass-through supported when `imageArg` accepts file paths.
+
+### `agents.defaults.heartbeat`
+
+Periodic heartbeat runs.
+
+```json5
+{
+ agents: {
+ defaults: {
+ heartbeat: {
+ every: "30m", // 0m disables
+ model: "openai/gpt-5.2-mini",
+ includeReasoning: false,
+ session: "main",
+ to: "+15555550123",
+ target: "last", // last | whatsapp | telegram | discord | ... | none
+ prompt: "Read HEARTBEAT.md if it exists...",
+ ackMaxChars: 300,
+ },
+ },
+ },
+}
+```
+
+- `every`: duration string (ms/s/m/h). Default: `30m`.
+- Per-agent: set `agents.list[].heartbeat`. When any agent defines `heartbeat`, **only those agents** run heartbeats.
+- Heartbeats run full agent turns — shorter intervals burn more tokens.
+
+### `agents.defaults.compaction`
+
+```json5
+{
+ agents: {
+ defaults: {
+ compaction: {
+ mode: "safeguard", // default | safeguard
+ reserveTokensFloor: 24000,
+ memoryFlush: {
+ enabled: true,
+ softThresholdTokens: 6000,
+ systemPrompt: "Session nearing compaction. Store durable memories now.",
+ prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
+ },
+ },
+ },
+ },
+}
+```
+
+- `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction).
+- `memoryFlush`: silent agentic turn before auto-compaction to store durable memories. Skipped when workspace is read-only.
+
+### `agents.defaults.contextPruning`
+
+Prunes **old tool results** from in-memory context before sending to the LLM. Does **not** modify session history on disk.
+
+```json5
+{
+ agents: {
+ defaults: {
+ contextPruning: {
+ mode: "cache-ttl", // off | cache-ttl
+ ttl: "1h", // duration (ms/s/m/h), default unit: minutes
+ keepLastAssistants: 3,
+ softTrimRatio: 0.3,
+ hardClearRatio: 0.5,
+ minPrunableToolChars: 50000,
+ softTrim: { maxChars: 4000, headChars: 1500, tailChars: 1500 },
+ hardClear: { enabled: true, placeholder: "[Old tool result content cleared]" },
+ tools: { deny: ["browser", "canvas"] },
+ },
+ },
+ },
+}
+```
+
+
+
+- `mode: "cache-ttl"` enables pruning passes.
+- `ttl` controls how often pruning can run again (after the last cache touch).
+- Pruning soft-trims oversized tool results first, then hard-clears older tool results if needed.
+
+**Soft-trim** keeps beginning + end and inserts `...` in the middle.
+
+**Hard-clear** replaces the entire tool result with the placeholder.
+
+Notes:
+
+- Image blocks are never trimmed/cleared.
+- Ratios are character-based (approximate), not exact token counts.
+- If fewer than `keepLastAssistants` assistant messages exist, pruning is skipped.
+
+
+
+See [Session Pruning](/concepts/session-pruning) for behavior details.
+
+### Block streaming
+
+```json5
+{
+ agents: {
+ defaults: {
+ blockStreamingDefault: "off", // on | off
+ blockStreamingBreak: "text_end", // text_end | message_end
+ blockStreamingChunk: { minChars: 800, maxChars: 1200 },
+ blockStreamingCoalesce: { idleMs: 1000 },
+ humanDelay: { mode: "natural" }, // off | natural | custom (use minMs/maxMs)
+ },
+ },
+}
+```
+
+- Non-Telegram channels require explicit `*.blockStreaming: true` to enable block replies.
+- Channel overrides: `channels..blockStreamingCoalesce` (and per-account variants). Signal/Slack/Discord/Google Chat default `minChars: 1500`.
+- `humanDelay`: randomized pause between block replies. `natural` = 800–2500ms. Per-agent override: `agents.list[].humanDelay`.
+
+See [Streaming](/concepts/streaming) for behavior + chunking details.
+
+### Typing indicators
+
+```json5
+{
+ agents: {
+ defaults: {
+ typingMode: "instant", // never | instant | thinking | message
+ typingIntervalSeconds: 6,
+ },
+ },
+}
+```
+
+- Defaults: `instant` for direct chats/mentions, `message` for unmentioned group chats.
+- Per-session overrides: `session.typingMode`, `session.typingIntervalSeconds`.
+
+See [Typing Indicators](/concepts/typing-indicators).
+
+### `agents.defaults.sandbox`
+
+Optional **Docker sandboxing** for the embedded agent. See [Sandboxing](/gateway/sandboxing) for the full guide.
+
+```json5
+{
+ agents: {
+ defaults: {
+ sandbox: {
+ mode: "non-main", // off | non-main | all
+ scope: "agent", // session | agent | shared
+ workspaceAccess: "none", // none | ro | rw
+ workspaceRoot: "~/.openclaw/sandboxes",
+ docker: {
+ image: "openclaw-sandbox:bookworm-slim",
+ containerPrefix: "openclaw-sbx-",
+ workdir: "/workspace",
+ readOnlyRoot: true,
+ tmpfs: ["/tmp", "/var/tmp", "/run"],
+ network: "none",
+ user: "1000:1000",
+ capDrop: ["ALL"],
+ env: { LANG: "C.UTF-8" },
+ setupCommand: "apt-get update && apt-get install -y git curl jq",
+ pidsLimit: 256,
+ memory: "1g",
+ memorySwap: "2g",
+ cpus: 1,
+ ulimits: {
+ nofile: { soft: 1024, hard: 2048 },
+ nproc: 256,
+ },
+ seccompProfile: "/path/to/seccomp.json",
+ apparmorProfile: "openclaw-sandbox",
+ dns: ["1.1.1.1", "8.8.8.8"],
+ extraHosts: ["internal.service:10.0.0.5"],
+ binds: ["/home/user/source:/source:rw"],
+ },
+ browser: {
+ enabled: false,
+ image: "openclaw-sandbox-browser:bookworm-slim",
+ cdpPort: 9222,
+ vncPort: 5900,
+ noVncPort: 6080,
+ headless: false,
+ enableNoVnc: true,
+ allowHostControl: false,
+ autoStart: true,
+ autoStartTimeoutMs: 12000,
+ },
+ prune: {
+ idleHours: 24,
+ maxAgeDays: 7,
+ },
+ },
+ },
+ },
+ tools: {
+ sandbox: {
+ tools: {
+ allow: [
+ "exec",
+ "process",
+ "read",
+ "write",
+ "edit",
+ "apply_patch",
+ "sessions_list",
+ "sessions_history",
+ "sessions_send",
+ "sessions_spawn",
+ "session_status",
+ ],
+ deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"],
+ },
+ },
+ },
+}
+```
+
+
+
+**Workspace access:**
+
+- `none`: per-scope sandbox workspace under `~/.openclaw/sandboxes`
+- `ro`: sandbox workspace at `/workspace`, agent workspace mounted read-only at `/agent`
+- `rw`: agent workspace mounted read/write at `/workspace`
+
+**Scope:**
+
+- `session`: per-session container + workspace
+- `agent`: one container + workspace per agent (default)
+- `shared`: shared container and workspace (no cross-session isolation)
+
+**`setupCommand`** runs once after container creation (via `sh -lc`). Needs network egress, writable root, root user.
+
+**Containers default to `network: "none"`** — set to `"bridge"` if the agent needs outbound access.
+
+**Inbound attachments** are staged into `media/inbound/*` in the active workspace.
+
+**`docker.binds`** mounts additional host directories; global and per-agent binds are merged.
+
+**Sandboxed browser** (`sandbox.browser.enabled`): Chromium + CDP in a container. noVNC URL injected into system prompt. Does not require `browser.enabled` in main config.
+
+- `allowHostControl: false` (default) blocks sandboxed sessions from targeting the host browser.
+
+
+
+Build images:
+
+```bash
+scripts/sandbox-setup.sh # main sandbox image
+scripts/sandbox-browser-setup.sh # optional browser image
+```
+
+### `agents.list` (per-agent overrides)
+
+```json5
+{
+ agents: {
+ list: [
+ {
+ id: "main",
+ default: true,
+ name: "Main Agent",
+ workspace: "~/.openclaw/workspace",
+ agentDir: "~/.openclaw/agents/main/agent",
+ model: "anthropic/claude-opus-4-6", // or { primary, fallbacks }
+ identity: {
+ name: "Samantha",
+ theme: "helpful sloth",
+ emoji: "🦥",
+ avatar: "avatars/samantha.png",
+ },
+ groupChat: { mentionPatterns: ["@openclaw"] },
+ sandbox: { mode: "off" },
+ subagents: { allowAgents: ["*"] },
+ tools: {
+ profile: "coding",
+ allow: ["browser"],
+ deny: ["canvas"],
+ elevated: { enabled: true },
+ },
+ },
+ ],
+ },
+}
+```
+
+- `id`: stable agent id (required).
+- `default`: when multiple are set, first wins (warning logged). If none set, first list entry is default.
+- `model`: string form overrides `primary` only; object form `{ primary, fallbacks }` overrides both (`[]` disables global fallbacks).
+- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
+- `identity` derives defaults: `ackReaction` from `emoji`, `mentionPatterns` from `name`/`emoji`.
+- `subagents.allowAgents`: allowlist of agent ids for `sessions_spawn` (`["*"]` = any; default: same agent only).
+
+---
+
+## Multi-agent routing
+
+Run multiple isolated agents inside one Gateway. See [Multi-Agent](/concepts/multi-agent).
+
+```json5
+{
+ agents: {
+ list: [
+ { id: "home", default: true, workspace: "~/.openclaw/workspace-home" },
+ { id: "work", workspace: "~/.openclaw/workspace-work" },
+ ],
+ },
+ bindings: [
+ { agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
+ { agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
+ ],
+}
+```
+
+### Binding match fields
+
+- `match.channel` (required)
+- `match.accountId` (optional; `*` = any account; omitted = default account)
+- `match.peer` (optional; `{ kind: direct|group|channel, id }`)
+- `match.guildId` / `match.teamId` (optional; channel-specific)
+
+**Deterministic match order:**
+
+1. `match.peer`
+2. `match.guildId`
+3. `match.teamId`
+4. `match.accountId` (exact, no peer/guild/team)
+5. `match.accountId: "*"` (channel-wide)
+6. Default agent
+
+Within each tier, the first matching `bindings` entry wins.
+
+### Per-agent access profiles
+
+
+
+```json5
+{
+ agents: {
+ list: [
+ {
+ id: "personal",
+ workspace: "~/.openclaw/workspace-personal",
+ sandbox: { mode: "off" },
+ },
+ ],
+ },
+}
+```
+
+
+
+
+
+```json5
+{
+ agents: {
+ list: [
+ {
+ id: "family",
+ workspace: "~/.openclaw/workspace-family",
+ sandbox: { mode: "all", scope: "agent", workspaceAccess: "ro" },
+ tools: {
+ allow: [
+ "read",
+ "sessions_list",
+ "sessions_history",
+ "sessions_send",
+ "sessions_spawn",
+ "session_status",
+ ],
+ deny: ["write", "edit", "apply_patch", "exec", "process", "browser"],
+ },
+ },
+ ],
+ },
+}
+```
+
+
+
+
+
+```json5
+{
+ agents: {
+ list: [
+ {
+ id: "public",
+ workspace: "~/.openclaw/workspace-public",
+ sandbox: { mode: "all", scope: "agent", workspaceAccess: "none" },
+ tools: {
+ allow: [
+ "sessions_list",
+ "sessions_history",
+ "sessions_send",
+ "sessions_spawn",
+ "session_status",
+ "whatsapp",
+ "telegram",
+ "slack",
+ "discord",
+ "gateway",
+ ],
+ deny: [
+ "read",
+ "write",
+ "edit",
+ "apply_patch",
+ "exec",
+ "process",
+ "browser",
+ "canvas",
+ "nodes",
+ "cron",
+ "gateway",
+ "image",
+ ],
+ },
+ },
+ ],
+ },
+}
+```
+
+
+
+See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for precedence details.
+
+---
+
+## Session
+
+```json5
+{
+ session: {
+ scope: "per-sender",
+ dmScope: "main", // main | per-peer | per-channel-peer | per-account-channel-peer
+ identityLinks: {
+ alice: ["telegram:123456789", "discord:987654321012345678"],
+ },
+ reset: {
+ mode: "daily", // daily | idle
+ atHour: 4,
+ idleMinutes: 60,
+ },
+ resetByType: {
+ thread: { mode: "daily", atHour: 4 },
+ direct: { mode: "idle", idleMinutes: 240 },
+ group: { mode: "idle", idleMinutes: 120 },
+ },
+ resetTriggers: ["/new", "/reset"],
+ store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
+ maintenance: {
+ mode: "warn", // warn | enforce
+ pruneAfter: "30d",
+ maxEntries: 500,
+ rotateBytes: "10mb",
+ },
+ mainKey: "main", // legacy (runtime always uses "main")
+ agentToAgent: { maxPingPongTurns: 5 },
+ sendPolicy: {
+ rules: [{ action: "deny", match: { channel: "discord", chatType: "group" } }],
+ default: "allow",
+ },
+ },
+}
+```
+
+
+
+- **`dmScope`**: how DMs are grouped.
+ - `main`: all DMs share the main session.
+ - `per-peer`: isolate by sender id across channels.
+ - `per-channel-peer`: isolate per channel + sender (recommended for multi-user inboxes).
+ - `per-account-channel-peer`: isolate per account + channel + sender (recommended for multi-account).
+- **`identityLinks`**: map canonical ids to provider-prefixed peers for cross-channel session sharing.
+- **`reset`**: primary reset policy. `daily` resets at `atHour` local time; `idle` resets after `idleMinutes`. When both configured, whichever expires first wins.
+- **`resetByType`**: per-type overrides (`direct`, `group`, `thread`). Legacy `dm` accepted as alias for `direct`.
+- **`mainKey`**: legacy field. Runtime now always uses `"main"` for the main direct-chat bucket.
+- **`sendPolicy`**: match by `channel`, `chatType` (`direct|group|channel`, with legacy `dm` alias), or `keyPrefix`. First deny wins.
+- **`maintenance`**: `warn` warns the active session on eviction; `enforce` applies pruning and rotation.
+
+
+
+---
+
+## Messages
+
+```json5
+{
+ messages: {
+ responsePrefix: "🦞", // or "auto"
+ ackReaction: "👀",
+ ackReactionScope: "group-mentions", // group-mentions | group-all | direct | all
+ removeAckAfterReply: false,
+ queue: {
+ mode: "collect", // steer | followup | collect | steer-backlog | steer+backlog | queue | interrupt
+ debounceMs: 1000,
+ cap: 20,
+ drop: "summarize", // old | new | summarize
+ byChannel: {
+ whatsapp: "collect",
+ telegram: "collect",
+ },
+ },
+ inbound: {
+ debounceMs: 2000, // 0 disables
+ byChannel: {
+ whatsapp: 5000,
+ slack: 1500,
+ },
+ },
+ },
+}
+```
+
+### Response prefix
+
+Per-channel/account overrides: `channels..responsePrefix`, `channels..accounts..responsePrefix`.
+
+Resolution (most specific wins): account → channel → global. `""` disables and stops cascade. `"auto"` derives `[{identity.name}]`.
+
+**Template variables:**
+
+| Variable | Description | Example |
+| ----------------- | ---------------------- | --------------------------- |
+| `{model}` | Short model name | `claude-opus-4-6` |
+| `{modelFull}` | Full model identifier | `anthropic/claude-opus-4-6` |
+| `{provider}` | Provider name | `anthropic` |
+| `{thinkingLevel}` | Current thinking level | `high`, `low`, `off` |
+| `{identity.name}` | Agent identity name | (same as `"auto"`) |
+
+Variables are case-insensitive. `{think}` is an alias for `{thinkingLevel}`.
+
+### Ack reaction
+
+- Defaults to active agent's `identity.emoji`, otherwise `"👀"`. Set `""` to disable.
+- Scope: `group-mentions` (default), `group-all`, `direct`, `all`.
+- `removeAckAfterReply`: removes ack after reply (Slack/Discord/Telegram/Google Chat only).
+
+### Inbound debounce
+
+Batches rapid text-only messages from the same sender into a single agent turn. Media/attachments flush immediately. Control commands bypass debouncing.
+
+### TTS (text-to-speech)
+
+```json5
+{
+ messages: {
+ tts: {
+ auto: "always", // off | always | inbound | tagged
+ mode: "final", // final | all
+ provider: "elevenlabs",
+ summaryModel: "openai/gpt-4.1-mini",
+ modelOverrides: { enabled: true },
+ maxTextLength: 4000,
+ timeoutMs: 30000,
+ prefsPath: "~/.openclaw/settings/tts.json",
+ elevenlabs: {
+ apiKey: "elevenlabs_api_key",
+ baseUrl: "https://api.elevenlabs.io",
+ voiceId: "voice_id",
+ modelId: "eleven_multilingual_v2",
+ seed: 42,
+ applyTextNormalization: "auto",
+ languageCode: "en",
+ voiceSettings: {
+ stability: 0.5,
+ similarityBoost: 0.75,
+ style: 0.0,
+ useSpeakerBoost: true,
+ speed: 1.0,
+ },
+ },
+ openai: {
+ apiKey: "openai_api_key",
+ model: "gpt-4o-mini-tts",
+ voice: "alloy",
+ },
+ },
+ },
+}
+```
+
+- `auto` controls auto-TTS. `/tts off|always|inbound|tagged` overrides per session.
+- `summaryModel` overrides `agents.defaults.model.primary` for auto-summary.
+- API keys fall back to `ELEVENLABS_API_KEY`/`XI_API_KEY` and `OPENAI_API_KEY`.
+
+---
+
+## Talk
+
+Defaults for Talk mode (macOS/iOS/Android).
+
+```json5
+{
+ talk: {
+ voiceId: "elevenlabs_voice_id",
+ voiceAliases: {
+ Clawd: "EXAVITQu4vr4xnSDxMaL",
+ Roger: "CwhRBWXzGAHq8TQ4Fs17",
+ },
+ modelId: "eleven_v3",
+ outputFormat: "mp3_44100_128",
+ apiKey: "elevenlabs_api_key",
+ interruptOnSpeech: true,
+ },
+}
+```
+
+- Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID`.
+- `apiKey` falls back to `ELEVENLABS_API_KEY`.
+- `voiceAliases` lets Talk directives use friendly names.
+
+---
+
+## Tools
+
+### Tool profiles
+
+`tools.profile` sets a base allowlist before `tools.allow`/`tools.deny`:
+
+| Profile | Includes |
+| ----------- | ----------------------------------------------------------------------------------------- |
+| `minimal` | `session_status` only |
+| `coding` | `group:fs`, `group:runtime`, `group:sessions`, `group:memory`, `image` |
+| `messaging` | `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status` |
+| `full` | No restriction (same as unset) |
+
+### Tool groups
+
+| Group | Tools |
+| ------------------ | ---------------------------------------------------------------------------------------- |
+| `group:runtime` | `exec`, `process` (`bash` is accepted as an alias for `exec`) |
+| `group:fs` | `read`, `write`, `edit`, `apply_patch` |
+| `group:sessions` | `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` |
+| `group:memory` | `memory_search`, `memory_get` |
+| `group:web` | `web_search`, `web_fetch` |
+| `group:ui` | `browser`, `canvas` |
+| `group:automation` | `cron`, `gateway` |
+| `group:messaging` | `message` |
+| `group:nodes` | `nodes` |
+| `group:openclaw` | All built-in tools (excludes provider plugins) |
+
+### `tools.allow` / `tools.deny`
+
+Global tool allow/deny policy (deny wins). Case-insensitive, supports `*` wildcards. Applied even when Docker sandbox is off.
+
+```json5
+{
+ tools: { deny: ["browser", "canvas"] },
+}
+```
+
+### `tools.byProvider`
+
+Further restrict tools for specific providers or models. Order: base profile → provider profile → allow/deny.
+
+```json5
+{
+ tools: {
+ profile: "coding",
+ byProvider: {
+ "google-antigravity": { profile: "minimal" },
+ "openai/gpt-5.2": { allow: ["group:fs", "sessions_list"] },
+ },
+ },
+}
+```
+
+### `tools.elevated`
+
+Controls elevated (host) exec access:
+
+```json5
+{
+ tools: {
+ elevated: {
+ enabled: true,
+ allowFrom: {
+ whatsapp: ["+15555550123"],
+ discord: ["steipete", "1234567890123"],
+ },
+ },
+ },
+}
+```
+
+- Per-agent override (`agents.list[].tools.elevated`) can only further restrict.
+- `/elevated on|off|ask|full` stores state per session; inline directives apply to single message.
+- Elevated `exec` runs on the host, bypasses sandboxing.
+
+### `tools.exec`
+
+```json5
+{
+ tools: {
+ exec: {
+ backgroundMs: 10000,
+ timeoutSec: 1800,
+ cleanupMs: 1800000,
+ notifyOnExit: true,
+ applyPatch: {
+ enabled: false,
+ allowModels: ["gpt-5.2"],
+ },
+ },
+ },
+}
+```
+
+### `tools.web`
+
+```json5
+{
+ tools: {
+ web: {
+ search: {
+ enabled: true,
+ apiKey: "brave_api_key", // or BRAVE_API_KEY env
+ maxResults: 5,
+ timeoutSeconds: 30,
+ cacheTtlMinutes: 15,
+ },
+ fetch: {
+ enabled: true,
+ maxChars: 50000,
+ maxCharsCap: 50000,
+ timeoutSeconds: 30,
+ cacheTtlMinutes: 15,
+ userAgent: "custom-ua",
+ },
+ },
+ },
+}
+```
+
+### `tools.media`
+
+Configures inbound media understanding (image/audio/video):
+
+```json5
+{
+ tools: {
+ media: {
+ concurrency: 2,
+ audio: {
+ enabled: true,
+ maxBytes: 20971520,
+ scope: {
+ default: "deny",
+ rules: [{ action: "allow", match: { chatType: "direct" } }],
+ },
+ models: [
+ { provider: "openai", model: "gpt-4o-mini-transcribe" },
+ { type: "cli", command: "whisper", args: ["--model", "base", "{{MediaPath}}"] },
+ ],
+ },
+ video: {
+ enabled: true,
+ maxBytes: 52428800,
+ models: [{ provider: "google", model: "gemini-3-flash-preview" }],
+ },
+ },
+ },
+}
+```
+
+
+
+**Provider entry** (`type: "provider"` or omitted):
+
+- `provider`: API provider id (`openai`, `anthropic`, `google`/`gemini`, `groq`, etc.)
+- `model`: model id override
+- `profile` / `preferredProfile`: auth profile selection
+
+**CLI entry** (`type: "cli"`):
+
+- `command`: executable to run
+- `args`: templated args (supports `{{MediaPath}}`, `{{Prompt}}`, `{{MaxChars}}`, etc.)
+
+**Common fields:**
+
+- `capabilities`: optional list (`image`, `audio`, `video`). Defaults: `openai`/`anthropic`/`minimax` → image, `google` → image+audio+video, `groq` → audio.
+- `prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language`: per-entry overrides.
+- Failures fall back to the next entry.
+
+Provider auth follows standard order: auth profiles → env vars → `models.providers.*.apiKey`.
+
+
+
+### `tools.agentToAgent`
+
+```json5
+{
+ tools: {
+ agentToAgent: {
+ enabled: false,
+ allow: ["home", "work"],
+ },
+ },
+}
+```
+
+### `tools.subagents`
+
+```json5
+{
+ agents: {
+ defaults: {
+ subagents: {
+ model: "minimax/MiniMax-M2.1",
+ maxConcurrent: 1,
+ archiveAfterMinutes: 60,
+ },
+ },
+ },
+}
+```
+
+- `model`: default model for spawned sub-agents. If omitted, sub-agents inherit the caller's model.
+- Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny`.
+
+---
+
+## Custom providers and base URLs
+
+OpenClaw uses the pi-coding-agent model catalog. Add custom providers via `models.providers` in config or `~/.openclaw/agents//agent/models.json`.
+
+```json5
+{
+ models: {
+ mode: "merge", // merge (default) | replace
+ providers: {
+ "custom-proxy": {
+ baseUrl: "http://localhost:4000/v1",
+ apiKey: "LITELLM_KEY",
+ api: "openai-completions", // openai-completions | openai-responses | anthropic-messages | google-generative-ai
+ models: [
+ {
+ id: "llama-3.1-8b",
+ name: "Llama 3.1 8B",
+ reasoning: false,
+ input: ["text"],
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: 128000,
+ maxTokens: 32000,
+ },
+ ],
+ },
+ },
+ },
+}
+```
+
+- Use `authHeader: true` + `headers` for custom auth needs.
+- Override agent config root with `OPENCLAW_AGENT_DIR` (or `PI_CODING_AGENT_DIR`).
+
+### Provider examples
+
+
+
+```json5
+{
+ env: { CEREBRAS_API_KEY: "sk-..." },
+ agents: {
+ defaults: {
+ model: {
+ primary: "cerebras/zai-glm-4.7",
+ fallbacks: ["cerebras/zai-glm-4.6"],
+ },
+ models: {
+ "cerebras/zai-glm-4.7": { alias: "GLM 4.7 (Cerebras)" },
+ "cerebras/zai-glm-4.6": { alias: "GLM 4.6 (Cerebras)" },
+ },
+ },
+ },
+ models: {
+ mode: "merge",
+ providers: {
+ cerebras: {
+ baseUrl: "https://api.cerebras.ai/v1",
+ apiKey: "${CEREBRAS_API_KEY}",
+ api: "openai-completions",
+ models: [
+ { id: "zai-glm-4.7", name: "GLM 4.7 (Cerebras)" },
+ { id: "zai-glm-4.6", name: "GLM 4.6 (Cerebras)" },
+ ],
+ },
+ },
+ },
+}
+```
+
+Use `cerebras/zai-glm-4.7` for Cerebras; `zai/glm-4.7` for Z.AI direct.
+
+
+
+
+
+```json5
+{
+ agents: {
+ defaults: {
+ model: { primary: "opencode/claude-opus-4-6" },
+ models: { "opencode/claude-opus-4-6": { alias: "Opus" } },
+ },
+ },
+}
+```
+
+Set `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`). Shortcut: `openclaw onboard --auth-choice opencode-zen`.
+
+
+
+
+
+```json5
+{
+ agents: {
+ defaults: {
+ model: { primary: "zai/glm-4.7" },
+ models: { "zai/glm-4.7": {} },
+ },
+ },
+}
+```
+
+Set `ZAI_API_KEY`. `z.ai/*` and `z-ai/*` are accepted aliases. Shortcut: `openclaw onboard --auth-choice zai-api-key`.
+
+- General endpoint: `https://api.z.ai/api/paas/v4`
+- Coding endpoint (default): `https://api.z.ai/api/coding/paas/v4`
+- For the general endpoint, define a custom provider with the base URL override.
+
+
+
+
+
+```json5
+{
+ env: { MOONSHOT_API_KEY: "sk-..." },
+ agents: {
+ defaults: {
+ model: { primary: "moonshot/kimi-k2.5" },
+ models: { "moonshot/kimi-k2.5": { alias: "Kimi K2.5" } },
+ },
+ },
+ models: {
+ mode: "merge",
+ providers: {
+ moonshot: {
+ baseUrl: "https://api.moonshot.ai/v1",
+ apiKey: "${MOONSHOT_API_KEY}",
+ api: "openai-completions",
+ models: [
+ {
+ id: "kimi-k2.5",
+ name: "Kimi K2.5",
+ reasoning: false,
+ input: ["text"],
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: 256000,
+ maxTokens: 8192,
+ },
+ ],
+ },
+ },
+ },
+}
+```
+
+For the China endpoint: `baseUrl: "https://api.moonshot.cn/v1"` or `openclaw onboard --auth-choice moonshot-api-key-cn`.
+
+
+
+
+
+```json5
+{
+ env: { KIMI_API_KEY: "sk-..." },
+ agents: {
+ defaults: {
+ model: { primary: "kimi-coding/k2p5" },
+ models: { "kimi-coding/k2p5": { alias: "Kimi K2.5" } },
+ },
+ },
+}
+```
+
+Anthropic-compatible, built-in provider. Shortcut: `openclaw onboard --auth-choice kimi-code-api-key`.
+
+
+
+
+
+```json5
+{
+ env: { SYNTHETIC_API_KEY: "sk-..." },
+ agents: {
+ defaults: {
+ model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.1" },
+ models: { "synthetic/hf:MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax M2.1" } },
+ },
+ },
+ models: {
+ mode: "merge",
+ providers: {
+ synthetic: {
+ baseUrl: "https://api.synthetic.new/anthropic",
+ apiKey: "${SYNTHETIC_API_KEY}",
+ api: "anthropic-messages",
+ models: [
+ {
+ id: "hf:MiniMaxAI/MiniMax-M2.1",
+ name: "MiniMax M2.1",
+ reasoning: false,
+ input: ["text"],
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
+ contextWindow: 192000,
+ maxTokens: 65536,
+ },
+ ],
+ },
+ },
+ },
+}
+```
+
+Base URL should omit `/v1` (Anthropic client appends it). Shortcut: `openclaw onboard --auth-choice synthetic-api-key`.
+
+
+
+
+
+```json5
+{
+ agents: {
+ defaults: {
+ model: { primary: "minimax/MiniMax-M2.1" },
+ models: {
+ "minimax/MiniMax-M2.1": { alias: "Minimax" },
+ },
+ },
+ },
+ models: {
+ mode: "merge",
+ providers: {
+ minimax: {
+ baseUrl: "https://api.minimax.io/anthropic",
+ apiKey: "${MINIMAX_API_KEY}",
+ api: "anthropic-messages",
+ models: [
+ {
+ id: "MiniMax-M2.1",
+ name: "MiniMax M2.1",
+ reasoning: false,
+ input: ["text"],
+ cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
+ contextWindow: 200000,
+ maxTokens: 8192,
+ },
+ ],
+ },
+ },
+ },
+}
+```
+
+Set `MINIMAX_API_KEY`. Shortcut: `openclaw onboard --auth-choice minimax-api`.
+
+
+
+
+
+See [Local Models](/gateway/local-models). TL;DR: run MiniMax M2.1 via LM Studio Responses API on serious hardware; keep hosted models merged for fallback.
+
+
+
+---
+
+## Skills
+
+```json5
+{
+ skills: {
+ allowBundled: ["gemini", "peekaboo"],
+ load: {
+ extraDirs: ["~/Projects/agent-scripts/skills"],
+ },
+ install: {
+ preferBrew: true,
+ nodeManager: "npm", // npm | pnpm | yarn
+ },
+ entries: {
+ "nano-banana-pro": {
+ apiKey: "GEMINI_KEY_HERE",
+ env: { GEMINI_API_KEY: "GEMINI_KEY_HERE" },
+ },
+ peekaboo: { enabled: true },
+ sag: { enabled: false },
+ },
+ },
+}
+```
+
+- `allowBundled`: optional allowlist for bundled skills only (managed/workspace skills unaffected).
+- `entries..enabled: false` disables a skill even if bundled/installed.
+- `entries..apiKey`: convenience for skills declaring a primary env var.
+
+---
+
+## Plugins
+
+```json5
+{
+ plugins: {
+ enabled: true,
+ allow: ["voice-call"],
+ deny: [],
+ load: {
+ paths: ["~/Projects/oss/voice-call-extension"],
+ },
+ entries: {
+ "voice-call": {
+ enabled: true,
+ config: { provider: "twilio" },
+ },
+ },
+ },
+}
+```
+
+- Loaded from `~/.openclaw/extensions`, `/.openclaw/extensions`, plus `plugins.load.paths`.
+- **Config changes require a gateway restart.**
+- `allow`: optional allowlist (only listed plugins load). `deny` wins.
+
+See [Plugins](/tools/plugin).
+
+---
+
+## Browser
+
+```json5
+{
+ browser: {
+ enabled: true,
+ evaluateEnabled: true,
+ defaultProfile: "chrome",
+ profiles: {
+ openclaw: { cdpPort: 18800, color: "#FF4500" },
+ work: { cdpPort: 18801, color: "#0066CC" },
+ remote: { cdpUrl: "http://10.0.0.42:9222", color: "#00AA00" },
+ },
+ color: "#FF4500",
+ // headless: false,
+ // noSandbox: false,
+ // executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
+ // attachOnly: false,
+ },
+}
+```
+
+- `evaluateEnabled: false` disables `act:evaluate` and `wait --fn`.
+- Remote profiles are attach-only (start/stop/reset disabled).
+- Auto-detect order: default browser if Chromium-based → Chrome → Brave → Edge → Chromium → Chrome Canary.
+- Control service: loopback only (port derived from `gateway.port`, default `18791`).
+
+---
+
+## UI
+
+```json5
+{
+ ui: {
+ seamColor: "#FF4500",
+ assistant: {
+ name: "OpenClaw",
+ avatar: "CB", // emoji, short text, image URL, or data URI
+ },
+ },
+}
+```
+
+- `seamColor`: accent color for native app UI chrome (Talk Mode bubble tint, etc.).
+- `assistant`: Control UI identity override. Falls back to active agent identity.
+
+---
+
+## Gateway
+
+```json5
+{
+ gateway: {
+ mode: "local", // local | remote
+ port: 18789,
+ bind: "loopback",
+ auth: {
+ mode: "token", // token | password
+ token: "your-token",
+ // password: "your-password", // or OPENCLAW_GATEWAY_PASSWORD
+ allowTailscale: true,
+ },
+ tailscale: {
+ mode: "off", // off | serve | funnel
+ resetOnExit: false,
+ },
+ controlUi: {
+ enabled: true,
+ basePath: "/openclaw",
+ // root: "dist/control-ui",
+ // allowInsecureAuth: false,
+ // dangerouslyDisableDeviceAuth: false,
+ },
+ remote: {
+ url: "ws://gateway.tailnet:18789",
+ transport: "ssh", // ssh | direct
+ token: "your-token",
+ // password: "your-password",
+ },
+ trustedProxies: ["10.0.0.1"],
+ },
+}
+```
+
+
+
+- `mode`: `local` (run gateway) or `remote` (connect to remote gateway). Gateway refuses to start unless `local`.
+- `port`: single multiplexed port for WS + HTTP. Precedence: `--port` > `OPENCLAW_GATEWAY_PORT` > `gateway.port` > `18789`.
+- `bind`: `auto`, `loopback` (default), `lan` (`0.0.0.0`), `tailnet` (Tailscale IP only), or `custom`.
+- **Auth**: required by default. Non-loopback binds require a shared token/password. Onboarding wizard generates a token by default.
+- `auth.allowTailscale`: when `true`, Tailscale Serve identity headers satisfy auth (verified via `tailscale whois`). Defaults to `true` when `tailscale.mode = "serve"`.
+- `tailscale.mode`: `serve` (tailnet only, loopback bind) or `funnel` (public, requires auth).
+- `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `ws://` or `wss://`.
+- `gateway.remote.token` is for remote CLI calls only; does not enable local gateway auth.
+- `trustedProxies`: reverse proxy IPs that terminate TLS. Only list proxies you control.
+
+
+
+### OpenAI-compatible endpoints
+
+- Chat Completions: disabled by default. Enable with `gateway.http.endpoints.chatCompletions.enabled: true`.
+- Responses API: `gateway.http.endpoints.responses.enabled`.
+
+### Multi-instance isolation
+
+Run multiple gateways on one host with unique ports and state dirs:
+
+```bash
+OPENCLAW_CONFIG_PATH=~/.openclaw/a.json \
+OPENCLAW_STATE_DIR=~/.openclaw-a \
+openclaw gateway --port 19001
+```
+
+Convenience flags: `--dev` (uses `~/.openclaw-dev` + port `19001`), `--profile ` (uses `~/.openclaw-`).
+
+See [Multiple Gateways](/gateway/multiple-gateways).
+
+---
+
+## Hooks
+
+```json5
+{
+ hooks: {
+ enabled: true,
+ token: "shared-secret",
+ path: "/hooks",
+ maxBodyBytes: 262144,
+ allowedAgentIds: ["hooks", "main"],
+ presets: ["gmail"],
+ transformsDir: "~/.openclaw/hooks",
+ mappings: [
+ {
+ match: { path: "gmail" },
+ action: "agent",
+ agentId: "hooks",
+ wakeMode: "now",
+ name: "Gmail",
+ sessionKey: "hook:gmail:{{messages[0].id}}",
+ messageTemplate: "From: {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}",
+ deliver: true,
+ channel: "last",
+ model: "openai/gpt-5.2-mini",
+ },
+ ],
+ },
+}
+```
+
+Auth: `Authorization: Bearer ` or `x-openclaw-token: `.
+
+**Endpoints:**
+
+- `POST /hooks/wake` → `{ text, mode?: "now"|"next-heartbeat" }`
+- `POST /hooks/agent` → `{ message, name?, agentId?, sessionKey?, wakeMode?, deliver?, channel?, to?, model?, thinking?, timeoutSeconds? }`
+- `POST /hooks/` → resolved via `hooks.mappings`
+
+
+
+- `match.path` matches sub-path after `/hooks` (e.g. `/hooks/gmail` → `gmail`).
+- `match.source` matches a payload field for generic paths.
+- Templates like `{{messages[0].subject}}` read from the payload.
+- `transform` can point to a JS/TS module returning a hook action.
+- `agentId` routes to a specific agent; unknown IDs fall back to default.
+- `allowedAgentIds`: restricts explicit routing (`*` or omitted = allow all, `[]` = deny all).
+- `deliver: true` sends final reply to a channel; `channel` defaults to `last`.
+- `model` overrides LLM for this hook run (must be allowed if model catalog is set).
+
+
+
+### Gmail integration
+
+```json5
+{
+ hooks: {
+ gmail: {
+ account: "openclaw@gmail.com",
+ topic: "projects//topics/gog-gmail-watch",
+ subscription: "gog-gmail-watch-push",
+ pushToken: "shared-push-token",
+ hookUrl: "http://127.0.0.1:18789/hooks/gmail",
+ includeBody: true,
+ maxBytes: 20000,
+ renewEveryMinutes: 720,
+ serve: { bind: "127.0.0.1", port: 8788, path: "/" },
+ tailscale: { mode: "funnel", path: "/gmail-pubsub" },
+ model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
+ thinking: "off",
+ },
+ },
+}
+```
+
+- Gateway auto-starts `gog gmail watch serve` on boot when configured. Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to disable.
+- Don't run a separate `gog gmail watch serve` alongside the Gateway.
+
+---
+
+## Canvas host
+
+```json5
+{
+ canvasHost: {
+ root: "~/.openclaw/workspace/canvas",
+ port: 18793,
+ liveReload: true,
+ // enabled: false, // or OPENCLAW_SKIP_CANVAS_HOST=1
+ },
+}
+```
+
+- Serves HTML/CSS/JS over HTTP for iOS/Android nodes.
+- Injects live-reload client into served HTML.
+- Auto-creates starter `index.html` when empty.
+- Also serves A2UI at `/__openclaw__/a2ui/`.
+- Changes require a gateway restart.
+- Disable live reload for large directories or `EMFILE` errors.
+
+---
+
+## Discovery
+
+### mDNS (Bonjour)
+
+```json5
+{
+ discovery: {
+ mdns: {
+ mode: "minimal", // minimal | full | off
+ },
+ },
+}
+```
+
+- `minimal` (default): omit `cliPath` + `sshPort` from TXT records.
+- `full`: include `cliPath` + `sshPort`.
+- Hostname defaults to `openclaw`. Override with `OPENCLAW_MDNS_HOSTNAME`.
+
+### Wide-area (DNS-SD)
+
+```json5
+{
+ discovery: {
+ wideArea: { enabled: true },
+ },
+}
+```
+
+Writes a unicast DNS-SD zone under `~/.openclaw/dns/`. For cross-network discovery, pair with a DNS server (CoreDNS recommended) + Tailscale split DNS.
+
+Setup: `openclaw dns setup --apply`.
+
+---
+
+## Environment
+
+### `env` (inline env vars)
+
+```json5
+{
+ env: {
+ OPENROUTER_API_KEY: "sk-or-...",
+ vars: {
+ GROQ_API_KEY: "gsk-...",
+ },
+ shellEnv: {
+ enabled: true,
+ timeoutMs: 15000,
+ },
+ },
+}
+```
+
+- Inline env vars are only applied if the process env is missing the key.
+- `.env` files: CWD `.env` + `~/.openclaw/.env` (neither overrides existing vars).
+- `shellEnv`: imports missing expected keys from your login shell profile.
+- See [Environment](/help/environment) for full precedence.
+
+### Env var substitution
+
+Reference env vars in any config string with `${VAR_NAME}`:
+
+```json5
+{
+ gateway: {
+ auth: { token: "${OPENCLAW_GATEWAY_TOKEN}" },
+ },
+}
+```
+
+- Only uppercase names matched: `[A-Z_][A-Z0-9_]*`.
+- Missing/empty vars throw an error at config load.
+- Escape with `$${VAR}` for a literal `${VAR}`.
+- Works with `$include`.
+
+---
+
+## Auth storage
+
+```json5
+{
+ auth: {
+ profiles: {
+ "anthropic:me@example.com": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
+ "anthropic:work": { provider: "anthropic", mode: "api_key" },
+ },
+ order: {
+ anthropic: ["anthropic:me@example.com", "anthropic:work"],
+ },
+ },
+}
+```
+
+- Per-agent auth profiles stored at `/auth-profiles.json`.
+- Legacy OAuth imports from `~/.openclaw/credentials/oauth.json`.
+- See [OAuth](/concepts/oauth).
+
+---
+
+## Logging
+
+```json5
+{
+ logging: {
+ level: "info",
+ file: "/tmp/openclaw/openclaw.log",
+ consoleLevel: "info",
+ consoleStyle: "pretty", // pretty | compact | json
+ redactSensitive: "tools", // off | tools
+ redactPatterns: ["\\bTOKEN\\b\\s*[=:]\\s*([\"']?)([^\\s\"']+)\\1"],
+ },
+}
+```
+
+- Default log file: `/tmp/openclaw/openclaw-YYYY-MM-DD.log`.
+- Set `logging.file` for a stable path.
+- `consoleLevel` bumps to `debug` when `--verbose`.
+
+---
+
+## Wizard
+
+Metadata written by CLI wizards (`onboard`, `configure`, `doctor`):
+
+```json5
+{
+ wizard: {
+ lastRunAt: "2026-01-01T00:00:00.000Z",
+ lastRunVersion: "2026.1.4",
+ lastRunCommit: "abc1234",
+ lastRunCommand: "configure",
+ lastRunMode: "local",
+ },
+}
+```
+
+---
+
+## Identity
+
+```json5
+{
+ agents: {
+ list: [
+ {
+ id: "main",
+ identity: {
+ name: "Samantha",
+ theme: "helpful sloth",
+ emoji: "🦥",
+ avatar: "avatars/samantha.png",
+ },
+ },
+ ],
+ },
+}
+```
+
+Written by the macOS onboarding assistant. Derives defaults:
+
+- `messages.ackReaction` from `identity.emoji` (falls back to 👀)
+- `mentionPatterns` from `identity.name`/`identity.emoji`
+- `avatar` accepts: workspace-relative path, `http(s)` URL, or `data:` URI
+
+---
+
+## Bridge (legacy, removed)
+
+Current builds no longer include the TCP bridge. Nodes connect over the Gateway WebSocket. `bridge.*` keys are no longer part of the config schema (validation fails until removed; `openclaw doctor --fix` can strip unknown keys).
+
+
+
+```json
+{
+ "bridge": {
+ "enabled": true,
+ "port": 18790,
+ "bind": "tailnet",
+ "tls": {
+ "enabled": true,
+ "autoGenerate": true
+ }
+ }
+}
+```
+
+
+
+---
+
+## Cron
+
+```json5
+{
+ cron: {
+ enabled: true,
+ maxConcurrentRuns: 2,
+ sessionRetention: "24h", // duration string or false
+ },
+}
+```
+
+- `sessionRetention`: how long to keep completed cron sessions before pruning. Default: `24h`.
+
+See [Cron Jobs](/automation/cron-jobs).
+
+---
+
+## Media model template variables
+
+Template placeholders expanded in `tools.media.*.models[].args`:
+
+| Variable | Description |
+| ------------------ | ------------------------------------------------- |
+| `{{Body}}` | Full inbound message body |
+| `{{RawBody}}` | Raw body (no history/sender wrappers) |
+| `{{BodyStripped}}` | Body with group mentions stripped |
+| `{{From}}` | Sender identifier |
+| `{{To}}` | Destination identifier |
+| `{{MessageSid}}` | Channel message id |
+| `{{SessionId}}` | Current session UUID |
+| `{{IsNewSession}}` | `"true"` when new session created |
+| `{{MediaUrl}}` | Inbound media pseudo-URL |
+| `{{MediaPath}}` | Local media path |
+| `{{MediaType}}` | Media type (image/audio/document/…) |
+| `{{Transcript}}` | Audio transcript |
+| `{{Prompt}}` | Resolved media prompt for CLI entries |
+| `{{MaxChars}}` | Resolved max output chars for CLI entries |
+| `{{ChatType}}` | `"direct"` or `"group"` |
+| `{{GroupSubject}}` | Group subject (best effort) |
+| `{{GroupMembers}}` | Group members preview (best effort) |
+| `{{SenderName}}` | Sender display name (best effort) |
+| `{{SenderE164}}` | Sender phone number (best effort) |
+| `{{Provider}}` | Provider hint (whatsapp, telegram, discord, etc.) |
+
+---
+
+## Config includes (`$include`)
+
+Split config into multiple files:
+
+```json5
+// ~/.openclaw/openclaw.json
+{
+ gateway: { port: 18789 },
+ agents: { $include: "./agents.json5" },
+ broadcast: {
+ $include: ["./clients/mueller.json5", "./clients/schmidt.json5"],
+ },
+}
+```
+
+**Merge behavior:**
+
+- Single file: replaces the containing object.
+- Array of files: deep-merged in order (later overrides earlier).
+- Sibling keys: merged after includes (override included values).
+- Nested includes: up to 10 levels deep.
+- Paths: relative (to the including file), absolute, or `../` parent references.
+- Errors: clear messages for missing files, parse errors, and circular includes.
+
+---
+
+_Related: [Configuration](/gateway/configuration) · [Configuration Examples](/gateway/configuration-examples) · [Doctor](/gateway/doctor)_
diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md
index bdb3b1ed729..496aed2ce64 100644
--- a/docs/gateway/configuration.md
+++ b/docs/gateway/configuration.md
@@ -1,3448 +1,479 @@
---
-summary: "All configuration options for ~/.openclaw/openclaw.json with examples"
+summary: "Configuration overview: common tasks, quick setup, and links to the full reference"
read_when:
- - Adding or modifying config fields
+ - Setting up OpenClaw for the first time
+ - Looking for common configuration patterns
+ - Navigating to specific config sections
title: "Configuration"
---
-# Configuration 🔧
+# Configuration
-OpenClaw reads an optional **JSON5** config from `~/.openclaw/openclaw.json` (comments + trailing commas allowed).
+OpenClaw reads an optional **JSON5** config from `~/.openclaw/openclaw.json`.
-If the file is missing, OpenClaw uses safe-ish defaults (embedded Pi agent + per-sender sessions + workspace `~/.openclaw/workspace`). You usually only need a config to:
+If the file is missing, OpenClaw uses safe defaults. Common reasons to add a config:
-- restrict who can trigger the bot (`channels.whatsapp.allowFrom`, `channels.telegram.allowFrom`, etc.)
-- control group allowlists + mention behavior (`channels.whatsapp.groups`, `channels.telegram.groups`, `channels.discord.guilds`, `agents.list[].groupChat`)
-- customize message prefixes (`messages`)
-- set the agent's workspace (`agents.defaults.workspace` or `agents.list[].workspace`)
-- tune the embedded agent defaults (`agents.defaults`) and session behavior (`session`)
-- set per-agent identity (`agents.list[].identity`)
+- Connect channels and control who can message the bot
+- Set models, tools, sandboxing, or automation (cron, hooks)
+- Tune sessions, media, networking, or UI
-> **New to configuration?** Check out the [Configuration Examples](/gateway/configuration-examples) guide for complete examples with detailed explanations!
+See the [full reference](/gateway/configuration-reference) for every available field.
-## Strict config validation
+
+**New to configuration?** Start with `openclaw onboard` for interactive setup, or check out the [Configuration Examples](/gateway/configuration-examples) guide for complete copy-paste configs.
+
-OpenClaw only accepts configurations that fully match the schema.
-Unknown keys, malformed types, or invalid values cause the Gateway to **refuse to start** for safety.
-
-When validation fails:
-
-- The Gateway does not boot.
-- Only diagnostic commands are allowed (for example: `openclaw doctor`, `openclaw logs`, `openclaw health`, `openclaw status`, `openclaw service`, `openclaw help`).
-- Run `openclaw doctor` to see the exact issues.
-- Run `openclaw doctor --fix` (or `--yes`) to apply migrations/repairs.
-
-Doctor never writes changes unless you explicitly opt into `--fix`/`--yes`.
-
-## Schema + UI hints
-
-The Gateway exposes a JSON Schema representation of the config via `config.schema` for UI editors.
-The Control UI renders a form from this schema, with a **Raw JSON** editor as an escape hatch.
-
-Channel plugins and extensions can register schema + UI hints for their config, so channel settings
-stay schema-driven across apps without hard-coded forms.
-
-Hints (labels, grouping, sensitive fields) ship alongside the schema so clients can render
-better forms without hard-coding config knowledge.
-
-## Apply + restart (RPC)
-
-Use `config.apply` to validate + write the full config and restart the Gateway in one step.
-It writes a restart sentinel and pings the last active session after the Gateway comes back.
-
-Warning: `config.apply` replaces the **entire config**. If you want to change only a few keys,
-use `config.patch` or `openclaw config set`. Keep a backup of `~/.openclaw/openclaw.json`.
-
-Params:
-
-- `raw` (string) — JSON5 payload for the entire config
-- `baseHash` (optional) — config hash from `config.get` (required when a config already exists)
-- `sessionKey` (optional) — last active session key for the wake-up ping
-- `note` (optional) — note to include in the restart sentinel
-- `restartDelayMs` (optional) — delay before restart (default 2000)
-
-Example (via `gateway call`):
-
-```bash
-openclaw gateway call config.get --params '{}' # capture payload.hash
-openclaw gateway call config.apply --params '{
- "raw": "{\\n agents: { defaults: { workspace: \\"~/.openclaw/workspace\\" } }\\n}\\n",
- "baseHash": "",
- "sessionKey": "agent:main:whatsapp:dm:+15555550123",
- "restartDelayMs": 1000
-}'
-```
-
-## Partial updates (RPC)
-
-Use `config.patch` to merge a partial update into the existing config without clobbering
-unrelated keys. It applies JSON merge patch semantics:
-
-- objects merge recursively
-- `null` deletes a key
-- arrays replace
- Like `config.apply`, it validates, writes the config, stores a restart sentinel, and schedules
- the Gateway restart (with an optional wake when `sessionKey` is provided).
-
-Params:
-
-- `raw` (string) — JSON5 payload containing just the keys to change
-- `baseHash` (required) — config hash from `config.get`
-- `sessionKey` (optional) — last active session key for the wake-up ping
-- `note` (optional) — note to include in the restart sentinel
-- `restartDelayMs` (optional) — delay before restart (default 2000)
-
-Example:
-
-```bash
-openclaw gateway call config.get --params '{}' # capture payload.hash
-openclaw gateway call config.patch --params '{
- "raw": "{\\n channels: { telegram: { groups: { \\"*\\": { requireMention: false } } } }\\n}\\n",
- "baseHash": "",
- "sessionKey": "agent:main:whatsapp:dm:+15555550123",
- "restartDelayMs": 1000
-}'
-```
-
-## Minimal config (recommended starting point)
+## Minimal config
```json5
+// ~/.openclaw/openclaw.json
{
agents: { defaults: { workspace: "~/.openclaw/workspace" } },
channels: { whatsapp: { allowFrom: ["+15555550123"] } },
}
```
-Build the default image once with:
+## Editing config
-```bash
-scripts/sandbox-setup.sh
-```
+
+
+ ```bash
+ openclaw onboard # full setup wizard
+ openclaw configure # config wizard
+ ```
+
+
+ ```bash
+ openclaw config get agents.defaults.workspace
+ openclaw config set agents.defaults.heartbeat.every "2h"
+ openclaw config unset tools.web.search.apiKey
+ ```
+
+
+ Open [http://127.0.0.1:18789](http://127.0.0.1:18789) and use the **Config** tab.
+ The Control UI renders a form from the config schema, with a **Raw JSON** editor as an escape hatch.
+
+
+ Edit `~/.openclaw/openclaw.json` directly. The Gateway watches the file and applies changes automatically (see [hot reload](#config-hot-reload)).
+
+
-## Self-chat mode (recommended for group control)
+## Strict validation
-To prevent the bot from responding to WhatsApp @-mentions in groups (only respond to specific text triggers):
+
+OpenClaw only accepts configurations that fully match the schema. Unknown keys, malformed types, or invalid values cause the Gateway to **refuse to start**.
+
-```json5
-{
- agents: {
- defaults: { workspace: "~/.openclaw/workspace" },
- list: [
- {
- id: "main",
- groupChat: { mentionPatterns: ["@openclaw", "reisponde"] },
+When validation fails:
+
+- The Gateway does not boot
+- Only diagnostic commands work (`openclaw doctor`, `openclaw logs`, `openclaw health`, `openclaw status`)
+- Run `openclaw doctor` to see exact issues
+- Run `openclaw doctor --fix` (or `--yes`) to apply repairs
+
+## Common tasks
+
+
+
+ Each channel has its own config section under `channels.`. See the dedicated channel page for setup steps:
+
+ - [WhatsApp](/channels/whatsapp) — `channels.whatsapp`
+ - [Telegram](/channels/telegram) — `channels.telegram`
+ - [Discord](/channels/discord) — `channels.discord`
+ - [Slack](/channels/slack) — `channels.slack`
+ - [Signal](/channels/signal) — `channels.signal`
+ - [iMessage](/channels/imessage) — `channels.imessage`
+ - [Google Chat](/channels/googlechat) — `channels.googlechat`
+ - [Mattermost](/channels/mattermost) — `channels.mattermost`
+ - [MS Teams](/channels/msteams) — `channels.msteams`
+
+ All channels share the same DM policy pattern:
+
+ ```json5
+ {
+ channels: {
+ telegram: {
+ enabled: true,
+ botToken: "123:abc",
+ dmPolicy: "pairing", // pairing | allowlist | open | disabled
+ allowFrom: ["tg:123"], // only for allowlist/open
+ },
},
- ],
- },
- channels: {
- whatsapp: {
- // Allowlist is DMs only; including your own number enables self-chat mode.
- allowFrom: ["+15555550123"],
- groups: { "*": { requireMention: true } },
- },
+ }
+ ```
+
+
+
+
+ Set the primary model and optional fallbacks:
+
+ ```json5
+ {
+ agents: {
+ defaults: {
+ model: {
+ primary: "anthropic/claude-sonnet-4-5",
+ fallbacks: ["openai/gpt-5.2"],
+ },
+ models: {
+ "anthropic/claude-sonnet-4-5": { alias: "Sonnet" },
+ "openai/gpt-5.2": { alias: "GPT" },
+ },
+ },
+ },
+ }
+ ```
+
+ - `agents.defaults.models` defines the model catalog and acts as the allowlist for `/model`.
+ - Model refs use `provider/model` format (e.g. `anthropic/claude-opus-4-6`).
+ - See [Models CLI](/concepts/models) for switching models in chat and [Model Failover](/concepts/model-failover) for auth rotation and fallback behavior.
+ - For custom/self-hosted providers, see [Custom providers](/gateway/configuration-reference#custom-providers-and-base-urls) in the reference.
+
+
+
+
+ DM access is controlled per channel via `dmPolicy`:
+
+ - `"pairing"` (default): unknown senders get a one-time pairing code to approve
+ - `"allowlist"`: only senders in `allowFrom` (or the paired allow store)
+ - `"open"`: allow all inbound DMs (requires `allowFrom: ["*"]`)
+ - `"disabled"`: ignore all DMs
+
+ For groups, use `groupPolicy` + `groupAllowFrom` or channel-specific allowlists.
+
+ See the [full reference](/gateway/configuration-reference#dm-and-group-access) for per-channel details.
+
+
+
+
+ Group messages default to **require mention**. Configure patterns per agent:
+
+ ```json5
+ {
+ agents: {
+ list: [
+ {
+ id: "main",
+ groupChat: {
+ mentionPatterns: ["@openclaw", "openclaw"],
+ },
+ },
+ ],
+ },
+ channels: {
+ whatsapp: {
+ groups: { "*": { requireMention: true } },
+ },
+ },
+ }
+ ```
+
+ - **Metadata mentions**: native @-mentions (WhatsApp tap-to-mention, Telegram @bot, etc.)
+ - **Text patterns**: regex patterns in `mentionPatterns`
+ - See [full reference](/gateway/configuration-reference#group-chat-mention-gating) for per-channel overrides and self-chat mode.
+
+
+
+
+ Sessions control conversation continuity and isolation:
+
+ ```json5
+ {
+ session: {
+ dmScope: "per-channel-peer", // recommended for multi-user
+ reset: {
+ mode: "daily",
+ atHour: 4,
+ idleMinutes: 120,
+ },
+ },
+ }
+ ```
+
+ - `dmScope`: `main` (shared) | `per-peer` | `per-channel-peer` | `per-account-channel-peer`
+ - See [Session Management](/concepts/session) for scoping, identity links, and send policy.
+ - See [full reference](/gateway/configuration-reference#session) for all fields.
+
+
+
+
+ Run agent sessions in isolated Docker containers:
+
+ ```json5
+ {
+ agents: {
+ defaults: {
+ sandbox: {
+ mode: "non-main", // off | non-main | all
+ scope: "agent", // session | agent | shared
+ },
+ },
+ },
+ }
+ ```
+
+ Build the image first: `scripts/sandbox-setup.sh`
+
+ See [Sandboxing](/gateway/sandboxing) for the full guide and [full reference](/gateway/configuration-reference#sandbox) for all options.
+
+
+
+
+ ```json5
+ {
+ agents: {
+ defaults: {
+ heartbeat: {
+ every: "30m",
+ target: "last",
+ },
+ },
+ },
+ }
+ ```
+
+ - `every`: duration string (`30m`, `2h`). Set `0m` to disable.
+ - `target`: `last` | `whatsapp` | `telegram` | `discord` | `none`
+ - See [Heartbeat](/gateway/heartbeat) for the full guide.
+
+
+
+
+ ```json5
+ {
+ cron: {
+ enabled: true,
+ maxConcurrentRuns: 2,
+ sessionRetention: "24h",
+ },
+ }
+ ```
+
+ See [Cron jobs](/automation/cron-jobs) for the feature overview and CLI examples.
+
+
+
+
+ Enable HTTP webhook endpoints on the Gateway:
+
+ ```json5
+ {
+ hooks: {
+ enabled: true,
+ token: "shared-secret",
+ path: "/hooks",
+ mappings: [
+ {
+ match: { path: "gmail" },
+ action: "agent",
+ agentId: "main",
+ deliver: true,
+ },
+ ],
+ },
+ }
+ ```
+
+ See [full reference](/gateway/configuration-reference#hooks) for all mapping options and Gmail integration.
+
+
+
+
+ Run multiple isolated agents with separate workspaces and sessions:
+
+ ```json5
+ {
+ agents: {
+ list: [
+ { id: "home", default: true, workspace: "~/.openclaw/workspace-home" },
+ { id: "work", workspace: "~/.openclaw/workspace-work" },
+ ],
+ },
+ bindings: [
+ { agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
+ { agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
+ ],
+ }
+ ```
+
+ See [Multi-Agent](/concepts/multi-agent) and [full reference](/gateway/configuration-reference#multi-agent-routing) for binding rules and per-agent access profiles.
+
+
+
+
+ Use `$include` to organize large configs:
+
+ ```json5
+ // ~/.openclaw/openclaw.json
+ {
+ gateway: { port: 18789 },
+ agents: { $include: "./agents.json5" },
+ broadcast: {
+ $include: ["./clients/a.json5", "./clients/b.json5"],
+ },
+ }
+ ```
+
+ - **Single file**: replaces the containing object
+ - **Array of files**: deep-merged in order (later wins)
+ - **Sibling keys**: merged after includes (override included values)
+ - **Nested includes**: supported up to 10 levels deep
+ - **Relative paths**: resolved relative to the including file
+ - **Error handling**: clear errors for missing files, parse errors, and circular includes
+
+
+
+
+## Config hot reload
+
+The Gateway watches `~/.openclaw/openclaw.json` and applies changes automatically — no manual restart needed for most settings.
+
+### Reload modes
+
+| Mode | Behavior |
+| ---------------------- | --------------------------------------------------------------------------------------- |
+| **`hybrid`** (default) | Hot-applies safe changes instantly. Automatically restarts for critical ones. |
+| **`hot`** | Hot-applies safe changes only. Logs a warning when a restart is needed — you handle it. |
+| **`restart`** | Restarts the Gateway on any config change, safe or not. |
+| **`off`** | Disables file watching. Changes take effect on the next manual restart. |
+
+```json5
+{
+ gateway: {
+ reload: { mode: "hybrid", debounceMs: 300 },
},
}
```
-## Config Includes (`$include`)
+### What hot-applies vs what needs a restart
-Split your config into multiple files using the `$include` directive. This is useful for:
+Most fields hot-apply without downtime. In `hybrid` mode, restart-required changes are handled automatically.
-- Organizing large configs (e.g., per-client agent definitions)
-- Sharing common settings across environments
-- Keeping sensitive configs separate
+| Category | Fields | Restart needed? |
+| ------------------- | -------------------------------------------------------------------- | --------------- |
+| Channels | `channels.*`, `web` (WhatsApp) — all built-in and extension channels | No |
+| Agent & models | `agent`, `agents`, `models`, `routing` | No |
+| Automation | `hooks`, `cron`, `agent.heartbeat` | No |
+| Sessions & messages | `session`, `messages` | No |
+| Tools & media | `tools`, `browser`, `skills`, `audio`, `talk` | No |
+| UI & misc | `ui`, `logging`, `identity`, `bindings` | No |
+| Gateway server | `gateway.*` (port, bind, auth, tailscale, TLS, HTTP) | **Yes** |
+| Infrastructure | `discovery`, `canvasHost`, `plugins` | **Yes** |
-### Basic usage
+
+`gateway.reload` and `gateway.remote` are exceptions — changing them does **not** trigger a restart.
+
-```json5
-// ~/.openclaw/openclaw.json
-{
- gateway: { port: 18789 },
+## Config RPC (programmatic updates)
- // Include a single file (replaces the key's value)
- agents: { $include: "./agents.json5" },
+
+
+ Validates + writes the full config and restarts the Gateway in one step.
- // Include multiple files (deep-merged in order)
- broadcast: {
- $include: ["./clients/mueller.json5", "./clients/schmidt.json5"],
- },
-}
-```
+
+ `config.apply` replaces the **entire config**. Use `config.patch` for partial updates, or `openclaw config set` for single keys.
+
-```json5
-// ~/.openclaw/agents.json5
-{
- defaults: { sandbox: { mode: "all", scope: "session" } },
- list: [{ id: "main", workspace: "~/.openclaw/workspace" }],
-}
-```
+ Params:
-### Merge behavior
+ - `raw` (string) — JSON5 payload for the entire config
+ - `baseHash` (optional) — config hash from `config.get` (required when config exists)
+ - `sessionKey` (optional) — session key for the post-restart wake-up ping
+ - `note` (optional) — note for the restart sentinel
+ - `restartDelayMs` (optional) — delay before restart (default 2000)
-- **Single file**: Replaces the object containing `$include`
-- **Array of files**: Deep-merges files in order (later files override earlier ones)
-- **With sibling keys**: Sibling keys are merged after includes (override included values)
-- **Sibling keys + arrays/primitives**: Not supported (included content must be an object)
+ ```bash
+ openclaw gateway call config.get --params '{}' # capture payload.hash
+ openclaw gateway call config.apply --params '{
+ "raw": "{ agents: { defaults: { workspace: \"~/.openclaw/workspace\" } } }",
+ "baseHash": "",
+ "sessionKey": "agent:main:whatsapp:dm:+15555550123"
+ }'
+ ```
-```json5
-// Sibling keys override included values
-{
- $include: "./base.json5", // { a: 1, b: 2 }
- b: 99, // Result: { a: 1, b: 99 }
-}
-```
+
-### Nested includes
+
+ Merges a partial update into the existing config (JSON merge patch semantics):
-Included files can themselves contain `$include` directives (up to 10 levels deep):
+ - Objects merge recursively
+ - `null` deletes a key
+ - Arrays replace
-```json5
-// clients/mueller.json5
-{
- agents: { $include: "./mueller/agents.json5" },
- broadcast: { $include: "./mueller/broadcast.json5" },
-}
-```
+ Params:
-### Path resolution
+ - `raw` (string) — JSON5 with just the keys to change
+ - `baseHash` (required) — config hash from `config.get`
+ - `sessionKey`, `note`, `restartDelayMs` — same as `config.apply`
-- **Relative paths**: Resolved relative to the including file
-- **Absolute paths**: Used as-is
-- **Parent directories**: `../` references work as expected
+ ```bash
+ openclaw gateway call config.patch --params '{
+ "raw": "{ channels: { telegram: { groups: { \"*\": { requireMention: false } } } } }",
+ "baseHash": ""
+ }'
+ ```
-```json5
-{ "$include": "./sub/config.json5" } // relative
-{ "$include": "/etc/openclaw/base.json5" } // absolute
-{ "$include": "../shared/common.json5" } // parent dir
-```
+
+
-### Error handling
+## Environment variables
-- **Missing file**: Clear error with resolved path
-- **Parse error**: Shows which included file failed
-- **Circular includes**: Detected and reported with include chain
-
-### Example: Multi-client legal setup
-
-```json5
-// ~/.openclaw/openclaw.json
-{
- gateway: { port: 18789, auth: { token: "secret" } },
-
- // Common agent defaults
- agents: {
- defaults: {
- sandbox: { mode: "all", scope: "session" },
- },
- // Merge agent lists from all clients
- list: { $include: ["./clients/mueller/agents.json5", "./clients/schmidt/agents.json5"] },
- },
-
- // Merge broadcast configs
- broadcast: {
- $include: ["./clients/mueller/broadcast.json5", "./clients/schmidt/broadcast.json5"],
- },
-
- channels: { whatsapp: { groupPolicy: "allowlist" } },
-}
-```
-
-```json5
-// ~/.openclaw/clients/mueller/agents.json5
-[
- { id: "mueller-transcribe", workspace: "~/clients/mueller/transcribe" },
- { id: "mueller-docs", workspace: "~/clients/mueller/docs" },
-]
-```
-
-```json5
-// ~/.openclaw/clients/mueller/broadcast.json5
-{
- "120363403215116621@g.us": ["mueller-transcribe", "mueller-docs"],
-}
-```
-
-## Common options
-
-### Env vars + `.env`
-
-OpenClaw reads env vars from the parent process (shell, launchd/systemd, CI, etc.).
-
-Additionally, it loads:
+OpenClaw reads env vars from the parent process plus:
- `.env` from the current working directory (if present)
-- a global fallback `.env` from `~/.openclaw/.env` (aka `$OPENCLAW_STATE_DIR/.env`)
+- `~/.openclaw/.env` (global fallback)
-Neither `.env` file overrides existing env vars.
-
-You can also provide inline env vars in config. These are only applied if the
-process env is missing the key (same non-overriding rule):
+Neither file overrides existing env vars. You can also set inline env vars in config:
```json5
{
env: {
OPENROUTER_API_KEY: "sk-or-...",
- vars: {
- GROQ_API_KEY: "gsk-...",
- },
+ vars: { GROQ_API_KEY: "gsk-..." },
},
}
```
-See [/environment](/help/environment) for full precedence and sources.
-
-### `env.shellEnv` (optional)
-
-Opt-in convenience: if enabled and none of the expected keys are set yet, OpenClaw runs your login shell and imports only the missing expected keys (never overrides).
-This effectively sources your shell profile.
+
+ If enabled and expected keys aren't set, OpenClaw runs your login shell and imports only the missing keys:
```json5
{
env: {
- shellEnv: {
- enabled: true,
- timeoutMs: 15000,
- },
+ shellEnv: { enabled: true, timeoutMs: 15000 },
},
}
```
-Env var equivalent:
+Env var equivalent: `OPENCLAW_LOAD_SHELL_ENV=1`
+
-- `OPENCLAW_LOAD_SHELL_ENV=1`
-- `OPENCLAW_SHELL_ENV_TIMEOUT_MS=15000`
-
-### Env var substitution in config
-
-You can reference environment variables directly in any config string value using
-`${VAR_NAME}` syntax. Variables are substituted at config load time, before validation.
-
-```json5
-{
- models: {
- providers: {
- "vercel-gateway": {
- apiKey: "${VERCEL_GATEWAY_API_KEY}",
- },
- },
- },
- gateway: {
- auth: {
- token: "${OPENCLAW_GATEWAY_TOKEN}",
- },
- },
-}
-```
-
-**Rules:**
-
-- Only uppercase env var names are matched: `[A-Z_][A-Z0-9_]*`
-- Missing or empty env vars throw an error at config load
-- Escape with `$${VAR}` to output a literal `${VAR}`
-- Works with `$include` (included files also get substitution)
-
-**Inline substitution:**
-
-```json5
-{
- models: {
- providers: {
- custom: {
- baseUrl: "${CUSTOM_API_BASE}/v1", // → "https://api.example.com/v1"
- },
- },
- },
-}
-```
-
-### Auth storage (OAuth + API keys)
-
-OpenClaw stores **per-agent** auth profiles (OAuth + API keys) in:
-
-- `/auth-profiles.json` (default: `~/.openclaw/agents//agent/auth-profiles.json`)
-
-See also: [/concepts/oauth](/concepts/oauth)
-
-Legacy OAuth imports:
-
-- `~/.openclaw/credentials/oauth.json` (or `$OPENCLAW_STATE_DIR/credentials/oauth.json`)
-
-The embedded Pi agent maintains a runtime cache at:
-
-- `/auth.json` (managed automatically; don’t edit manually)
-
-Legacy agent dir (pre multi-agent):
-
-- `~/.openclaw/agent/*` (migrated by `openclaw doctor` into `~/.openclaw/agents//agent/*`)
-
-Overrides:
-
-- OAuth dir (legacy import only): `OPENCLAW_OAUTH_DIR`
-- Agent dir (default agent root override): `OPENCLAW_AGENT_DIR` (preferred), `PI_CODING_AGENT_DIR` (legacy)
-
-On first use, OpenClaw imports `oauth.json` entries into `auth-profiles.json`.
-
-### `auth`
-
-Optional metadata for auth profiles. This does **not** store secrets; it maps
-profile IDs to a provider + mode (and optional email) and defines the provider
-rotation order used for failover.
-
-```json5
-{
- auth: {
- profiles: {
- "anthropic:me@example.com": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
- "anthropic:work": { provider: "anthropic", mode: "api_key" },
- },
- order: {
- anthropic: ["anthropic:me@example.com", "anthropic:work"],
- },
- },
-}
-```
-
-### `agents.list[].identity`
-
-Optional per-agent identity used for defaults and UX. This is written by the macOS onboarding assistant.
-
-If set, OpenClaw derives defaults (only when you haven’t set them explicitly):
-
-- `messages.ackReaction` from the **active agent**’s `identity.emoji` (falls back to 👀)
-- `agents.list[].groupChat.mentionPatterns` from the agent’s `identity.name`/`identity.emoji` (so “@Samantha” works in groups across Telegram/Slack/Discord/Google Chat/iMessage/WhatsApp)
-- `identity.avatar` accepts a workspace-relative image path or a remote URL/data URL. Local files must live inside the agent workspace.
-
-`identity.avatar` accepts:
-
-- Workspace-relative path (must stay within the agent workspace)
-- `http(s)` URL
-- `data:` URI
-
-```json5
-{
- agents: {
- list: [
- {
- id: "main",
- identity: {
- name: "Samantha",
- theme: "helpful sloth",
- emoji: "🦥",
- avatar: "avatars/samantha.png",
- },
- },
- ],
- },
-}
-```
-
-### `wizard`
-
-Metadata written by CLI wizards (`onboard`, `configure`, `doctor`).
-
-```json5
-{
- wizard: {
- lastRunAt: "2026-01-01T00:00:00.000Z",
- lastRunVersion: "2026.1.4",
- lastRunCommit: "abc1234",
- lastRunCommand: "configure",
- lastRunMode: "local",
- },
-}
-```
-
-### `logging`
-
-- Default log file: `/tmp/openclaw/openclaw-YYYY-MM-DD.log`
-- If you want a stable path, set `logging.file` to `/tmp/openclaw/openclaw.log`.
-- Console output can be tuned separately via:
- - `logging.consoleLevel` (defaults to `info`, bumps to `debug` when `--verbose`)
- - `logging.consoleStyle` (`pretty` | `compact` | `json`)
-- Tool summaries can be redacted to avoid leaking secrets:
- - `logging.redactSensitive` (`off` | `tools`, default: `tools`)
- - `logging.redactPatterns` (array of regex strings; overrides defaults)
-
-```json5
-{
- logging: {
- level: "info",
- file: "/tmp/openclaw/openclaw.log",
- consoleLevel: "info",
- consoleStyle: "pretty",
- redactSensitive: "tools",
- redactPatterns: [
- // Example: override defaults with your own rules.
- "\\bTOKEN\\b\\s*[=:]\\s*([\"']?)([^\\s\"']+)\\1",
- "/\\bsk-[A-Za-z0-9_-]{8,}\\b/gi",
- ],
- },
-}
-```
-
-### `channels.whatsapp.dmPolicy`
-
-Controls how WhatsApp direct chats (DMs) are handled:
-
-- `"pairing"` (default): unknown senders get a pairing code; owner must approve
-- `"allowlist"`: only allow senders in `channels.whatsapp.allowFrom` (or paired allow store)
-- `"open"`: allow all inbound DMs (**requires** `channels.whatsapp.allowFrom` to include `"*"`)
-- `"disabled"`: ignore all inbound DMs
-
-Pairing codes expire after 1 hour; the bot only sends a pairing code when a new request is created. Pending DM pairing requests are capped at **3 per channel** by default.
-
-Pairing approvals:
-
-- `openclaw pairing list whatsapp`
-- `openclaw pairing approve whatsapp `
-
-### `channels.whatsapp.allowFrom`
-
-Allowlist of E.164 phone numbers that may trigger WhatsApp auto-replies (**DMs only**).
-If empty and `channels.whatsapp.dmPolicy="pairing"`, unknown senders will receive a pairing code.
-For groups, use `channels.whatsapp.groupPolicy` + `channels.whatsapp.groupAllowFrom`.
-
-```json5
-{
- channels: {
- whatsapp: {
- dmPolicy: "pairing", // pairing | allowlist | open | disabled
- allowFrom: ["+15555550123", "+447700900123"],
- textChunkLimit: 4000, // optional outbound chunk size (chars)
- chunkMode: "length", // optional chunking mode (length | newline)
- mediaMaxMb: 50, // optional inbound media cap (MB)
- },
- },
-}
-```
-
-### `channels.whatsapp.sendReadReceipts`
-
-Controls whether inbound WhatsApp messages are marked as read (blue ticks). Default: `true`.
-
-Self-chat mode always skips read receipts, even when enabled.
-
-Per-account override: `channels.whatsapp.accounts..sendReadReceipts`.
-
-```json5
-{
- channels: {
- whatsapp: { sendReadReceipts: false },
- },
-}
-```
-
-### `channels.whatsapp.accounts` (multi-account)
-
-Run multiple WhatsApp accounts in one gateway:
-
-```json5
-{
- channels: {
- whatsapp: {
- accounts: {
- default: {}, // optional; keeps the default id stable
- personal: {},
- biz: {
- // Optional override. Default: ~/.openclaw/credentials/whatsapp/biz
- // authDir: "~/.openclaw/credentials/whatsapp/biz",
- },
- },
- },
- },
-}
-```
-
-Notes:
-
-- Outbound commands default to account `default` if present; otherwise the first configured account id (sorted).
-- The legacy single-account Baileys auth dir is migrated by `openclaw doctor` into `whatsapp/default`.
-
-### `channels.telegram.accounts` / `channels.discord.accounts` / `channels.googlechat.accounts` / `channels.slack.accounts` / `channels.mattermost.accounts` / `channels.signal.accounts` / `channels.imessage.accounts`
-
-Run multiple accounts per channel (each account has its own `accountId` and optional `name`):
-
-```json5
-{
- channels: {
- telegram: {
- accounts: {
- default: {
- name: "Primary bot",
- botToken: "123456:ABC...",
- },
- alerts: {
- name: "Alerts bot",
- botToken: "987654:XYZ...",
- },
- },
- },
- },
-}
-```
-
-Notes:
-
-- `default` is used when `accountId` is omitted (CLI + routing).
-- Env tokens only apply to the **default** account.
-- Base channel settings (group policy, mention gating, etc.) apply to all accounts unless overridden per account.
-- Use `bindings[].match.accountId` to route each account to a different agents.defaults.
-
-### Group chat mention gating (`agents.list[].groupChat` + `messages.groupChat`)
-
-Group messages default to **require mention** (either metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, Google Chat, and iMessage group chats.
-
-**Mention types:**
-
-- **Metadata mentions**: Native platform @-mentions (e.g., WhatsApp tap-to-mention). Ignored in WhatsApp self-chat mode (see `channels.whatsapp.allowFrom`).
-- **Text patterns**: Regex patterns defined in `agents.list[].groupChat.mentionPatterns`. Always checked regardless of self-chat mode.
-- Mention gating is enforced only when mention detection is possible (native mentions or at least one `mentionPattern`).
-
-```json5
-{
- messages: {
- groupChat: { historyLimit: 50 },
- },
- agents: {
- list: [{ id: "main", groupChat: { mentionPatterns: ["@openclaw", "openclaw"] } }],
- },
-}
-```
-
-`messages.groupChat.historyLimit` sets the global default for group history context. Channels can override with `channels..historyLimit` (or `channels..accounts.*.historyLimit` for multi-account). Set `0` to disable history wrapping.
-
-#### DM history limits
-
-DM conversations use session-based history managed by the agent. You can limit the number of user turns retained per DM session:
-
-```json5
-{
- channels: {
- telegram: {
- dmHistoryLimit: 30, // limit DM sessions to 30 user turns
- dms: {
- "123456789": { historyLimit: 50 }, // per-user override (user ID)
- },
- },
- },
-}
-```
-
-Resolution order:
-
-1. Per-DM override: `channels..dms[userId].historyLimit`
-2. Provider default: `channels..dmHistoryLimit`
-3. No limit (all history retained)
-
-Supported providers: `telegram`, `whatsapp`, `discord`, `slack`, `signal`, `imessage`, `msteams`.
-
-Per-agent override (takes precedence when set, even `[]`):
-
-```json5
-{
- agents: {
- list: [
- { id: "work", groupChat: { mentionPatterns: ["@workbot", "\\+15555550123"] } },
- { id: "personal", groupChat: { mentionPatterns: ["@homebot", "\\+15555550999"] } },
- ],
- },
-}
-```
-
-Mention gating defaults live per channel (`channels.whatsapp.groups`, `channels.telegram.groups`, `channels.imessage.groups`, `channels.discord.guilds`). When `*.groups` is set, it also acts as a group allowlist; include `"*"` to allow all groups.
-
-To respond **only** to specific text triggers (ignoring native @-mentions):
-
-```json5
-{
- channels: {
- whatsapp: {
- // Include your own number to enable self-chat mode (ignore native @-mentions).
- allowFrom: ["+15555550123"],
- groups: { "*": { requireMention: true } },
- },
- },
- agents: {
- list: [
- {
- id: "main",
- groupChat: {
- // Only these text patterns will trigger responses
- mentionPatterns: ["reisponde", "@openclaw"],
- },
- },
- ],
- },
-}
-```
-
-### Group policy (per channel)
-
-Use `channels.*.groupPolicy` to control whether group/room messages are accepted at all:
-
-```json5
-{
- channels: {
- whatsapp: {
- groupPolicy: "allowlist",
- groupAllowFrom: ["+15551234567"],
- },
- telegram: {
- groupPolicy: "allowlist",
- groupAllowFrom: ["tg:123456789", "@alice"],
- },
- signal: {
- groupPolicy: "allowlist",
- groupAllowFrom: ["+15551234567"],
- },
- imessage: {
- groupPolicy: "allowlist",
- groupAllowFrom: ["chat_id:123"],
- },
- msteams: {
- groupPolicy: "allowlist",
- groupAllowFrom: ["user@org.com"],
- },
- discord: {
- groupPolicy: "allowlist",
- guilds: {
- GUILD_ID: {
- channels: { help: { allow: true } },
- },
- },
- },
- slack: {
- groupPolicy: "allowlist",
- channels: { "#general": { allow: true } },
- },
- },
-}
-```
-
-Notes:
-
-- `"open"`: groups bypass allowlists; mention-gating still applies.
-- `"disabled"`: block all group/room messages.
-- `"allowlist"`: only allow groups/rooms that match the configured allowlist.
-- `channels.defaults.groupPolicy` sets the default when a provider’s `groupPolicy` is unset.
-- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams use `groupAllowFrom` (fallback: explicit `allowFrom`).
-- Discord/Slack use channel allowlists (`channels.discord.guilds.*.channels`, `channels.slack.channels`).
-- Group DMs (Discord/Slack) are still controlled by `dm.groupEnabled` + `dm.groupChannels`.
-- Default is `groupPolicy: "allowlist"` (unless overridden by `channels.defaults.groupPolicy`); if no allowlist is configured, group messages are blocked.
-
-### Multi-agent routing (`agents.list` + `bindings`)
-
-Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside one Gateway.
-Inbound messages are routed to an agent via bindings.
-
-- `agents.list[]`: per-agent overrides.
- - `id`: stable agent id (required).
- - `default`: optional; when multiple are set, the first wins and a warning is logged.
- If none are set, the **first entry** in the list is the default agent.
- - `name`: display name for the agent.
- - `workspace`: default `~/.openclaw/workspace-` (for `main`, falls back to `agents.defaults.workspace`).
- - `agentDir`: default `~/.openclaw/agents//agent`.
- - `model`: per-agent default model, overrides `agents.defaults.model` for that agent.
- - string form: `"provider/model"`, overrides only `agents.defaults.model.primary`
- - object form: `{ primary, fallbacks }` (fallbacks override `agents.defaults.model.fallbacks`; `[]` disables global fallbacks for that agent)
- - `identity`: per-agent name/theme/emoji (used for mention patterns + ack reactions).
- - `groupChat`: per-agent mention-gating (`mentionPatterns`).
- - `sandbox`: per-agent sandbox config (overrides `agents.defaults.sandbox`).
- - `mode`: `"off"` | `"non-main"` | `"all"`
- - `workspaceAccess`: `"none"` | `"ro"` | `"rw"`
- - `scope`: `"session"` | `"agent"` | `"shared"`
- - `workspaceRoot`: custom sandbox workspace root
- - `docker`: per-agent docker overrides (e.g. `image`, `network`, `env`, `setupCommand`, limits; ignored when `scope: "shared"`)
- - `browser`: per-agent sandboxed browser overrides (ignored when `scope: "shared"`)
- - `prune`: per-agent sandbox pruning overrides (ignored when `scope: "shared"`)
- - `subagents`: per-agent sub-agent defaults.
- - `allowAgents`: allowlist of agent ids for `sessions_spawn` from this agent (`["*"]` = allow any; default: only same agent)
- - `tools`: per-agent tool restrictions (applied before sandbox tool policy).
- - `profile`: base tool profile (applied before allow/deny)
- - `allow`: array of allowed tool names
- - `deny`: array of denied tool names (deny wins)
-- `agents.defaults`: shared agent defaults (model, workspace, sandbox, etc.).
-- `bindings[]`: routes inbound messages to an `agentId`.
- - `match.channel` (required)
- - `match.accountId` (optional; `*` = any account; omitted = default account)
- - `match.peer` (optional; `{ kind: direct|group|channel, id }`)
- - `match.guildId` / `match.teamId` (optional; channel-specific)
-
-Deterministic match order:
-
-1. `match.peer`
-2. `match.guildId`
-3. `match.teamId`
-4. `match.accountId` (exact, no peer/guild/team)
-5. `match.accountId: "*"` (channel-wide, no peer/guild/team)
-6. default agent (`agents.list[].default`, else first list entry, else `"main"`)
-
-Within each match tier, the first matching entry in `bindings` wins.
-
-#### Per-agent access profiles (multi-agent)
-
-Each agent can carry its own sandbox + tool policy. Use this to mix access
-levels in one gateway:
-
-- **Full access** (personal agent)
-- **Read-only** tools + workspace
-- **No filesystem access** (messaging/session tools only)
-
-See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for precedence and
-additional examples.
-
-Full access (no sandbox):
-
-```json5
-{
- agents: {
- list: [
- {
- id: "personal",
- workspace: "~/.openclaw/workspace-personal",
- sandbox: { mode: "off" },
- },
- ],
- },
-}
-```
-
-Read-only tools + read-only workspace:
-
-```json5
-{
- agents: {
- list: [
- {
- id: "family",
- workspace: "~/.openclaw/workspace-family",
- sandbox: {
- mode: "all",
- scope: "agent",
- workspaceAccess: "ro",
- },
- tools: {
- allow: [
- "read",
- "sessions_list",
- "sessions_history",
- "sessions_send",
- "sessions_spawn",
- "session_status",
- ],
- deny: ["write", "edit", "apply_patch", "exec", "process", "browser"],
- },
- },
- ],
- },
-}
-```
-
-No filesystem access (messaging/session tools enabled):
-
-```json5
-{
- agents: {
- list: [
- {
- id: "public",
- workspace: "~/.openclaw/workspace-public",
- sandbox: {
- mode: "all",
- scope: "agent",
- workspaceAccess: "none",
- },
- tools: {
- allow: [
- "sessions_list",
- "sessions_history",
- "sessions_send",
- "sessions_spawn",
- "session_status",
- "whatsapp",
- "telegram",
- "slack",
- "discord",
- "gateway",
- ],
- deny: [
- "read",
- "write",
- "edit",
- "apply_patch",
- "exec",
- "process",
- "browser",
- "canvas",
- "nodes",
- "cron",
- "gateway",
- "image",
- ],
- },
- },
- ],
- },
-}
-```
-
-Example: two WhatsApp accounts → two agents:
-
-```json5
-{
- agents: {
- list: [
- { id: "home", default: true, workspace: "~/.openclaw/workspace-home" },
- { id: "work", workspace: "~/.openclaw/workspace-work" },
- ],
- },
- bindings: [
- { agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
- { agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
- ],
- channels: {
- whatsapp: {
- accounts: {
- personal: {},
- biz: {},
- },
- },
- },
-}
-```
-
-### `tools.agentToAgent` (optional)
-
-Agent-to-agent messaging is opt-in:
-
-```json5
-{
- tools: {
- agentToAgent: {
- enabled: false,
- allow: ["home", "work"],
- },
- },
-}
-```
-
-### `messages.queue`
-
-Controls how inbound messages behave when an agent run is already active.
-
-```json5
-{
- messages: {
- queue: {
- mode: "collect", // steer | followup | collect | steer-backlog (steer+backlog ok) | interrupt (queue=steer legacy)
- debounceMs: 1000,
- cap: 20,
- drop: "summarize", // old | new | summarize
- byChannel: {
- whatsapp: "collect",
- telegram: "collect",
- discord: "collect",
- imessage: "collect",
- webchat: "collect",
- },
- },
- },
-}
-```
-
-### `messages.inbound`
-
-Debounce rapid inbound messages from the **same sender** so multiple back-to-back
-messages become a single agent turn. Debouncing is scoped per channel + conversation
-and uses the most recent message for reply threading/IDs.
-
-```json5
-{
- messages: {
- inbound: {
- debounceMs: 2000, // 0 disables
- byChannel: {
- whatsapp: 5000,
- slack: 1500,
- discord: 1500,
- },
- },
- },
-}
-```
-
-Notes:
-
-- Debounce batches **text-only** messages; media/attachments flush immediately.
-- Control commands (e.g. `/queue`, `/new`) bypass debouncing so they stay standalone.
-
-### `commands` (chat command handling)
-
-Controls how chat commands are enabled across connectors.
-
-```json5
-{
- commands: {
- native: "auto", // register native commands when supported (auto)
- text: true, // parse slash commands in chat messages
- bash: false, // allow ! (alias: /bash) (host-only; requires tools.elevated allowlists)
- bashForegroundMs: 2000, // bash foreground window (0 backgrounds immediately)
- config: false, // allow /config (writes to disk)
- debug: false, // allow /debug (runtime-only overrides)
- restart: false, // allow /restart + gateway restart tool
- allowFrom: {
- "*": ["user1"], // optional per-provider command allowlist
- discord: ["user:123"],
- },
- useAccessGroups: true, // enforce access-group allowlists/policies for commands
- },
-}
-```
-
-Notes:
-
-- Text commands must be sent as a **standalone** message and use the leading `/` (no plain-text aliases).
-- `commands.text: false` disables parsing chat messages for commands.
-- `commands.native: "auto"` (default) turns on native commands for Discord/Telegram and leaves Slack off; unsupported channels stay text-only.
-- Set `commands.native: true|false` to force all, or override per channel with `channels.discord.commands.native`, `channels.telegram.commands.native`, `channels.slack.commands.native` (bool or `"auto"`). `false` clears previously registered commands on Discord/Telegram at startup; Slack commands are managed in the Slack app.
-- `channels.telegram.customCommands` adds extra Telegram bot menu entries. Names are normalized; conflicts with native commands are ignored.
-- `commands.bash: true` enables `! ` to run host shell commands (`/bash ` also works as an alias). Requires `tools.elevated.enabled` and allowlisting the sender in `tools.elevated.allowFrom.`.
-- `commands.bashForegroundMs` controls how long bash waits before backgrounding. While a bash job is running, new `! ` requests are rejected (one at a time).
-- `commands.config: true` enables `/config` (reads/writes `openclaw.json`).
-- `channels..configWrites` gates config mutations initiated by that channel (default: true). This applies to `/config set|unset` plus provider-specific auto-migrations (Telegram supergroup ID changes, Slack channel ID changes).
-- `commands.debug: true` enables `/debug` (runtime-only overrides).
-- `commands.restart: true` enables `/restart` and the gateway tool restart action.
-- `commands.allowFrom` sets a per-provider allowlist for command execution. When configured, it is the **only**
- authorization source for commands and directives (channel allowlists/pairing and `commands.useAccessGroups` are ignored).
- Use `"*"` for a global default; provider-specific keys (for example `discord`) override it.
-- `commands.useAccessGroups: false` allows commands to bypass access-group allowlists/policies when `commands.allowFrom`
- is not set.
-- Slash commands and directives are only honored for **authorized senders**. If `commands.allowFrom` is set,
- authorization comes solely from that list; otherwise it is derived from channel allowlists/pairing plus
- `commands.useAccessGroups`.
-
-### `web` (WhatsApp web channel runtime)
-
-WhatsApp runs through the gateway’s web channel (Baileys Web). It starts automatically when a linked session exists.
-Set `web.enabled: false` to keep it off by default.
-
-```json5
-{
- web: {
- enabled: true,
- heartbeatSeconds: 60,
- reconnect: {
- initialMs: 2000,
- maxMs: 120000,
- factor: 1.4,
- jitter: 0.2,
- maxAttempts: 0,
- },
- },
-}
-```
-
-### `channels.telegram` (bot transport)
-
-OpenClaw starts Telegram only when a `channels.telegram` config section exists. The bot token is resolved from `channels.telegram.botToken` (or `channels.telegram.tokenFile`), with `TELEGRAM_BOT_TOKEN` as a fallback for the default account.
-Set `channels.telegram.enabled: false` to disable automatic startup.
-Multi-account support lives under `channels.telegram.accounts` (see the multi-account section above). Env tokens only apply to the default account.
-Set `channels.telegram.configWrites: false` to block Telegram-initiated config writes (including supergroup ID migrations and `/config set|unset`).
-
-```json5
-{
- channels: {
- telegram: {
- enabled: true,
- botToken: "your-bot-token",
- dmPolicy: "pairing", // pairing | allowlist | open | disabled
- allowFrom: ["tg:123456789"], // optional; "open" requires ["*"]
- groups: {
- "*": { requireMention: true },
- "-1001234567890": {
- allowFrom: ["@admin"],
- systemPrompt: "Keep answers brief.",
- topics: {
- "99": {
- requireMention: false,
- skills: ["search"],
- systemPrompt: "Stay on topic.",
- },
- },
- },
- },
- customCommands: [
- { command: "backup", description: "Git backup" },
- { command: "generate", description: "Create an image" },
- ],
- historyLimit: 50, // include last N group messages as context (0 disables)
- replyToMode: "first", // off | first | all
- linkPreview: true, // toggle outbound link previews
- streamMode: "partial", // off | partial | block (draft streaming; separate from block streaming)
- draftChunk: {
- // optional; only for streamMode=block
- minChars: 200,
- maxChars: 800,
- breakPreference: "paragraph", // paragraph | newline | sentence
- },
- actions: { reactions: true, sendMessage: true }, // tool action gates (false disables)
- reactionNotifications: "own", // off | own | all
- mediaMaxMb: 5,
- retry: {
- // outbound retry policy
- attempts: 3,
- minDelayMs: 400,
- maxDelayMs: 30000,
- jitter: 0.1,
- },
- network: {
- // transport overrides
- autoSelectFamily: false,
- },
- proxy: "socks5://localhost:9050",
- webhookUrl: "https://example.com/telegram-webhook", // requires webhookSecret
- webhookSecret: "secret",
- webhookPath: "/telegram-webhook",
- },
- },
-}
-```
-
-Draft streaming notes:
-
-- Uses Telegram `sendMessageDraft` (draft bubble, not a real message).
-- Requires **private chat topics** (message_thread_id in DMs; bot has topics enabled).
-- `/reasoning stream` streams reasoning into the draft, then sends the final answer.
- Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry).
-
-### `channels.discord` (bot transport)
-
-Configure the Discord bot by setting the bot token and optional gating:
-Multi-account support lives under `channels.discord.accounts` (see the multi-account section above). Env tokens only apply to the default account.
-
-```json5
-{
- channels: {
- discord: {
- enabled: true,
- token: "your-bot-token",
- mediaMaxMb: 8, // clamp inbound media size
- allowBots: false, // allow bot-authored messages
- actions: {
- // tool action gates (false disables)
- reactions: true,
- stickers: true,
- polls: true,
- permissions: true,
- messages: true,
- threads: true,
- pins: true,
- search: true,
- memberInfo: true,
- roleInfo: true,
- roles: false,
- channelInfo: true,
- voiceStatus: true,
- events: true,
- moderation: false,
- },
- replyToMode: "off", // off | first | all
- dm: {
- enabled: true, // disable all DMs when false
- policy: "pairing", // pairing | allowlist | open | disabled
- allowFrom: ["1234567890", "steipete"], // optional DM allowlist ("open" requires ["*"])
- groupEnabled: false, // enable group DMs
- groupChannels: ["openclaw-dm"], // optional group DM allowlist
- },
- guilds: {
- "123456789012345678": {
- // guild id (preferred) or slug
- slug: "friends-of-openclaw",
- requireMention: false, // per-guild default
- reactionNotifications: "own", // off | own | all | allowlist
- users: ["987654321098765432"], // optional per-guild user allowlist
- channels: {
- general: { allow: true },
- help: {
- allow: true,
- requireMention: true,
- users: ["987654321098765432"],
- skills: ["docs"],
- systemPrompt: "Short answers only.",
- },
- },
- },
- },
- historyLimit: 20, // include last N guild messages as context
- textChunkLimit: 2000, // optional outbound text chunk size (chars)
- chunkMode: "length", // optional chunking mode (length | newline)
- maxLinesPerMessage: 17, // soft max lines per message (Discord UI clipping)
- retry: {
- // outbound retry policy
- attempts: 3,
- minDelayMs: 500,
- maxDelayMs: 30000,
- jitter: 0.1,
- },
- },
- },
-}
-```
-
-OpenClaw starts Discord only when a `channels.discord` config section exists. The token is resolved from `channels.discord.token`, with `DISCORD_BOT_TOKEN` as a fallback for the default account (unless `channels.discord.enabled` is `false`). Use `user:` (DM) or `channel:` (guild channel) when specifying delivery targets for cron/CLI commands; bare numeric IDs are ambiguous and rejected.
-Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged channel name (no leading `#`). Prefer guild ids as keys to avoid rename ambiguity.
-Bot-authored messages are ignored by default. Enable with `channels.discord.allowBots` (own messages are still filtered to prevent self-reply loops).
-Reaction notification modes:
-
-- `off`: no reaction events.
-- `own`: reactions on the bot's own messages (default).
-- `all`: all reactions on all messages.
-- `allowlist`: reactions from `guilds..users` on all messages (empty list disables).
- Outbound text is chunked by `channels.discord.textChunkLimit` (default 2000). Set `channels.discord.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking. Discord clients can clip very tall messages, so `channels.discord.maxLinesPerMessage` (default 17) splits long multi-line replies even when under 2000 chars.
- Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry).
-
-### `channels.googlechat` (Chat API webhook)
-
-Google Chat runs over HTTP webhooks with app-level auth (service account).
-Multi-account support lives under `channels.googlechat.accounts` (see the multi-account section above). Env vars only apply to the default account.
-
-```json5
-{
- channels: {
- googlechat: {
- enabled: true,
- serviceAccountFile: "/path/to/service-account.json",
- audienceType: "app-url", // app-url | project-number
- audience: "https://gateway.example.com/googlechat",
- webhookPath: "/googlechat",
- botUser: "users/1234567890", // optional; improves mention detection
- dm: {
- enabled: true,
- policy: "pairing", // pairing | allowlist | open | disabled
- allowFrom: ["users/1234567890"], // optional; "open" requires ["*"]
- },
- groupPolicy: "allowlist",
- groups: {
- "spaces/AAAA": { allow: true, requireMention: true },
- },
- actions: { reactions: true },
- typingIndicator: "message",
- mediaMaxMb: 20,
- },
- },
-}
-```
-
-Notes:
-
-- Service account JSON can be inline (`serviceAccount`) or file-based (`serviceAccountFile`).
-- Env fallbacks for the default account: `GOOGLE_CHAT_SERVICE_ACCOUNT` or `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE`.
-- `audienceType` + `audience` must match the Chat app’s webhook auth config.
-- Use `spaces/` or `users/` when setting delivery targets.
-
-### `channels.slack` (socket mode)
-
-Slack runs in Socket Mode and requires both a bot token and app token:
-
-```json5
-{
- channels: {
- slack: {
- enabled: true,
- botToken: "xoxb-...",
- appToken: "xapp-...",
- dm: {
- enabled: true,
- policy: "pairing", // pairing | allowlist | open | disabled
- allowFrom: ["U123", "U456", "*"], // optional; "open" requires ["*"]
- groupEnabled: false,
- groupChannels: ["G123"],
- },
- channels: {
- C123: { allow: true, requireMention: true, allowBots: false },
- "#general": {
- allow: true,
- requireMention: true,
- allowBots: false,
- users: ["U123"],
- skills: ["docs"],
- systemPrompt: "Short answers only.",
- },
- },
- historyLimit: 50, // include last N channel/group messages as context (0 disables)
- allowBots: false,
- reactionNotifications: "own", // off | own | all | allowlist
- reactionAllowlist: ["U123"],
- replyToMode: "off", // off | first | all
- thread: {
- historyScope: "thread", // thread | channel
- inheritParent: false,
- },
- actions: {
- reactions: true,
- messages: true,
- pins: true,
- memberInfo: true,
- emojiList: true,
- },
- slashCommand: {
- enabled: true,
- name: "openclaw",
- sessionPrefix: "slack:slash",
- ephemeral: true,
- },
- textChunkLimit: 4000,
- chunkMode: "length",
- mediaMaxMb: 20,
- },
- },
-}
-```
-
-Multi-account support lives under `channels.slack.accounts` (see the multi-account section above). Env tokens only apply to the default account.
-
-OpenClaw starts Slack when the provider is enabled and both tokens are set (via config or `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN`). Use `user:` (DM) or `channel:` when specifying delivery targets for cron/CLI commands.
-Set `channels.slack.configWrites: false` to block Slack-initiated config writes (including channel ID migrations and `/config set|unset`).
-
-Bot-authored messages are ignored by default. Enable with `channels.slack.allowBots` or `channels.slack.channels..allowBots`.
-
-Reaction notification modes:
-
-- `off`: no reaction events.
-- `own`: reactions on the bot's own messages (default).
-- `all`: all reactions on all messages.
-- `allowlist`: reactions from `channels.slack.reactionAllowlist` on all messages (empty list disables).
-
-Thread session isolation:
-
-- `channels.slack.thread.historyScope` controls whether thread history is per-thread (`thread`, default) or shared across the channel (`channel`).
-- `channels.slack.thread.inheritParent` controls whether new thread sessions inherit the parent channel transcript (default: false).
-
-Slack action groups (gate `slack` tool actions):
-
-| Action group | Default | Notes |
-| ------------ | ------- | ---------------------- |
-| reactions | enabled | React + list reactions |
-| messages | enabled | Read/send/edit/delete |
-| pins | enabled | Pin/unpin/list |
-| memberInfo | enabled | Member info |
-| emojiList | enabled | Custom emoji list |
-
-### `channels.mattermost` (bot token)
-
-Mattermost ships as a plugin and is not bundled with the core install.
-Install it first: `openclaw plugins install @openclaw/mattermost` (or `./extensions/mattermost` from a git checkout).
-
-Mattermost requires a bot token plus the base URL for your server:
-
-```json5
-{
- channels: {
- mattermost: {
- enabled: true,
- botToken: "mm-token",
- baseUrl: "https://chat.example.com",
- dmPolicy: "pairing",
- chatmode: "oncall", // oncall | onmessage | onchar
- oncharPrefixes: [">", "!"],
- textChunkLimit: 4000,
- chunkMode: "length",
- },
- },
-}
-```
-
-OpenClaw starts Mattermost when the account is configured (bot token + base URL) and enabled. The token + base URL are resolved from `channels.mattermost.botToken` + `channels.mattermost.baseUrl` or `MATTERMOST_BOT_TOKEN` + `MATTERMOST_URL` for the default account (unless `channels.mattermost.enabled` is `false`).
-
-Chat modes:
-
-- `oncall` (default): respond to channel messages only when @mentioned.
-- `onmessage`: respond to every channel message.
-- `onchar`: respond when a message starts with a trigger prefix (`channels.mattermost.oncharPrefixes`, default `[">", "!"]`).
-
-Access control:
-
-- Default DMs: `channels.mattermost.dmPolicy="pairing"` (unknown senders get a pairing code).
-- Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`.
-- Groups: `channels.mattermost.groupPolicy="allowlist"` by default (mention-gated). Use `channels.mattermost.groupAllowFrom` to restrict senders.
-
-Multi-account support lives under `channels.mattermost.accounts` (see the multi-account section above). Env vars only apply to the default account.
-Use `channel:` or `user:` (or `@username`) when specifying delivery targets; bare ids are treated as channel ids.
-
-### `channels.signal` (signal-cli)
-
-Signal reactions can emit system events (shared reaction tooling):
-
-```json5
-{
- channels: {
- signal: {
- reactionNotifications: "own", // off | own | all | allowlist
- reactionAllowlist: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
- historyLimit: 50, // include last N group messages as context (0 disables)
- },
- },
-}
-```
-
-Reaction notification modes:
-
-- `off`: no reaction events.
-- `own`: reactions on the bot's own messages (default).
-- `all`: all reactions on all messages.
-- `allowlist`: reactions from `channels.signal.reactionAllowlist` on all messages (empty list disables).
-
-### `channels.imessage` (imsg CLI)
-
-OpenClaw spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
-
-```json5
-{
- channels: {
- imessage: {
- enabled: true,
- cliPath: "imsg",
- dbPath: "~/Library/Messages/chat.db",
- remoteHost: "user@gateway-host", // SCP for remote attachments when using SSH wrapper
- dmPolicy: "pairing", // pairing | allowlist | open | disabled
- allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
- historyLimit: 50, // include last N group messages as context (0 disables)
- includeAttachments: false,
- mediaMaxMb: 16,
- service: "auto",
- region: "US",
- },
- },
-}
-```
-
-Multi-account support lives under `channels.imessage.accounts` (see the multi-account section above).
-
-Notes:
-
-- Requires Full Disk Access to the Messages DB.
-- The first send will prompt for Messages automation permission.
-- Prefer `chat_id:` targets. Use `imsg chats --limit 20` to list chats.
-- `channels.imessage.cliPath` can point to a wrapper script (e.g. `ssh` to another Mac that runs `imsg rpc`); use SSH keys to avoid password prompts.
-- For remote SSH wrappers, set `channels.imessage.remoteHost` to fetch attachments via SCP when `includeAttachments` is enabled.
-
-Example wrapper:
-
-```bash
-#!/usr/bin/env bash
-exec ssh -T gateway-host imsg "$@"
-```
-
-### `agents.defaults.workspace`
-
-Sets the **single global workspace directory** used by the agent for file operations.
-
-Default: `~/.openclaw/workspace`.
-
-```json5
-{
- agents: { defaults: { workspace: "~/.openclaw/workspace" } },
-}
-```
-
-If `agents.defaults.sandbox` is enabled, non-main sessions can override this with their
-own per-scope workspaces under `agents.defaults.sandbox.workspaceRoot`.
-
-### `agents.defaults.repoRoot`
-
-Optional repository root to show in the system prompt’s Runtime line. If unset, OpenClaw
-tries to detect a `.git` directory by walking upward from the workspace (and current
-working directory). The path must exist to be used.
-
-```json5
-{
- agents: { defaults: { repoRoot: "~/Projects/openclaw" } },
-}
-```
-
-### `agents.defaults.skipBootstrap`
-
-Disables automatic creation of the workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, and `BOOTSTRAP.md`).
-
-Use this for pre-seeded deployments where your workspace files come from a repo.
-
-```json5
-{
- agents: { defaults: { skipBootstrap: true } },
-}
-```
-
-### `agents.defaults.bootstrapMaxChars`
-
-Max characters of each workspace bootstrap file injected into the system prompt
-before truncation. Default: `20000`.
-
-When a file exceeds this limit, OpenClaw logs a warning and injects a truncated
-head/tail with a marker.
-
-```json5
-{
- agents: { defaults: { bootstrapMaxChars: 20000 } },
-}
-```
-
-### `agents.defaults.userTimezone`
-
-Sets the user’s timezone for **system prompt context** (not for timestamps in
-message envelopes). If unset, OpenClaw uses the host timezone at runtime.
-
-```json5
-{
- agents: { defaults: { userTimezone: "America/Chicago" } },
-}
-```
-
-### `agents.defaults.timeFormat`
-
-Controls the **time format** shown in the system prompt’s Current Date & Time section.
-Default: `auto` (OS preference).
-
-```json5
-{
- agents: { defaults: { timeFormat: "auto" } }, // auto | 12 | 24
-}
-```
-
-### `messages`
-
-Controls inbound/outbound prefixes and optional ack reactions.
-See [Messages](/concepts/messages) for queueing, sessions, and streaming context.
-
-```json5
-{
- messages: {
- responsePrefix: "🦞", // or "auto"
- ackReaction: "👀",
- ackReactionScope: "group-mentions",
- removeAckAfterReply: false,
- },
-}
-```
-
-`responsePrefix` is applied to **all outbound replies** (tool summaries, block
-streaming, final replies) across channels unless already present.
-
-Overrides can be configured per channel and per account:
-
-- `channels..responsePrefix`
-- `channels..accounts..responsePrefix`
-
-Resolution order (most specific wins):
-
-1. `channels..accounts..responsePrefix`
-2. `channels..responsePrefix`
-3. `messages.responsePrefix`
-
-Semantics:
-
-- `undefined` falls through to the next level.
-- `""` explicitly disables the prefix and stops the cascade.
-- `"auto"` derives `[{identity.name}]` for the routed agent.
-
-Overrides apply to all channels, including extensions, and to every outbound reply kind.
-
-If `messages.responsePrefix` is unset, no prefix is applied by default. WhatsApp self-chat
-replies are the exception: they default to `[{identity.name}]` when set, otherwise
-`[openclaw]`, so same-phone conversations stay legible.
-Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set).
-
-#### Template variables
-
-The `responsePrefix` string can include template variables that resolve dynamically:
-
-| Variable | Description | Example |
-| ----------------- | ---------------------- | --------------------------- |
-| `{model}` | Short model name | `claude-opus-4-6`, `gpt-4o` |
-| `{modelFull}` | Full model identifier | `anthropic/claude-opus-4-6` |
-| `{provider}` | Provider name | `anthropic`, `openai` |
-| `{thinkingLevel}` | Current thinking level | `high`, `low`, `off` |
-| `{identity.name}` | Agent identity name | (same as `"auto"` mode) |
-
-Variables are case-insensitive (`{MODEL}` = `{model}`). `{think}` is an alias for `{thinkingLevel}`.
-Unresolved variables remain as literal text.
-
-```json5
-{
- messages: {
- responsePrefix: "[{model} | think:{thinkingLevel}]",
- },
-}
-```
-
-Example output: `[claude-opus-4-6 | think:high] Here's my response...`
-
-WhatsApp inbound prefix is configured via `channels.whatsapp.messagePrefix` (deprecated:
-`messages.messagePrefix`). Default stays **unchanged**: `"[openclaw]"` when
-`channels.whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using
-`"[openclaw]"`, OpenClaw will instead use `[{identity.name}]` when the routed
-agent has `identity.name` set.
-
-`ackReaction` sends a best-effort emoji reaction to acknowledge inbound messages
-on channels that support reactions (Slack/Discord/Telegram/Google Chat). Defaults to the
-active agent’s `identity.emoji` when set, otherwise `"👀"`. Set it to `""` to disable.
-
-`ackReactionScope` controls when reactions fire:
-
-- `group-mentions` (default): only when a group/room requires mentions **and** the bot was mentioned
-- `group-all`: all group/room messages
-- `direct`: direct messages only
-- `all`: all messages
-
-`removeAckAfterReply` removes the bot’s ack reaction after a reply is sent
-(Slack/Discord/Telegram/Google Chat only). Default: `false`.
-
-#### `messages.tts`
-
-Enable text-to-speech for outbound replies. When on, OpenClaw generates audio
-using ElevenLabs or OpenAI and attaches it to responses. Telegram uses Opus
-voice notes; other channels send MP3 audio.
-
-```json5
-{
- messages: {
- tts: {
- auto: "always", // off | always | inbound | tagged
- mode: "final", // final | all (include tool/block replies)
- provider: "elevenlabs",
- summaryModel: "openai/gpt-4.1-mini",
- modelOverrides: {
- enabled: true,
- },
- maxTextLength: 4000,
- timeoutMs: 30000,
- prefsPath: "~/.openclaw/settings/tts.json",
- elevenlabs: {
- apiKey: "elevenlabs_api_key",
- baseUrl: "https://api.elevenlabs.io",
- voiceId: "voice_id",
- modelId: "eleven_multilingual_v2",
- seed: 42,
- applyTextNormalization: "auto",
- languageCode: "en",
- voiceSettings: {
- stability: 0.5,
- similarityBoost: 0.75,
- style: 0.0,
- useSpeakerBoost: true,
- speed: 1.0,
- },
- },
- openai: {
- apiKey: "openai_api_key",
- model: "gpt-4o-mini-tts",
- voice: "alloy",
- },
- },
- },
-}
-```
-
-Notes:
-
-- `messages.tts.auto` controls auto‑TTS (`off`, `always`, `inbound`, `tagged`).
-- `/tts off|always|inbound|tagged` sets the per‑session auto mode (overrides config).
-- `messages.tts.enabled` is legacy; doctor migrates it to `messages.tts.auto`.
-- `prefsPath` stores local overrides (provider/limit/summarize).
-- `maxTextLength` is a hard cap for TTS input; summaries are truncated to fit.
-- `summaryModel` overrides `agents.defaults.model.primary` for auto-summary.
- - Accepts `provider/model` or an alias from `agents.defaults.models`.
-- `modelOverrides` enables model-driven overrides like `[[tts:...]]` tags (on by default).
-- `/tts limit` and `/tts summary` control per-user summarization settings.
-- `apiKey` values fall back to `ELEVENLABS_API_KEY`/`XI_API_KEY` and `OPENAI_API_KEY`.
-- `elevenlabs.baseUrl` overrides the ElevenLabs API base URL.
-- `elevenlabs.voiceSettings` supports `stability`/`similarityBoost`/`style` (0..1),
- `useSpeakerBoost`, and `speed` (0.5..2.0).
-
-### `talk`
-
-Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID` when unset.
-`apiKey` falls back to `ELEVENLABS_API_KEY` (or the gateway’s shell profile) when unset.
-`voiceAliases` lets Talk directives use friendly names (e.g. `"voice":"Clawd"`).
-
-```json5
-{
- talk: {
- voiceId: "elevenlabs_voice_id",
- voiceAliases: {
- Clawd: "EXAVITQu4vr4xnSDxMaL",
- Roger: "CwhRBWXzGAHq8TQ4Fs17",
- },
- modelId: "eleven_v3",
- outputFormat: "mp3_44100_128",
- apiKey: "elevenlabs_api_key",
- interruptOnSpeech: true,
- },
-}
-```
-
-### `agents.defaults`
-
-Controls the embedded agent runtime (model/thinking/verbose/timeouts).
-`agents.defaults.models` defines the configured model catalog (and acts as the allowlist for `/model`).
-`agents.defaults.model.primary` sets the default model; `agents.defaults.model.fallbacks` are global failovers.
-`agents.defaults.imageModel` is optional and is **only used if the primary model lacks image input**.
-Each `agents.defaults.models` entry can include:
-
-- `alias` (optional model shortcut, e.g. `/opus`).
-- `params` (optional provider-specific API params passed through to the model request).
-
-`params` is also applied to streaming runs (embedded agent + compaction). Supported keys today: `temperature`, `maxTokens`. These merge with call-time options; caller-supplied values win. `temperature` is an advanced knob—leave unset unless you know the model’s defaults and need a change.
-
-Example:
-
-```json5
-{
- agents: {
- defaults: {
- models: {
- "anthropic/claude-sonnet-4-5-20250929": {
- params: { temperature: 0.6 },
- },
- "openai/gpt-5.2": {
- params: { maxTokens: 8192 },
- },
- },
- },
- },
-}
-```
-
-Z.AI GLM-4.x models automatically enable thinking mode unless you:
-
-- set `--thinking off`, or
-- define `agents.defaults.models["zai/"].params.thinking` yourself.
-
-OpenClaw also ships a few built-in alias shorthands. Defaults only apply when the model
-is already present in `agents.defaults.models`:
-
-- `opus` -> `anthropic/claude-opus-4-6`
-- `sonnet` -> `anthropic/claude-sonnet-4-5`
-- `gpt` -> `openai/gpt-5.2`
-- `gpt-mini` -> `openai/gpt-5-mini`
-- `gemini` -> `google/gemini-3-pro-preview`
-- `gemini-flash` -> `google/gemini-3-flash-preview`
-
-If you configure the same alias name (case-insensitive) yourself, your value wins (defaults never override).
-
-Example: Opus 4.6 primary with MiniMax M2.1 fallback (hosted MiniMax):
-
-```json5
-{
- agents: {
- defaults: {
- models: {
- "anthropic/claude-opus-4-6": { alias: "opus" },
- "minimax/MiniMax-M2.1": { alias: "minimax" },
- },
- model: {
- primary: "anthropic/claude-opus-4-6",
- fallbacks: ["minimax/MiniMax-M2.1"],
- },
- },
- },
-}
-```
-
-MiniMax auth: set `MINIMAX_API_KEY` (env) or configure `models.providers.minimax`.
-
-#### `agents.defaults.cliBackends` (CLI fallback)
-
-Optional CLI backends for text-only fallback runs (no tool calls). These are useful as a
-backup path when API providers fail. Image pass-through is supported when you configure
-an `imageArg` that accepts file paths.
-
-Notes:
-
-- CLI backends are **text-first**; tools are always disabled.
-- Sessions are supported when `sessionArg` is set; session ids are persisted per backend.
-- For `claude-cli`, defaults are wired in. Override the command path if PATH is minimal
- (launchd/systemd).
-
-Example:
-
-```json5
-{
- agents: {
- defaults: {
- cliBackends: {
- "claude-cli": {
- command: "/opt/homebrew/bin/claude",
- },
- "my-cli": {
- command: "my-cli",
- args: ["--json"],
- output: "json",
- modelArg: "--model",
- sessionArg: "--session",
- sessionMode: "existing",
- systemPromptArg: "--system",
- systemPromptWhen: "first",
- imageArg: "--image",
- imageMode: "repeat",
- },
- },
- },
- },
-}
-```
-
-```json5
-{
- agents: {
- defaults: {
- models: {
- "anthropic/claude-opus-4-6": { alias: "Opus" },
- "anthropic/claude-sonnet-4-1": { alias: "Sonnet" },
- "openrouter/deepseek/deepseek-r1:free": {},
- "zai/glm-4.7": {
- alias: "GLM",
- params: {
- thinking: {
- type: "enabled",
- clear_thinking: false,
- },
- },
- },
- },
- model: {
- primary: "anthropic/claude-opus-4-6",
- fallbacks: [
- "openrouter/deepseek/deepseek-r1:free",
- "openrouter/meta-llama/llama-3.3-70b-instruct:free",
- ],
- },
- imageModel: {
- primary: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
- fallbacks: ["openrouter/google/gemini-2.0-flash-vision:free"],
- },
- thinkingDefault: "low",
- verboseDefault: "off",
- elevatedDefault: "on",
- timeoutSeconds: 600,
- mediaMaxMb: 5,
- heartbeat: {
- every: "30m",
- target: "last",
- },
- maxConcurrent: 3,
- subagents: {
- model: "minimax/MiniMax-M2.1",
- maxConcurrent: 1,
- archiveAfterMinutes: 60,
- },
- exec: {
- backgroundMs: 10000,
- timeoutSec: 1800,
- cleanupMs: 1800000,
- },
- contextTokens: 200000,
- },
- },
-}
-```
-
-#### `agents.defaults.contextPruning` (tool-result pruning)
-
-`agents.defaults.contextPruning` prunes **old tool results** from the in-memory context right before a request is sent to the LLM.
-It does **not** modify the session history on disk (`*.jsonl` remains complete).
-
-This is intended to reduce token usage for chatty agents that accumulate large tool outputs over time.
-
-High level:
-
-- Never touches user/assistant messages.
-- Protects the last `keepLastAssistants` assistant messages (no tool results after that point are pruned).
-- Protects the bootstrap prefix (nothing before the first user message is pruned).
-- Modes:
- - `adaptive`: soft-trims oversized tool results (keep head/tail) when the estimated context ratio crosses `softTrimRatio`.
- Then hard-clears the oldest eligible tool results when the estimated context ratio crosses `hardClearRatio` **and**
- there’s enough prunable tool-result bulk (`minPrunableToolChars`).
- - `aggressive`: always replaces eligible tool results before the cutoff with the `hardClear.placeholder` (no ratio checks).
-
-Soft vs hard pruning (what changes in the context sent to the LLM):
-
-- **Soft-trim**: only for _oversized_ tool results. Keeps the beginning + end and inserts `...` in the middle.
- - Before: `toolResult("…very long output…")`
- - After: `toolResult("HEAD…\n...\n…TAIL\n\n[Tool result trimmed: …]")`
-- **Hard-clear**: replaces the entire tool result with the placeholder.
- - Before: `toolResult("…very long output…")`
- - After: `toolResult("[Old tool result content cleared]")`
-
-Notes / current limitations:
-
-- Tool results containing **image blocks are skipped** (never trimmed/cleared) right now.
-- The estimated “context ratio” is based on **characters** (approximate), not exact tokens.
-- If the session doesn’t contain at least `keepLastAssistants` assistant messages yet, pruning is skipped.
-- In `aggressive` mode, `hardClear.enabled` is ignored (eligible tool results are always replaced with `hardClear.placeholder`).
-
-Default (adaptive):
-
-```json5
-{
- agents: { defaults: { contextPruning: { mode: "adaptive" } } },
-}
-```
-
-To disable:
-
-```json5
-{
- agents: { defaults: { contextPruning: { mode: "off" } } },
-}
-```
-
-Defaults (when `mode` is `"adaptive"` or `"aggressive"`):
-
-- `keepLastAssistants`: `3`
-- `softTrimRatio`: `0.3` (adaptive only)
-- `hardClearRatio`: `0.5` (adaptive only)
-- `minPrunableToolChars`: `50000` (adaptive only)
-- `softTrim`: `{ maxChars: 4000, headChars: 1500, tailChars: 1500 }` (adaptive only)
-- `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }`
-
-Example (aggressive, minimal):
-
-```json5
-{
- agents: { defaults: { contextPruning: { mode: "aggressive" } } },
-}
-```
-
-Example (adaptive tuned):
-
-```json5
-{
- agents: {
- defaults: {
- contextPruning: {
- mode: "adaptive",
- keepLastAssistants: 3,
- softTrimRatio: 0.3,
- hardClearRatio: 0.5,
- minPrunableToolChars: 50000,
- softTrim: { maxChars: 4000, headChars: 1500, tailChars: 1500 },
- hardClear: { enabled: true, placeholder: "[Old tool result content cleared]" },
- // Optional: restrict pruning to specific tools (deny wins; supports "*" wildcards)
- tools: { deny: ["browser", "canvas"] },
- },
- },
- },
-}
-```
-
-See [/concepts/session-pruning](/concepts/session-pruning) for behavior details.
-
-#### `agents.defaults.compaction` (reserve headroom + memory flush)
-
-`agents.defaults.compaction.mode` selects the compaction summarization strategy. Defaults to `default`; set `safeguard` to enable chunked summarization for very long histories. See [/concepts/compaction](/concepts/compaction).
-
-`agents.defaults.compaction.reserveTokensFloor` enforces a minimum `reserveTokens`
-value for Pi compaction (default: `20000`). Set it to `0` to disable the floor.
-
-`agents.defaults.compaction.memoryFlush` runs a **silent** agentic turn before
-auto-compaction, instructing the model to store durable memories on disk (e.g.
-`memory/YYYY-MM-DD.md`). It triggers when the session token estimate crosses a
-soft threshold below the compaction limit.
-
-Legacy defaults:
-
-- `memoryFlush.enabled`: `true`
-- `memoryFlush.softThresholdTokens`: `4000`
-- `memoryFlush.prompt` / `memoryFlush.systemPrompt`: built-in defaults with `NO_REPLY`
-- Note: memory flush is skipped when the session workspace is read-only
- (`agents.defaults.sandbox.workspaceAccess: "ro"` or `"none"`).
-
-Example (tuned):
-
-```json5
-{
- agents: {
- defaults: {
- compaction: {
- mode: "safeguard",
- reserveTokensFloor: 24000,
- memoryFlush: {
- enabled: true,
- softThresholdTokens: 6000,
- systemPrompt: "Session nearing compaction. Store durable memories now.",
- prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
- },
- },
- },
- },
-}
-```
-
-Block streaming:
-
-- `agents.defaults.blockStreamingDefault`: `"on"`/`"off"` (default off).
-- Channel overrides: `*.blockStreaming` (and per-account variants) to force block streaming on/off.
- Non-Telegram channels require an explicit `*.blockStreaming: true` to enable block replies.
-- `agents.defaults.blockStreamingBreak`: `"text_end"` or `"message_end"` (default: text_end).
-- `agents.defaults.blockStreamingChunk`: soft chunking for streamed blocks. Defaults to
- 800–1200 chars, prefers paragraph breaks (`\n\n`), then newlines, then sentences.
- Example:
-
- ```json5
- {
- agents: { defaults: { blockStreamingChunk: { minChars: 800, maxChars: 1200 } } },
- }
- ```
-
-- `agents.defaults.blockStreamingCoalesce`: merge streamed blocks before sending.
- Defaults to `{ idleMs: 1000 }` and inherits `minChars` from `blockStreamingChunk`
- with `maxChars` capped to the channel text limit. Signal/Slack/Discord/Google Chat default
- to `minChars: 1500` unless overridden.
- Channel overrides: `channels.whatsapp.blockStreamingCoalesce`, `channels.telegram.blockStreamingCoalesce`,
- `channels.discord.blockStreamingCoalesce`, `channels.slack.blockStreamingCoalesce`, `channels.mattermost.blockStreamingCoalesce`,
- `channels.signal.blockStreamingCoalesce`, `channels.imessage.blockStreamingCoalesce`, `channels.msteams.blockStreamingCoalesce`,
- `channels.googlechat.blockStreamingCoalesce`
- (and per-account variants).
-- `agents.defaults.humanDelay`: randomized pause between **block replies** after the first.
- Modes: `off` (default), `natural` (800–2500ms), `custom` (use `minMs`/`maxMs`).
- Per-agent override: `agents.list[].humanDelay`.
- Example:
-
- ```json5
- {
- agents: { defaults: { humanDelay: { mode: "natural" } } },
- }
- ```
-
- See [/concepts/streaming](/concepts/streaming) for behavior + chunking details.
-
-Typing indicators:
-
-- `agents.defaults.typingMode`: `"never" | "instant" | "thinking" | "message"`. Defaults to
- `instant` for direct chats / mentions and `message` for unmentioned group chats.
-- `session.typingMode`: per-session override for the mode.
-- `agents.defaults.typingIntervalSeconds`: how often the typing signal is refreshed (default: 6s).
-- `session.typingIntervalSeconds`: per-session override for the refresh interval.
- See [/concepts/typing-indicators](/concepts/typing-indicators) for behavior details.
-
-`agents.defaults.model.primary` should be set as `provider/model` (e.g. `anthropic/claude-opus-4-6`).
-Aliases come from `agents.defaults.models.*.alias` (e.g. `Opus`).
-If you omit the provider, OpenClaw currently assumes `anthropic` as a temporary
-deprecation fallback.
-Z.AI models are available as `zai/` (e.g. `zai/glm-4.7`) and require
-`ZAI_API_KEY` (or legacy `Z_AI_API_KEY`) in the environment.
-
-`agents.defaults.heartbeat` configures periodic heartbeat runs:
-
-- `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Default:
- `30m`. Set `0m` to disable.
-- `model`: optional override model for heartbeat runs (`provider/model`).
-- `includeReasoning`: when `true`, heartbeats will also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`). Default: `false`.
-- `session`: optional session key to control which session the heartbeat runs in. Default: `main`.
-- `to`: optional recipient override (channel-specific id, e.g. E.164 for WhatsApp, chat id for Telegram).
-- `target`: optional delivery channel (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `msteams`, `signal`, `imessage`, `none`). Default: `last`.
-- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`). Overrides are sent verbatim; include a `Read HEARTBEAT.md` line if you still want the file read.
-- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 300).
-
-Per-agent heartbeats:
-
-- Set `agents.list[].heartbeat` to enable or override heartbeat settings for a specific agent.
-- If any agent entry defines `heartbeat`, **only those agents** run heartbeats; defaults
- become the shared baseline for those agents.
-
-Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful
-of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`.
-
-`tools.exec` configures background exec defaults:
-
-- `backgroundMs`: time before auto-background (ms, default 10000)
-- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
-- `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000)
-- `notifyOnExit`: enqueue a system event + request heartbeat when backgrounded exec exits (default true)
-- `applyPatch.enabled`: enable experimental `apply_patch` (OpenAI/OpenAI Codex only; default false)
-- `applyPatch.allowModels`: optional allowlist of model ids (e.g. `gpt-5.2` or `openai/gpt-5.2`)
- Note: `applyPatch` is only under `tools.exec`.
-
-`tools.web` configures web search + fetch tools:
-
-- `tools.web.search.enabled` (default: true when key is present)
-- `tools.web.search.apiKey` (recommended: set via `openclaw configure --section web`, or use `BRAVE_API_KEY` env var)
-- `tools.web.search.maxResults` (1–10, default 5)
-- `tools.web.search.timeoutSeconds` (default 30)
-- `tools.web.search.cacheTtlMinutes` (default 15)
-- `tools.web.fetch.enabled` (default true)
-- `tools.web.fetch.maxChars` (default 50000)
-- `tools.web.fetch.maxCharsCap` (default 50000; clamps maxChars from config/tool calls)
-- `tools.web.fetch.timeoutSeconds` (default 30)
-- `tools.web.fetch.cacheTtlMinutes` (default 15)
-- `tools.web.fetch.userAgent` (optional override)
-- `tools.web.fetch.readability` (default true; disable to use basic HTML cleanup only)
-- `tools.web.fetch.firecrawl.enabled` (default true when an API key is set)
-- `tools.web.fetch.firecrawl.apiKey` (optional; defaults to `FIRECRAWL_API_KEY`)
-- `tools.web.fetch.firecrawl.baseUrl` (default [https://api.firecrawl.dev](https://api.firecrawl.dev))
-- `tools.web.fetch.firecrawl.onlyMainContent` (default true)
-- `tools.web.fetch.firecrawl.maxAgeMs` (optional)
-- `tools.web.fetch.firecrawl.timeoutSeconds` (optional)
-
-`tools.media` configures inbound media understanding (image/audio/video):
-
-- `tools.media.models`: shared model list (capability-tagged; used after per-cap lists).
-- `tools.media.concurrency`: max concurrent capability runs (default 2).
-- `tools.media.image` / `tools.media.audio` / `tools.media.video`:
- - `enabled`: opt-out switch (default true when models are configured).
- - `prompt`: optional prompt override (image/video append a `maxChars` hint automatically).
- - `maxChars`: max output characters (default 500 for image/video; unset for audio).
- - `maxBytes`: max media size to send (defaults: image 10MB, audio 20MB, video 50MB).
- - `timeoutSeconds`: request timeout (defaults: image 60s, audio 60s, video 120s).
- - `language`: optional audio hint.
- - `attachments`: attachment policy (`mode`, `maxAttachments`, `prefer`).
- - `scope`: optional gating (first match wins) with `match.channel`, `match.chatType`, or `match.keyPrefix`.
- - `models`: ordered list of model entries; failures or oversize media fall back to the next entry.
-- Each `models[]` entry:
- - Provider entry (`type: "provider"` or omitted):
- - `provider`: API provider id (`openai`, `anthropic`, `google`/`gemini`, `groq`, etc).
- - `model`: model id override (required for image; defaults to `gpt-4o-mini-transcribe`/`whisper-large-v3-turbo` for audio providers, and `gemini-3-flash-preview` for video).
- - `profile` / `preferredProfile`: auth profile selection.
- - CLI entry (`type: "cli"`):
- - `command`: executable to run.
- - `args`: templated args (supports `{{MediaPath}}`, `{{Prompt}}`, `{{MaxChars}}`, etc).
- - `capabilities`: optional list (`image`, `audio`, `video`) to gate a shared entry. Defaults when omitted: `openai`/`anthropic`/`minimax` → image, `google` → image+audio+video, `groq` → audio.
- - `prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language` can be overridden per entry.
-
-If no models are configured (or `enabled: false`), understanding is skipped; the model still receives the original attachments.
-
-Provider auth follows the standard model auth order (auth profiles, env vars like `OPENAI_API_KEY`/`GROQ_API_KEY`/`GEMINI_API_KEY`, or `models.providers.*.apiKey`).
-
-Example:
-
-```json5
-{
- tools: {
- media: {
- audio: {
- enabled: true,
- maxBytes: 20971520,
- scope: {
- default: "deny",
- rules: [{ action: "allow", match: { chatType: "direct" } }],
- },
- models: [
- { provider: "openai", model: "gpt-4o-mini-transcribe" },
- { type: "cli", command: "whisper", args: ["--model", "base", "{{MediaPath}}"] },
- ],
- },
- video: {
- enabled: true,
- maxBytes: 52428800,
- models: [{ provider: "google", model: "gemini-3-flash-preview" }],
- },
- },
- },
-}
-```
-
-`agents.defaults.subagents` configures sub-agent defaults:
-
-- `model`: default model for spawned sub-agents (string or `{ primary, fallbacks }`). If omitted, sub-agents inherit the caller’s model unless overridden per agent or per call.
-- `maxConcurrent`: max concurrent sub-agent runs (default 1)
-- `archiveAfterMinutes`: auto-archive sub-agent sessions after N minutes (default 60; set `0` to disable)
-- Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny` (deny wins)
-
-`tools.profile` sets a **base tool allowlist** before `tools.allow`/`tools.deny`:
-
-- `minimal`: `session_status` only
-- `coding`: `group:fs`, `group:runtime`, `group:sessions`, `group:memory`, `image`
-- `messaging`: `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status`
-- `full`: no restriction (same as unset)
-
-Per-agent override: `agents.list[].tools.profile`.
-
-Example (messaging-only by default, allow Slack + Discord tools too):
-
-```json5
-{
- tools: {
- profile: "messaging",
- allow: ["slack", "discord"],
- },
-}
-```
-
-Example (coding profile, but deny exec/process everywhere):
-
-```json5
-{
- tools: {
- profile: "coding",
- deny: ["group:runtime"],
- },
-}
-```
-
-`tools.byProvider` lets you **further restrict** tools for specific providers (or a single `provider/model`).
-Per-agent override: `agents.list[].tools.byProvider`.
-
-Order: base profile → provider profile → allow/deny policies.
-Provider keys accept either `provider` (e.g. `google-antigravity`) or `provider/model`
-(e.g. `openai/gpt-5.2`).
-
-Example (keep global coding profile, but minimal tools for Google Antigravity):
-
-```json5
-{
- tools: {
- profile: "coding",
- byProvider: {
- "google-antigravity": { profile: "minimal" },
- },
- },
-}
-```
-
-Example (provider/model-specific allowlist):
-
-```json5
-{
- tools: {
- allow: ["group:fs", "group:runtime", "sessions_list"],
- byProvider: {
- "openai/gpt-5.2": { allow: ["group:fs", "sessions_list"] },
- },
- },
-}
-```
-
-`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
-Matching is case-insensitive and supports `*` wildcards (`"*"` means all tools).
-This is applied even when the Docker sandbox is **off**.
-
-Example (disable browser/canvas everywhere):
-
-```json5
-{
- tools: { deny: ["browser", "canvas"] },
-}
-```
-
-Tool groups (shorthands) work in **global** and **per-agent** tool policies:
-
-- `group:runtime`: `exec`, `bash`, `process`
-- `group:fs`: `read`, `write`, `edit`, `apply_patch`
-- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status`
-- `group:memory`: `memory_search`, `memory_get`
-- `group:web`: `web_search`, `web_fetch`
-- `group:ui`: `browser`, `canvas`
-- `group:automation`: `cron`, `gateway`
-- `group:messaging`: `message`
-- `group:nodes`: `nodes`
-- `group:openclaw`: all built-in OpenClaw tools (excludes provider plugins)
-
-`tools.elevated` controls elevated (host) exec access:
-
-- `enabled`: allow elevated mode (default true)
-- `allowFrom`: per-channel allowlists (empty = disabled)
- - `whatsapp`: E.164 numbers
- - `telegram`: chat ids or usernames
- - `discord`: user ids or usernames (falls back to `channels.discord.dm.allowFrom` if omitted)
- - `signal`: E.164 numbers
- - `imessage`: handles/chat ids
- - `webchat`: session ids or usernames
-
-Example:
-
-```json5
-{
- tools: {
- elevated: {
- enabled: true,
- allowFrom: {
- whatsapp: ["+15555550123"],
- discord: ["steipete", "1234567890123"],
- },
- },
- },
-}
-```
-
-Per-agent override (further restrict):
-
-```json5
-{
- agents: {
- list: [
- {
- id: "family",
- tools: {
- elevated: { enabled: false },
- },
- },
- ],
- },
-}
-```
-
-Notes:
-
-- `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can only further restrict (both must allow).
-- `/elevated on|off|ask|full` stores state per session key; inline directives apply to a single message.
-- Elevated `exec` runs on the host and bypasses sandboxing.
-- Tool policy still applies; if `exec` is denied, elevated cannot be used.
-
-`agents.defaults.maxConcurrent` sets the maximum number of embedded agent runs that can
-execute in parallel across sessions. Each session is still serialized (one run
-per session key at a time). Default: 1.
-
-### `agents.defaults.sandbox`
-
-Optional **Docker sandboxing** for the embedded agent. Intended for non-main
-sessions so they cannot access your host system.
-
-Details: [Sandboxing](/gateway/sandboxing)
-
-Defaults (if enabled):
-
-- scope: `"agent"` (one container + workspace per agent)
-- Debian bookworm-slim based image
-- agent workspace access: `workspaceAccess: "none"` (default)
- - `"none"`: use a per-scope sandbox workspace under `~/.openclaw/sandboxes`
-- `"ro"`: keep the sandbox workspace at `/workspace`, and mount the agent workspace read-only at `/agent` (disables `write`/`edit`/`apply_patch`)
- - `"rw"`: mount the agent workspace read/write at `/workspace`
-- auto-prune: idle > 24h OR age > 7d
-- tool policy: allow only `exec`, `process`, `read`, `write`, `edit`, `apply_patch`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` (deny wins)
- - configure via `tools.sandbox.tools`, override per-agent via `agents.list[].tools.sandbox.tools`
- - tool group shorthands supported in sandbox policy: `group:runtime`, `group:fs`, `group:sessions`, `group:memory` (see [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated#tool-groups-shorthands))
-- optional sandboxed browser (Chromium + CDP, noVNC observer)
-- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`
-
-Warning: `scope: "shared"` means a shared container and shared workspace. No
-cross-session isolation. Use `scope: "session"` for per-session isolation.
-
-Legacy: `perSession` is still supported (`true` → `scope: "session"`,
-`false` → `scope: "shared"`).
-
-`setupCommand` runs **once** after the container is created (inside the container via `sh -lc`).
-For package installs, ensure network egress, a writable root FS, and a root user.
-
-```json5
-{
- agents: {
- defaults: {
- sandbox: {
- mode: "non-main", // off | non-main | all
- scope: "agent", // session | agent | shared (agent is default)
- workspaceAccess: "none", // none | ro | rw
- workspaceRoot: "~/.openclaw/sandboxes",
- docker: {
- image: "openclaw-sandbox:bookworm-slim",
- containerPrefix: "openclaw-sbx-",
- workdir: "/workspace",
- readOnlyRoot: true,
- tmpfs: ["/tmp", "/var/tmp", "/run"],
- network: "none",
- user: "1000:1000",
- capDrop: ["ALL"],
- env: { LANG: "C.UTF-8" },
- setupCommand: "apt-get update && apt-get install -y git curl jq",
- // Per-agent override (multi-agent): agents.list[].sandbox.docker.*
- pidsLimit: 256,
- memory: "1g",
- memorySwap: "2g",
- cpus: 1,
- ulimits: {
- nofile: { soft: 1024, hard: 2048 },
- nproc: 256,
- },
- seccompProfile: "/path/to/seccomp.json",
- apparmorProfile: "openclaw-sandbox",
- dns: ["1.1.1.1", "8.8.8.8"],
- extraHosts: ["internal.service:10.0.0.5"],
- binds: ["/var/run/docker.sock:/var/run/docker.sock", "/home/user/source:/source:rw"],
- },
- browser: {
- enabled: false,
- image: "openclaw-sandbox-browser:bookworm-slim",
- containerPrefix: "openclaw-sbx-browser-",
- cdpPort: 9222,
- vncPort: 5900,
- noVncPort: 6080,
- headless: false,
- enableNoVnc: true,
- allowHostControl: false,
- allowedControlUrls: ["http://10.0.0.42:18791"],
- allowedControlHosts: ["browser.lab.local", "10.0.0.42"],
- allowedControlPorts: [18791],
- autoStart: true,
- autoStartTimeoutMs: 12000,
- },
- prune: {
- idleHours: 24, // 0 disables idle pruning
- maxAgeDays: 7, // 0 disables max-age pruning
- },
- },
- },
- },
- tools: {
- sandbox: {
- tools: {
- allow: [
- "exec",
- "process",
- "read",
- "write",
- "edit",
- "apply_patch",
- "sessions_list",
- "sessions_history",
- "sessions_send",
- "sessions_spawn",
- "session_status",
- ],
- deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"],
- },
- },
- },
-}
-```
-
-Build the default sandbox image once with:
-
-```bash
-scripts/sandbox-setup.sh
-```
-
-Note: sandbox containers default to `network: "none"`; set `agents.defaults.sandbox.docker.network`
-to `"bridge"` (or your custom network) if the agent needs outbound access.
-
-Note: inbound attachments are staged into the active workspace at `media/inbound/*`. With `workspaceAccess: "rw"`, that means files are written into the agent workspace.
-
-Note: `docker.binds` mounts additional host directories; global and per-agent binds are merged.
-
-Build the optional browser image with:
-
-```bash
-scripts/sandbox-browser-setup.sh
-```
-
-When `agents.defaults.sandbox.browser.enabled=true`, the browser tool uses a sandboxed
-Chromium instance (CDP). If noVNC is enabled (default when headless=false),
-the noVNC URL is injected into the system prompt so the agent can reference it.
-This does not require `browser.enabled` in the main config; the sandbox control
-URL is injected per session.
-
-`agents.defaults.sandbox.browser.allowHostControl` (default: false) allows
-sandboxed sessions to explicitly target the **host** browser control server
-via the browser tool (`target: "host"`). Leave this off if you want strict
-sandbox isolation.
-
-Allowlists for remote control:
-
-- `allowedControlUrls`: exact control URLs permitted for `target: "custom"`.
-- `allowedControlHosts`: hostnames permitted (hostname only, no port).
-- `allowedControlPorts`: ports permitted (defaults: http=80, https=443).
- Defaults: all allowlists are unset (no restriction). `allowHostControl` defaults to false.
-
-### `models` (custom providers + base URLs)
-
-OpenClaw uses the **pi-coding-agent** model catalog. You can add custom providers
-(LiteLLM, local OpenAI-compatible servers, Anthropic proxies, etc.) by writing
-`~/.openclaw/agents//agent/models.json` or by defining the same schema inside your
-OpenClaw config under `models.providers`.
-Provider-by-provider overview + examples: [/concepts/model-providers](/concepts/model-providers).
-
-When `models.providers` is present, OpenClaw writes/merges a `models.json` into
-`~/.openclaw/agents//agent/` on startup:
-
-- default behavior: **merge** (keeps existing providers, overrides on name)
-- set `models.mode: "replace"` to overwrite the file contents
-
-Select the model via `agents.defaults.model.primary` (provider/model).
-
-```json5
-{
- agents: {
- defaults: {
- model: { primary: "custom-proxy/llama-3.1-8b" },
- models: {
- "custom-proxy/llama-3.1-8b": {},
- },
- },
- },
- models: {
- mode: "merge",
- providers: {
- "custom-proxy": {
- baseUrl: "http://localhost:4000/v1",
- apiKey: "LITELLM_KEY",
- api: "openai-completions",
- models: [
- {
- id: "llama-3.1-8b",
- name: "Llama 3.1 8B",
- reasoning: false,
- input: ["text"],
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
- contextWindow: 128000,
- maxTokens: 32000,
- },
- ],
- },
- },
- },
-}
-```
-
-### OpenCode Zen (multi-model proxy)
-
-OpenCode Zen is a multi-model gateway with per-model endpoints. OpenClaw uses
-the built-in `opencode` provider from pi-ai; set `OPENCODE_API_KEY` (or
-`OPENCODE_ZEN_API_KEY`) from [https://opencode.ai/auth](https://opencode.ai/auth).
-
-Notes:
-
-- Model refs use `opencode/` (example: `opencode/claude-opus-4-6`).
-- If you enable an allowlist via `agents.defaults.models`, add each model you plan to use.
-- Shortcut: `openclaw onboard --auth-choice opencode-zen`.
-
-```json5
-{
- agents: {
- defaults: {
- model: { primary: "opencode/claude-opus-4-6" },
- models: { "opencode/claude-opus-4-6": { alias: "Opus" } },
- },
- },
-}
-```
-
-### Z.AI (GLM-4.7) — provider alias support
-
-Z.AI models are available via the built-in `zai` provider. Set `ZAI_API_KEY`
-in your environment and reference the model by provider/model.
-
-Shortcut: `openclaw onboard --auth-choice zai-api-key`.
-
-```json5
-{
- agents: {
- defaults: {
- model: { primary: "zai/glm-4.7" },
- models: { "zai/glm-4.7": {} },
- },
- },
-}
-```
-
-Notes:
-
-- `z.ai/*` and `z-ai/*` are accepted aliases and normalize to `zai/*`.
-- If `ZAI_API_KEY` is missing, requests to `zai/*` will fail with an auth error at runtime.
-- Example error: `No API key found for provider "zai".`
-- Z.AI’s general API endpoint is `https://api.z.ai/api/paas/v4`. GLM coding
- requests use the dedicated Coding endpoint `https://api.z.ai/api/coding/paas/v4`.
- The built-in `zai` provider uses the Coding endpoint. If you need the general
- endpoint, define a custom provider in `models.providers` with the base URL
- override (see the custom providers section above).
-- Use a fake placeholder in docs/configs; never commit real API keys.
-
-### Moonshot AI (Kimi)
-
-Use Moonshot's OpenAI-compatible endpoint:
-
-```json5
-{
- env: { MOONSHOT_API_KEY: "sk-..." },
- agents: {
- defaults: {
- model: { primary: "moonshot/kimi-k2.5" },
- models: { "moonshot/kimi-k2.5": { alias: "Kimi K2.5" } },
- },
- },
- models: {
- mode: "merge",
- providers: {
- moonshot: {
- baseUrl: "https://api.moonshot.ai/v1",
- apiKey: "${MOONSHOT_API_KEY}",
- api: "openai-completions",
- models: [
- {
- id: "kimi-k2.5",
- name: "Kimi K2.5",
- reasoning: false,
- input: ["text"],
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
- contextWindow: 256000,
- maxTokens: 8192,
- },
- ],
- },
- },
- },
-}
-```
-
-Notes:
-
-- Set `MOONSHOT_API_KEY` in the environment or use `openclaw onboard --auth-choice moonshot-api-key`.
-- Model ref: `moonshot/kimi-k2.5`.
-- For the China endpoint, either:
- - Run `openclaw onboard --auth-choice moonshot-api-key-cn` (wizard will set `https://api.moonshot.cn/v1`), or
- - Manually set `baseUrl: "https://api.moonshot.cn/v1"` in `models.providers.moonshot`.
-
-### Kimi Coding
-
-Use Moonshot AI's Kimi Coding endpoint (Anthropic-compatible, built-in provider):
-
-```json5
-{
- env: { KIMI_API_KEY: "sk-..." },
- agents: {
- defaults: {
- model: { primary: "kimi-coding/k2p5" },
- models: { "kimi-coding/k2p5": { alias: "Kimi K2.5" } },
- },
- },
-}
-```
-
-Notes:
-
-- Set `KIMI_API_KEY` in the environment or use `openclaw onboard --auth-choice kimi-code-api-key`.
-- Model ref: `kimi-coding/k2p5`.
-
-### Synthetic (Anthropic-compatible)
-
-Use Synthetic's Anthropic-compatible endpoint:
-
-```json5
-{
- env: { SYNTHETIC_API_KEY: "sk-..." },
- agents: {
- defaults: {
- model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.1" },
- models: { "synthetic/hf:MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax M2.1" } },
- },
- },
- models: {
- mode: "merge",
- providers: {
- synthetic: {
- baseUrl: "https://api.synthetic.new/anthropic",
- apiKey: "${SYNTHETIC_API_KEY}",
- api: "anthropic-messages",
- models: [
- {
- id: "hf:MiniMaxAI/MiniMax-M2.1",
- name: "MiniMax M2.1",
- reasoning: false,
- input: ["text"],
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
- contextWindow: 192000,
- maxTokens: 65536,
- },
- ],
- },
- },
- },
-}
-```
-
-Notes:
-
-- Set `SYNTHETIC_API_KEY` or use `openclaw onboard --auth-choice synthetic-api-key`.
-- Model ref: `synthetic/hf:MiniMaxAI/MiniMax-M2.1`.
-- Base URL should omit `/v1` because the Anthropic client appends it.
-
-### Local models (LM Studio) — recommended setup
-
-See [/gateway/local-models](/gateway/local-models) for the current local guidance. TL;DR: run MiniMax M2.1 via LM Studio Responses API on serious hardware; keep hosted models merged for fallback.
-
-### MiniMax M2.1
-
-Use MiniMax M2.1 directly without LM Studio:
-
-```json5
-{
- agent: {
- model: { primary: "minimax/MiniMax-M2.1" },
- models: {
- "anthropic/claude-opus-4-6": { alias: "Opus" },
- "minimax/MiniMax-M2.1": { alias: "Minimax" },
- },
- },
- models: {
- mode: "merge",
- providers: {
- minimax: {
- baseUrl: "https://api.minimax.io/anthropic",
- apiKey: "${MINIMAX_API_KEY}",
- api: "anthropic-messages",
- models: [
- {
- id: "MiniMax-M2.1",
- name: "MiniMax M2.1",
- reasoning: false,
- input: ["text"],
- // Pricing: update in models.json if you need exact cost tracking.
- cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
- contextWindow: 200000,
- maxTokens: 8192,
- },
- ],
- },
- },
- },
-}
-```
-
-Notes:
-
-- Set `MINIMAX_API_KEY` environment variable or use `openclaw onboard --auth-choice minimax-api`.
-- Available model: `MiniMax-M2.1` (default).
-- Update pricing in `models.json` if you need exact cost tracking.
-
-### Cerebras (GLM 4.6 / 4.7)
-
-Use Cerebras via their OpenAI-compatible endpoint:
-
-```json5
-{
- env: { CEREBRAS_API_KEY: "sk-..." },
- agents: {
- defaults: {
- model: {
- primary: "cerebras/zai-glm-4.7",
- fallbacks: ["cerebras/zai-glm-4.6"],
- },
- models: {
- "cerebras/zai-glm-4.7": { alias: "GLM 4.7 (Cerebras)" },
- "cerebras/zai-glm-4.6": { alias: "GLM 4.6 (Cerebras)" },
- },
- },
- },
- models: {
- mode: "merge",
- providers: {
- cerebras: {
- baseUrl: "https://api.cerebras.ai/v1",
- apiKey: "${CEREBRAS_API_KEY}",
- api: "openai-completions",
- models: [
- { id: "zai-glm-4.7", name: "GLM 4.7 (Cerebras)" },
- { id: "zai-glm-4.6", name: "GLM 4.6 (Cerebras)" },
- ],
- },
- },
- },
-}
-```
-
-Notes:
-
-- Use `cerebras/zai-glm-4.7` for Cerebras; use `zai/glm-4.7` for Z.AI direct.
-- Set `CEREBRAS_API_KEY` in the environment or config.
-
-Notes:
-
-- Supported APIs: `openai-completions`, `openai-responses`, `anthropic-messages`,
- `google-generative-ai`
-- Use `authHeader: true` + `headers` for custom auth needs.
-- Override the agent config root with `OPENCLAW_AGENT_DIR` (or `PI_CODING_AGENT_DIR`)
- if you want `models.json` stored elsewhere (default: `~/.openclaw/agents/main/agent`).
-
-### `session`
-
-Controls session scoping, reset policy, reset triggers, and where the session store is written.
-
-```json5
-{
- session: {
- scope: "per-sender",
- dmScope: "main",
- identityLinks: {
- alice: ["telegram:123456789", "discord:987654321012345678"],
- },
- reset: {
- mode: "daily",
- atHour: 4,
- idleMinutes: 60,
- },
- resetByType: {
- thread: { mode: "daily", atHour: 4 },
- direct: { mode: "idle", idleMinutes: 240 },
- group: { mode: "idle", idleMinutes: 120 },
- },
- resetTriggers: ["/new", "/reset"],
- // Default is already per-agent under ~/.openclaw/agents//sessions/sessions.json
- // You can override with {agentId} templating:
- store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
- maintenance: {
- mode: "warn",
- pruneAfter: "30d",
- maxEntries: 500,
- rotateBytes: "10mb",
- },
- // Direct chats collapse to agent:: (default: "main").
- mainKey: "main",
- agentToAgent: {
- // Max ping-pong reply turns between requester/target (0–5).
- maxPingPongTurns: 5,
- },
- sendPolicy: {
- rules: [{ action: "deny", match: { channel: "discord", chatType: "group" } }],
- default: "allow",
- },
- },
-}
-```
-
-Fields:
-
-- `mainKey`: direct-chat bucket key (default: `"main"`). Useful when you want to “rename” the primary DM thread without changing `agentId`.
- - Sandbox note: `agents.defaults.sandbox.mode: "non-main"` uses this key to detect the main session. Any session key that does not match `mainKey` (groups/channels) is sandboxed.
-- `dmScope`: how DM sessions are grouped (default: `"main"`).
- - `main`: all DMs share the main session for continuity.
- - `per-peer`: isolate DMs by sender id across channels.
- - `per-channel-peer`: isolate DMs per channel + sender (recommended for multi-user inboxes).
- - `per-account-channel-peer`: isolate DMs per account + channel + sender (recommended for multi-account inboxes).
- - Secure DM mode (recommended): set `session.dmScope: "per-channel-peer"` when multiple people can DM the bot (shared inboxes, multi-person allowlists, or `dmPolicy: "open"`).
-- `identityLinks`: map canonical ids to provider-prefixed peers so the same person shares a DM session across channels when using `per-peer`, `per-channel-peer`, or `per-account-channel-peer`.
- - Example: `alice: ["telegram:123456789", "discord:987654321012345678"]`.
-- `reset`: primary reset policy. Defaults to daily resets at 4:00 AM local time on the gateway host.
- - `mode`: `daily` or `idle` (default: `daily` when `reset` is present).
- - `atHour`: local hour (0-23) for the daily reset boundary.
- - `idleMinutes`: sliding idle window in minutes. When daily + idle are both configured, whichever expires first wins.
-- `resetByType`: per-session overrides for `direct`, `group`, and `thread`. Legacy `dm` key is accepted as an alias for `direct`.
- - If you only set legacy `session.idleMinutes` without any `reset`/`resetByType`, OpenClaw stays in idle-only mode for backward compatibility.
-- `heartbeatIdleMinutes`: optional idle override for heartbeat checks (daily reset still applies when enabled).
-- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
-- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
-- `sendPolicy.rules[]`: match by `channel`, `chatType` (`direct|group|room`), or `keyPrefix` (e.g. `cron:`). First deny wins; otherwise allow.
-- `maintenance`: session store maintenance settings for pruning, capping, and rotation.
- - `mode`: `"warn"` (default) warns the active session (best-effort delivery) when it would be evicted without enforcing maintenance. `"enforce"` applies pruning and rotation.
- - `pruneAfter`: remove entries older than this duration (for example `"30m"`, `"1h"`, `"30d"`). Default "30d".
- - `maxEntries`: cap the number of session entries kept (default 500).
- - `rotateBytes`: rotate `sessions.json` when it exceeds this size (for example `"10kb"`, `"1mb"`, `"10mb"`). Default "10mb".
-
-### `skills` (skills config)
-
-Controls bundled allowlist, install preferences, extra skill folders, and per-skill
-overrides. Applies to **bundled** skills and `~/.openclaw/skills` (workspace skills
-still win on name conflicts).
-
-Fields:
-
-- `allowBundled`: optional allowlist for **bundled** skills only. If set, only those
- bundled skills are eligible (managed/workspace skills unaffected).
-- `load.extraDirs`: additional skill directories to scan (lowest precedence).
-- `install.preferBrew`: prefer brew installers when available (default: true).
-- `install.nodeManager`: node installer preference (`npm` | `pnpm` | `yarn`, default: npm).
-- `entries.`: per-skill config overrides.
-
-Per-skill fields:
-
-- `enabled`: set `false` to disable a skill even if it’s bundled/installed.
-- `env`: environment variables injected for the agent run (only if not already set).
-- `apiKey`: optional convenience for skills that declare a primary env var (e.g. `nano-banana-pro` → `GEMINI_API_KEY`).
-
-Example:
-
-```json5
-{
- skills: {
- allowBundled: ["gemini", "peekaboo"],
- load: {
- extraDirs: ["~/Projects/agent-scripts/skills", "~/Projects/oss/some-skill-pack/skills"],
- },
- install: {
- preferBrew: true,
- nodeManager: "npm",
- },
- entries: {
- "nano-banana-pro": {
- apiKey: "GEMINI_KEY_HERE",
- env: {
- GEMINI_API_KEY: "GEMINI_KEY_HERE",
- },
- },
- peekaboo: { enabled: true },
- sag: { enabled: false },
- },
- },
-}
-```
-
-### `plugins` (extensions)
-
-Controls plugin discovery, allow/deny, and per-plugin config. Plugins are loaded
-from `~/.openclaw/extensions`, `/.openclaw/extensions`, plus any
-`plugins.load.paths` entries. **Config changes require a gateway restart.**
-See [/plugin](/tools/plugin) for full usage.
-
-Fields:
-
-- `enabled`: master toggle for plugin loading (default: true).
-- `allow`: optional allowlist of plugin ids; when set, only listed plugins load.
-- `deny`: optional denylist of plugin ids (deny wins).
-- `load.paths`: extra plugin files or directories to load (absolute or `~`).
-- `entries.`: per-plugin overrides.
- - `enabled`: set `false` to disable.
- - `config`: plugin-specific config object (validated by the plugin if provided).
-
-Example:
-
-```json5
-{
- plugins: {
- enabled: true,
- allow: ["voice-call"],
- load: {
- paths: ["~/Projects/oss/voice-call-extension"],
- },
- entries: {
- "voice-call": {
- enabled: true,
- config: {
- provider: "twilio",
- },
- },
- },
- },
-}
-```
-
-### `browser` (openclaw-managed browser)
-
-OpenClaw can start a **dedicated, isolated** Chrome/Brave/Edge/Chromium instance for openclaw and expose a small loopback control service.
-Profiles can point at a **remote** Chromium-based browser via `profiles..cdpUrl`. Remote
-profiles are attach-only (start/stop/reset are disabled).
-
-`browser.cdpUrl` remains for legacy single-profile configs and as the base
-scheme/host for profiles that only set `cdpPort`.
-
-Defaults:
-
-- enabled: `true`
-- evaluateEnabled: `true` (set `false` to disable `act:evaluate` and `wait --fn`)
-- control service: loopback only (port derived from `gateway.port`, default `18791`)
-- CDP URL: `http://127.0.0.1:18792` (control service + 1, legacy single-profile)
-- profile color: `#FF4500` (lobster-orange)
-- Note: the control server is started by the running gateway (OpenClaw.app menubar, or `openclaw gateway`).
-- Auto-detect order: default browser if Chromium-based; otherwise Chrome → Brave → Edge → Chromium → Chrome Canary.
-
-```json5
-{
- browser: {
- enabled: true,
- evaluateEnabled: true,
- // cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override
- defaultProfile: "chrome",
- profiles: {
- openclaw: { cdpPort: 18800, color: "#FF4500" },
- work: { cdpPort: 18801, color: "#0066CC" },
- remote: { cdpUrl: "http://10.0.0.42:9222", color: "#00AA00" },
- },
- color: "#FF4500",
- // Advanced:
- // headless: false,
- // noSandbox: false,
- // executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
- // attachOnly: false, // set true when tunneling a remote CDP to localhost
- },
-}
-```
-
-### `ui` (Appearance)
-
-Optional accent color used by the native apps for UI chrome (e.g. Talk Mode bubble tint).
-
-If unset, clients fall back to a muted light-blue.
-
-```json5
-{
- ui: {
- seamColor: "#FF4500", // hex (RRGGBB or #RRGGBB)
- // Optional: Control UI assistant identity override.
- // If unset, the Control UI uses the active agent identity (config or IDENTITY.md).
- assistant: {
- name: "OpenClaw",
- avatar: "CB", // emoji, short text, or image URL/data URI
- },
- },
-}
-```
-
-### `gateway` (Gateway server mode + bind)
-
-Use `gateway.mode` to explicitly declare whether this machine should run the Gateway.
-
-Defaults:
-
-- mode: **unset** (treated as “do not auto-start”)
-- bind: `loopback`
-- port: `18789` (single port for WS + HTTP)
-
-```json5
-{
- gateway: {
- mode: "local", // or "remote"
- port: 18789, // WS + HTTP multiplex
- bind: "loopback",
- // controlUi: { enabled: true, basePath: "/openclaw" }
- // auth: { mode: "token", token: "your-token" } // token gates WS + Control UI access
- // tailscale: { mode: "off" | "serve" | "funnel" }
- },
-}
-```
-
-Control UI base path:
-
-- `gateway.controlUi.basePath` sets the URL prefix where the Control UI is served.
-- Examples: `"/ui"`, `"/openclaw"`, `"/apps/openclaw"`.
-- Default: root (`/`) (unchanged).
-- `gateway.controlUi.root` sets the filesystem root for Control UI assets (default: `dist/control-ui`).
-- `gateway.controlUi.allowInsecureAuth` allows token-only auth for the Control UI when
- device identity is omitted (typically over HTTP). Default: `false`. Prefer HTTPS
- (Tailscale Serve) or `127.0.0.1`.
-- `gateway.controlUi.dangerouslyDisableDeviceAuth` disables device identity checks for the
- Control UI (token/password only). Default: `false`. Break-glass only.
-
-Related docs:
-
-- [Control UI](/web/control-ui)
-- [Web overview](/web)
-- [Tailscale](/gateway/tailscale)
-- [Remote access](/gateway/remote)
-
-Trusted proxies:
-
-- `gateway.trustedProxies`: list of reverse proxy IPs that terminate TLS in front of the Gateway.
-- When a connection comes from one of these IPs, OpenClaw uses `x-forwarded-for` (or `x-real-ip`) to determine the client IP for local pairing checks and HTTP auth/local checks.
-- Only list proxies you fully control, and ensure they **overwrite** incoming `x-forwarded-for`.
-
-Notes:
-
-- `openclaw gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
-- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
-- OpenAI Chat Completions endpoint: **disabled by default**; enable with `gateway.http.endpoints.chatCompletions.enabled: true`.
-- Precedence: `--port` > `OPENCLAW_GATEWAY_PORT` > `gateway.port` > default `18789`.
-- Gateway auth is required by default (token/password or Tailscale Serve identity). Non-loopback binds require a shared token/password.
-- The onboarding wizard generates a gateway token by default (even on loopback).
-- `gateway.remote.token` is **only** for remote CLI calls; it does not enable local gateway auth. `gateway.token` is ignored.
-
-Auth and Tailscale:
-
-- `gateway.auth.mode` sets the handshake requirements (`token` or `password`). When unset, token auth is assumed.
-- `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine).
-- When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers).
-- `gateway.auth.password` can be set here, or via `OPENCLAW_GATEWAY_PASSWORD` (recommended).
-- `gateway.auth.allowTailscale` allows Tailscale Serve identity headers
- (`tailscale-user-login`) to satisfy auth when the request arrives on loopback
- with `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host`. OpenClaw
- verifies the identity by resolving the `x-forwarded-for` address via
- `tailscale whois` before accepting it. When `true`, Serve requests do not need
- a token/password; set `false` to require explicit credentials. Defaults to
- `true` when `tailscale.mode = "serve"` and auth mode is not `password`.
-- `gateway.tailscale.mode: "serve"` uses Tailscale Serve (tailnet only, loopback bind).
-- `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth.
-- `gateway.tailscale.resetOnExit` resets Serve/Funnel config on shutdown.
-
-Remote client defaults (CLI):
-
-- `gateway.remote.url` sets the default Gateway WebSocket URL for CLI calls when `gateway.mode = "remote"`.
-- `gateway.remote.transport` selects the macOS remote transport (`ssh` default, `direct` for ws/wss). When `direct`, `gateway.remote.url` must be `ws://` or `wss://`. `ws://host` defaults to port `18789`.
-- `gateway.remote.token` supplies the token for remote calls (leave unset for no auth).
-- `gateway.remote.password` supplies the password for remote calls (leave unset for no auth).
-
-macOS app behavior:
-
-- OpenClaw.app watches `~/.openclaw/openclaw.json` and switches modes live when `gateway.mode` or `gateway.remote.url` changes.
-- If `gateway.mode` is unset but `gateway.remote.url` is set, the macOS app treats it as remote mode.
-- When you change connection mode in the macOS app, it writes `gateway.mode` (and `gateway.remote.url` + `gateway.remote.transport` in remote mode) back to the config file.
-
-```json5
-{
- gateway: {
- mode: "remote",
- remote: {
- url: "ws://gateway.tailnet:18789",
- token: "your-token",
- password: "your-password",
- },
- },
-}
-```
-
-Direct transport example (macOS app):
-
-```json5
-{
- gateway: {
- mode: "remote",
- remote: {
- transport: "direct",
- url: "wss://gateway.example.ts.net",
- token: "your-token",
- },
- },
-}
-```
-
-### `gateway.reload` (Config hot reload)
-
-The Gateway watches `~/.openclaw/openclaw.json` (or `OPENCLAW_CONFIG_PATH`) and applies changes automatically.
-
-Modes:
-
-- `hybrid` (default): hot-apply safe changes; restart the Gateway for critical changes.
-- `hot`: only apply hot-safe changes; log when a restart is required.
-- `restart`: restart the Gateway on any config change.
-- `off`: disable hot reload.
-
-```json5
-{
- gateway: {
- reload: {
- mode: "hybrid",
- debounceMs: 300,
- },
- },
-}
-```
-
-#### Hot reload matrix (files + impact)
-
-Files watched:
-
-- `~/.openclaw/openclaw.json` (or `OPENCLAW_CONFIG_PATH`)
-
-Hot-applied (no full gateway restart):
-
-- `hooks` (webhook auth/path/mappings) + `hooks.gmail` (Gmail watcher restarted)
-- `browser` (browser control server restart)
-- `cron` (cron service restart + concurrency update)
-- `agents.defaults.heartbeat` (heartbeat runner restart)
-- `web` (WhatsApp web channel restart)
-- `telegram`, `discord`, `signal`, `imessage` (channel restarts)
-- `agent`, `models`, `routing`, `messages`, `session`, `whatsapp`, `logging`, `skills`, `ui`, `talk`, `identity`, `wizard` (dynamic reads)
-
-Requires full Gateway restart:
-
-- `gateway` (port/bind/auth/control UI/tailscale)
-- `bridge` (legacy)
-- `discovery`
-- `canvasHost`
-- `plugins`
-- Any unknown/unsupported config path (defaults to restart for safety)
-
-### Multi-instance isolation
-
-To run multiple gateways on one host (for redundancy or a rescue bot), isolate per-instance state + config and use unique ports:
-
-- `OPENCLAW_CONFIG_PATH` (per-instance config)
-- `OPENCLAW_STATE_DIR` (sessions/creds)
-- `agents.defaults.workspace` (memories)
-- `gateway.port` (unique per instance)
-
-Convenience flags (CLI):
-
-- `openclaw --dev …` → uses `~/.openclaw-dev` + shifts ports from base `19001`
-- `openclaw --profile …` → uses `~/.openclaw-` (port via config/env/flags)
-
-See [Gateway runbook](/gateway) for the derived port mapping (gateway/browser/canvas).
-See [Multiple gateways](/gateway/multiple-gateways) for browser/CDP port isolation details.
-
-Example:
-
-```bash
-OPENCLAW_CONFIG_PATH=~/.openclaw/a.json \
-OPENCLAW_STATE_DIR=~/.openclaw-a \
-openclaw gateway --port 19001
-```
-
-### `hooks` (Gateway webhooks)
-
-Enable a simple HTTP webhook endpoint on the Gateway HTTP server.
-
-Defaults:
-
-- enabled: `false`
-- path: `/hooks`
-- maxBodyBytes: `262144` (256 KB)
-
-```json5
-{
- hooks: {
- enabled: true,
- token: "shared-secret",
- path: "/hooks",
- // Optional: restrict explicit `agentId` routing.
- // Omit or include "*" to allow any agent.
- // Set [] to deny all explicit `agentId` routing.
- allowedAgentIds: ["hooks", "main"],
- presets: ["gmail"],
- transformsDir: "~/.openclaw/hooks",
- mappings: [
- {
- match: { path: "gmail" },
- action: "agent",
- agentId: "hooks",
- wakeMode: "now",
- name: "Gmail",
- sessionKey: "hook:gmail:{{messages[0].id}}",
- messageTemplate: "From: {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}",
- deliver: true,
- channel: "last",
- model: "openai/gpt-5.2-mini",
- },
- ],
- },
-}
-```
-
-Requests must include the hook token:
-
-- `Authorization: Bearer ` **or**
-- `x-openclaw-token: `
-
-Endpoints:
-
-- `POST /hooks/wake` → `{ text, mode?: "now"|"next-heartbeat" }`
-- `POST /hooks/agent` → `{ message, name?, agentId?, sessionKey?, wakeMode?, deliver?, channel?, to?, model?, thinking?, timeoutSeconds? }`
-- `POST /hooks/` → resolved via `hooks.mappings`
-
-`/hooks/agent` always posts a summary into the main session (and can optionally trigger an immediate heartbeat via `wakeMode: "now"`).
-
-Mapping notes:
-
-- `match.path` matches the sub-path after `/hooks` (e.g. `/hooks/gmail` → `gmail`).
-- `match.source` matches a payload field (e.g. `{ source: "gmail" }`) so you can use a generic `/hooks/ingest` path.
-- Templates like `{{messages[0].subject}}` read from the payload.
-- `transform` can point to a JS/TS module that returns a hook action.
-- `agentId` can route to a specific agent; unknown IDs fall back to the default agent.
-- `hooks.allowedAgentIds` restricts explicit `agentId` routing (`*` or omitted means allow all, `[]` denies all explicit routing).
-- `deliver: true` sends the final reply to a channel; `channel` defaults to `last` (falls back to WhatsApp).
-- If there is no prior delivery route, set `channel` + `to` explicitly (required for Telegram/Discord/Google Chat/Slack/Signal/iMessage/MS Teams).
-- `model` overrides the LLM for this hook run (`provider/model` or alias; must be allowed if `agents.defaults.models` is set).
-
-Gmail helper config (used by `openclaw webhooks gmail setup` / `run`):
-
-```json5
-{
- hooks: {
- gmail: {
- account: "openclaw@gmail.com",
- topic: "projects//topics/gog-gmail-watch",
- subscription: "gog-gmail-watch-push",
- pushToken: "shared-push-token",
- hookUrl: "http://127.0.0.1:18789/hooks/gmail",
- includeBody: true,
- maxBytes: 20000,
- renewEveryMinutes: 720,
- serve: { bind: "127.0.0.1", port: 8788, path: "/" },
- tailscale: { mode: "funnel", path: "/gmail-pubsub" },
-
- // Optional: use a cheaper model for Gmail hook processing
- // Falls back to agents.defaults.model.fallbacks, then primary, on auth/rate-limit/timeout
- model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
- // Optional: default thinking level for Gmail hooks
- thinking: "off",
- },
- },
-}
-```
-
-Model override for Gmail hooks:
-
-- `hooks.gmail.model` specifies a model to use for Gmail hook processing (defaults to session primary).
-- Accepts `provider/model` refs or aliases from `agents.defaults.models`.
-- Falls back to `agents.defaults.model.fallbacks`, then `agents.defaults.model.primary`, on auth/rate-limit/timeouts.
-- If `agents.defaults.models` is set, include the hooks model in the allowlist.
-- At startup, warns if the configured model is not in the model catalog or allowlist.
-- `hooks.gmail.thinking` sets the default thinking level for Gmail hooks and is overridden by per-hook `thinking`.
-
-Gateway auto-start:
-
-- If `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts
- `gog gmail watch serve` on boot and auto-renews the watch.
-- Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to disable the auto-start (for manual runs).
-- Avoid running a separate `gog gmail watch serve` alongside the Gateway; it will
- fail with `listen tcp 127.0.0.1:8788: bind: address already in use`.
-
-Note: when `tailscale.mode` is on, OpenClaw defaults `serve.path` to `/` so
-Tailscale can proxy `/gmail-pubsub` correctly (it strips the set-path prefix).
-If you need the backend to receive the prefixed path, set
-`hooks.gmail.tailscale.target` to a full URL (and align `serve.path`).
-
-### `canvasHost` (LAN/tailnet Canvas file server + live reload)
-
-The Gateway serves a directory of HTML/CSS/JS over HTTP so iOS/Android nodes can simply `canvas.navigate` to it.
-
-Default root: `~/.openclaw/workspace/canvas`
-Default port: `18793` (chosen to avoid the openclaw browser CDP port `18792`)
-The server listens on the **gateway bind host** (LAN or Tailnet) so nodes can reach it.
-
-The server:
-
-- serves files under `canvasHost.root`
-- injects a tiny live-reload client into served HTML
-- watches the directory and broadcasts reloads over a WebSocket endpoint at `/__openclaw__/ws`
-- auto-creates a starter `index.html` when the directory is empty (so you see something immediately)
-- also serves A2UI at `/__openclaw__/a2ui/` and is advertised to nodes as `canvasHostUrl`
- (always used by nodes for Canvas/A2UI)
-
-Disable live reload (and file watching) if the directory is large or you hit `EMFILE`:
-
-- config: `canvasHost: { liveReload: false }`
-
-```json5
-{
- canvasHost: {
- root: "~/.openclaw/workspace/canvas",
- port: 18793,
- liveReload: true,
- },
-}
-```
-
-Changes to `canvasHost.*` require a gateway restart (config reload will restart).
-
-Disable with:
-
-- config: `canvasHost: { enabled: false }`
-- env: `OPENCLAW_SKIP_CANVAS_HOST=1`
-
-### `bridge` (legacy TCP bridge, removed)
-
-Current builds no longer include the TCP bridge listener; `bridge.*` config keys are ignored.
-Nodes connect over the Gateway WebSocket. This section is kept for historical reference.
-
-Legacy behavior:
-
-- The Gateway could expose a simple TCP bridge for nodes (iOS/Android), typically on port `18790`.
-
-Defaults:
-
-- enabled: `true`
-- port: `18790`
-- bind: `lan` (binds to `0.0.0.0`)
-
-Bind modes:
-
-- `lan`: `0.0.0.0` (reachable on any interface, including LAN/Wi‑Fi and Tailscale)
-- `tailnet`: bind only to the machine’s Tailscale IP (recommended for Vienna ⇄ London)
-- `loopback`: `127.0.0.1` (local only)
-- `auto`: prefer tailnet IP if present, else `lan`
-
-TLS:
-
-- `bridge.tls.enabled`: enable TLS for bridge connections (TLS-only when enabled).
-- `bridge.tls.autoGenerate`: generate a self-signed cert when no cert/key are present (default: true).
-- `bridge.tls.certPath` / `bridge.tls.keyPath`: PEM paths for the bridge certificate + private key.
-- `bridge.tls.caPath`: optional PEM CA bundle (custom roots or future mTLS).
-
-When TLS is enabled, the Gateway advertises `bridgeTls=1` and `bridgeTlsSha256` in discovery TXT
-records so nodes can pin the certificate. Manual connections use trust-on-first-use if no
-fingerprint is stored yet.
-Auto-generated certs require `openssl` on PATH; if generation fails, the bridge will not start.
-
-```json5
-{
- bridge: {
- enabled: true,
- port: 18790,
- bind: "tailnet",
- tls: {
- enabled: true,
- // Uses ~/.openclaw/bridge/tls/bridge-{cert,key}.pem when omitted.
- // certPath: "~/.openclaw/bridge/tls/bridge-cert.pem",
- // keyPath: "~/.openclaw/bridge/tls/bridge-key.pem"
- },
- },
-}
-```
-
-### `discovery.mdns` (Bonjour / mDNS broadcast mode)
-
-Controls LAN mDNS discovery broadcasts (`_openclaw-gw._tcp`).
-
-- `minimal` (default): omit `cliPath` + `sshPort` from TXT records
-- `full`: include `cliPath` + `sshPort` in TXT records
-- `off`: disable mDNS broadcasts entirely
-- Hostname: defaults to `openclaw` (advertises `openclaw.local`). Override with `OPENCLAW_MDNS_HOSTNAME`.
-
-```json5
-{
- discovery: { mdns: { mode: "minimal" } },
-}
-```
-
-### `discovery.wideArea` (Wide-Area Bonjour / unicast DNS‑SD)
-
-When enabled, the Gateway writes a unicast DNS-SD zone for `_openclaw-gw._tcp` under `~/.openclaw/dns/` using the configured discovery domain (example: `openclaw.internal.`).
-
-To make iOS/Android discover across networks (Vienna ⇄ London), pair this with:
-
-- a DNS server on the gateway host serving your chosen domain (CoreDNS is recommended)
-- Tailscale **split DNS** so clients resolve that domain via the gateway DNS server
-
-One-time setup helper (gateway host):
-
-```bash
-openclaw dns setup --apply
-```
+
+ Reference env vars in any config string value with `${VAR_NAME}`:
```json5
{
- discovery: { wideArea: { enabled: true } },
+ gateway: { auth: { token: "${OPENCLAW_GATEWAY_TOKEN}" } },
+ models: { providers: { custom: { apiKey: "${CUSTOM_API_KEY}" } } },
}
```
-## Media model template variables
-
-Template placeholders are expanded in `tools.media.*.models[].args` and `tools.media.models[].args` (and any future templated argument fields).
+Rules:
-| Variable | Description |
-| ------------------ | ------------------------------------------------------------------------------- | -------- | ------- | ---------- | ----- | ------ | -------- | ------- | ------- | --- |
-| `{{Body}}` | Full inbound message body |
-| `{{RawBody}}` | Raw inbound message body (no history/sender wrappers; best for command parsing) |
-| `{{BodyStripped}}` | Body with group mentions stripped (best default for agents) |
-| `{{From}}` | Sender identifier (E.164 for WhatsApp; may differ per channel) |
-| `{{To}}` | Destination identifier |
-| `{{MessageSid}}` | Channel message id (when available) |
-| `{{SessionId}}` | Current session UUID |
-| `{{IsNewSession}}` | `"true"` when a new session was created |
-| `{{MediaUrl}}` | Inbound media pseudo-URL (if present) |
-| `{{MediaPath}}` | Local media path (if downloaded) |
-| `{{MediaType}}` | Media type (image/audio/document/…) |
-| `{{Transcript}}` | Audio transcript (when enabled) |
-| `{{Prompt}}` | Resolved media prompt for CLI entries |
-| `{{MaxChars}}` | Resolved max output chars for CLI entries |
-| `{{ChatType}}` | `"direct"` or `"group"` |
-| `{{GroupSubject}}` | Group subject (best effort) |
-| `{{GroupMembers}}` | Group members preview (best effort) |
-| `{{SenderName}}` | Sender display name (best effort) |
-| `{{SenderE164}}` | Sender phone number (best effort) |
-| `{{Provider}}` | Provider hint (whatsapp | telegram | discord | googlechat | slack | signal | imessage | msteams | webchat | …) |
+- Only uppercase names matched: `[A-Z_][A-Z0-9_]*`
+- Missing/empty vars throw an error at load time
+- Escape with `$${VAR}` for literal output
+- Works inside `$include` files
+- Inline substitution: `"${BASE}/v1"` → `"https://api.example.com/v1"`
-## Cron (Gateway scheduler)
+
-Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron jobs](/automation/cron-jobs) for the feature overview and CLI examples.
-
-```json5
-{
- cron: {
- enabled: true,
- maxConcurrentRuns: 2,
- sessionRetention: "24h",
- },
-}
-```
+See [Environment](/help/environment) for full precedence and sources.
-Fields:
+## Full reference
-- `sessionRetention`: how long to keep completed cron run sessions before pruning. Accepts a duration string like `"24h"` or `"7d"`. Use `false` to disable pruning. Default is 24h.
+For the complete field-by-field reference, see **[Configuration Reference](/gateway/configuration-reference)**.
---
-_Next: [Agent Runtime](/concepts/agent)_ 🦞
+_Related: [Configuration Examples](/gateway/configuration-examples) · [Configuration Reference](/gateway/configuration-reference) · [Doctor](/gateway/doctor)_
diff --git a/docs/gateway/index.md b/docs/gateway/index.md
index 64697f1f461..c1e06d63457 100644
--- a/docs/gateway/index.md
+++ b/docs/gateway/index.md
@@ -5,120 +5,173 @@ read_when:
title: "Gateway Runbook"
---
-# Gateway service runbook
+# Gateway runbook
-Last updated: 2025-12-09
+Use this page for day-1 startup and day-2 operations of the Gateway service.
-## What it is
+
+
+ Symptom-first diagnostics with exact command ladders and log signatures.
+
+
+ Task-oriented setup guide + full configuration reference.
+
+
-- The always-on process that owns the single Baileys/Telegram connection and the control/event plane.
-- Replaces the legacy `gateway` command. CLI entry point: `openclaw gateway`.
-- Runs until stopped; exits non-zero on fatal errors so the supervisor restarts it.
+## 5-minute local startup
-## How to run (local)
+
+
```bash
openclaw gateway --port 18789
-# for full debug/trace logs in stdio:
+# debug/trace mirrored to stdio
openclaw gateway --port 18789 --verbose
-# if the port is busy, terminate listeners then start:
+# force-kill listener on selected port, then start
openclaw gateway --force
-# dev loop (auto-reload on TS changes):
-pnpm gateway:watch
```
-- Config hot reload watches `~/.openclaw/openclaw.json` (or `OPENCLAW_CONFIG_PATH`).
- - Default mode: `gateway.reload.mode="hybrid"` (hot-apply safe changes, restart on critical).
- - Hot reload uses in-process restart via **SIGUSR1** when needed.
- - Disable with `gateway.reload.mode="off"`.
-- Binds WebSocket control plane to `127.0.0.1:` (default 18789).
-- The same port also serves HTTP (control UI, hooks, A2UI). Single-port multiplex.
- - OpenAI Chat Completions (HTTP): [`/v1/chat/completions`](/gateway/openai-http-api).
- - OpenResponses (HTTP): [`/v1/responses`](/gateway/openresponses-http-api).
- - Tools Invoke (HTTP): [`/tools/invoke`](/gateway/tools-invoke-http-api).
-- Starts a Canvas file server by default on `canvasHost.port` (default `18793`), serving `http://:18793/__openclaw__/canvas/` from `~/.openclaw/workspace/canvas`. Disable with `canvasHost.enabled=false` or `OPENCLAW_SKIP_CANVAS_HOST=1`.
-- Logs to stdout; use launchd/systemd to keep it alive and rotate logs.
-- Pass `--verbose` to mirror debug logging (handshakes, req/res, events) from the log file into stdio when troubleshooting.
-- `--force` uses `lsof` to find listeners on the chosen port, sends SIGTERM, logs what it killed, then starts the gateway (fails fast if `lsof` is missing).
-- If you run under a supervisor (launchd/systemd/mac app child-process mode), a stop/restart typically sends **SIGTERM**; older builds may surface this as `pnpm` `ELIFECYCLE` exit code **143** (SIGTERM), which is a normal shutdown, not a crash.
-- **SIGUSR1** triggers an in-process restart when authorized (gateway tool/config apply/update, or enable `commands.restart` for manual restarts).
-- Gateway auth is required by default: set `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`) or `gateway.auth.password`. Clients must send `connect.params.auth.token/password` unless using Tailscale Serve identity.
-- The wizard now generates a token by default, even on loopback.
-- Port precedence: `--port` > `OPENCLAW_GATEWAY_PORT` > `gateway.port` > default `18789`.
+
+
+
+
+```bash
+openclaw gateway status
+openclaw status
+openclaw logs --follow
+```
+
+Healthy baseline: `Runtime: running` and `RPC probe: ok`.
+
+
+
+
+
+```bash
+openclaw channels status --probe
+```
+
+
+
+
+
+Gateway config reload watches the active config file path (resolved from profile/state defaults, or `OPENCLAW_CONFIG_PATH` when set).
+Default mode is `gateway.reload.mode="hybrid"`.
+
+
+## Runtime model
+
+- One always-on process for routing, control plane, and channel connections.
+- Single multiplexed port for:
+ - WebSocket control/RPC
+ - HTTP APIs (OpenAI-compatible, Responses, tools invoke)
+ - Control UI and hooks
+- Default bind mode: `loopback`.
+- Auth is required by default (`gateway.auth.token` / `gateway.auth.password`, or `OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`).
+
+### Port and bind precedence
+
+| Setting | Resolution order |
+| ------------ | ------------------------------------------------------------- |
+| Gateway port | `--port` → `OPENCLAW_GATEWAY_PORT` → `gateway.port` → `18789` |
+| Bind mode | CLI/override → `gateway.bind` → `loopback` |
+
+### Hot reload modes
+
+| `gateway.reload.mode` | Behavior |
+| --------------------- | ------------------------------------------ |
+| `off` | No config reload |
+| `hot` | Apply only hot-safe changes |
+| `restart` | Restart on reload-required changes |
+| `hybrid` (default) | Hot-apply when safe, restart when required |
+
+## Operator command set
+
+```bash
+openclaw gateway status
+openclaw gateway status --deep
+openclaw gateway status --json
+openclaw gateway install
+openclaw gateway restart
+openclaw gateway stop
+openclaw logs --follow
+openclaw doctor
+```
## Remote access
-- Tailscale/VPN preferred; otherwise SSH tunnel:
-
- ```bash
- ssh -N -L 18789:127.0.0.1:18789 user@host
- ```
-
-- Clients then connect to `ws://127.0.0.1:18789` through the tunnel.
-- If a token is configured, clients must include it in `connect.params.auth.token` even over the tunnel.
-
-## Multiple gateways (same host)
-
-Usually unnecessary: one Gateway can serve multiple messaging channels and agents. Use multiple Gateways only for redundancy or strict isolation (ex: rescue bot).
-
-Supported if you isolate state + config and use unique ports. Full guide: [Multiple gateways](/gateway/multiple-gateways).
-
-Service names are profile-aware:
-
-- macOS: `bot.molt.` (legacy `com.openclaw.*` may still exist)
-- Linux: `openclaw-gateway-.service`
-- Windows: `OpenClaw Gateway ()`
-
-Install metadata is embedded in the service config:
-
-- `OPENCLAW_SERVICE_MARKER=openclaw`
-- `OPENCLAW_SERVICE_KIND=gateway`
-- `OPENCLAW_SERVICE_VERSION=`
-
-Rescue-Bot Pattern: keep a second Gateway isolated with its own profile, state dir, workspace, and base port spacing. Full guide: [Rescue-bot guide](/gateway/multiple-gateways#rescue-bot-guide).
-
-### Dev profile (`--dev`)
-
-Fast path: run a fully-isolated dev instance (config/state/workspace) without touching your primary setup.
+Preferred: Tailscale/VPN.
+Fallback: SSH tunnel.
```bash
-openclaw --dev setup
-openclaw --dev gateway --allow-unconfigured
-# then target the dev instance:
-openclaw --dev status
-openclaw --dev health
+ssh -N -L 18789:127.0.0.1:18789 user@host
```
-Defaults (can be overridden via env/flags/config):
+Then connect clients to `ws://127.0.0.1:18789` locally.
-- `OPENCLAW_STATE_DIR=~/.openclaw-dev`
-- `OPENCLAW_CONFIG_PATH=~/.openclaw-dev/openclaw.json`
-- `OPENCLAW_GATEWAY_PORT=19001` (Gateway WS + HTTP)
-- browser control service port = `19003` (derived: `gateway.port+2`, loopback only)
-- `canvasHost.port=19005` (derived: `gateway.port+4`)
-- `agents.defaults.workspace` default becomes `~/.openclaw/workspace-dev` when you run `setup`/`onboard` under `--dev`.
+
+If gateway auth is configured, clients still must send auth (`token`/`password`) even over SSH tunnels.
+
-Derived ports (rules of thumb):
+See: [Remote Gateway](/gateway/remote), [Authentication](/gateway/authentication), [Tailscale](/gateway/tailscale).
-- Base port = `gateway.port` (or `OPENCLAW_GATEWAY_PORT` / `--port`)
-- browser control service port = base + 2 (loopback only)
-- `canvasHost.port = base + 4` (or `OPENCLAW_CANVAS_HOST_PORT` / config override)
-- Browser profile CDP ports auto-allocate from `browser.controlPort + 9 .. + 108` (persisted per profile).
+## Supervision and service lifecycle
+
+Use supervised runs for production-like reliability.
+
+
+
+
+```bash
+openclaw gateway install
+openclaw gateway status
+openclaw gateway restart
+openclaw gateway stop
+```
+
+LaunchAgent labels are `ai.openclaw.gateway` (default) or `ai.openclaw.` (named profile). `openclaw doctor` audits and repairs service config drift.
+
+
+
+
+
+```bash
+openclaw gateway install
+systemctl --user enable --now openclaw-gateway[-].service
+openclaw gateway status
+```
+
+For persistence after logout, enable lingering:
+
+```bash
+sudo loginctl enable-linger
+```
+
+
+
+
+
+Use a system unit for multi-user/always-on hosts.
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl enable --now openclaw-gateway[-].service
+```
+
+
+
+
+## Multiple gateways on one host
+
+Most setups should run **one** Gateway.
+Use multiple only for strict isolation/redundancy (for example a rescue profile).
Checklist per instance:
-- unique `gateway.port`
-- unique `OPENCLAW_CONFIG_PATH`
-- unique `OPENCLAW_STATE_DIR`
-- unique `agents.defaults.workspace`
-- separate WhatsApp numbers (if using WA)
-
-Service install per profile:
-
-```bash
-openclaw --profile main gateway install
-openclaw --profile rescue gateway install
-```
+- Unique `gateway.port`
+- Unique `OPENCLAW_CONFIG_PATH`
+- Unique `OPENCLAW_STATE_DIR`
+- Unique `agents.defaults.workspace`
Example:
@@ -127,204 +180,75 @@ OPENCLAW_CONFIG_PATH=~/.openclaw/a.json OPENCLAW_STATE_DIR=~/.openclaw-a opencla
OPENCLAW_CONFIG_PATH=~/.openclaw/b.json OPENCLAW_STATE_DIR=~/.openclaw-b openclaw gateway --port 19002
```
-## Protocol (operator view)
+See: [Multiple gateways](/gateway/multiple-gateways).
-- Full docs: [Gateway protocol](/gateway/protocol) and [Bridge protocol (legacy)](/gateway/bridge-protocol).
-- Mandatory first frame from client: `req {type:"req", id, method:"connect", params:{minProtocol,maxProtocol,client:{id,displayName?,version,platform,deviceFamily?,modelIdentifier?,mode,instanceId?}, caps, auth?, locale?, userAgent? } }`.
-- Gateway replies `res {type:"res", id, ok:true, payload:hello-ok }` (or `ok:false` with an error, then closes).
-- After handshake:
- - Requests: `{type:"req", id, method, params}` → `{type:"res", id, ok, payload|error}`
- - Events: `{type:"event", event, payload, seq?, stateVersion?}`
-- Structured presence entries: `{host, ip, version, platform?, deviceFamily?, modelIdentifier?, mode, lastInputSeconds?, ts, reason?, tags?[], instanceId? }` (for WS clients, `instanceId` comes from `connect.client.instanceId`).
-- `agent` responses are two-stage: first `res` ack `{runId,status:"accepted"}`, then a final `res` `{runId,status:"ok"|"error",summary}` after the run finishes; streamed output arrives as `event:"agent"`.
-
-## Methods (initial set)
-
-- `health` — full health snapshot (same shape as `openclaw health --json`).
-- `status` — short summary.
-- `system-presence` — current presence list.
-- `system-event` — post a presence/system note (structured).
-- `send` — send a message via the active channel(s).
-- `agent` — run an agent turn (streams events back on same connection).
-- `node.list` — list paired + currently-connected nodes (includes `caps`, `deviceFamily`, `modelIdentifier`, `paired`, `connected`, and advertised `commands`).
-- `node.describe` — describe a node (capabilities + supported `node.invoke` commands; works for paired nodes and for currently-connected unpaired nodes).
-- `node.invoke` — invoke a command on a node (e.g. `canvas.*`, `camera.*`).
-- `node.pair.*` — pairing lifecycle (`request`, `list`, `approve`, `reject`, `verify`).
-
-See also: [Presence](/concepts/presence) for how presence is produced/deduped and why a stable `client.instanceId` matters.
-
-## Events
-
-- `agent` — streamed tool/output events from the agent run (seq-tagged).
-- `presence` — presence updates (deltas with stateVersion) pushed to all connected clients.
-- `tick` — periodic keepalive/no-op to confirm liveness.
-- `shutdown` — Gateway is exiting; payload includes `reason` and optional `restartExpectedMs`. Clients should reconnect.
-
-## WebChat integration
-
-- WebChat is a native SwiftUI UI that talks directly to the Gateway WebSocket for history, sends, abort, and events.
-- Remote use goes through the same SSH/Tailscale tunnel; if a gateway token is configured, the client includes it during `connect`.
-- macOS app connects via a single WS (shared connection); it hydrates presence from the initial snapshot and listens for `presence` events to update the UI.
-
-## Typing and validation
-
-- Server validates every inbound frame with AJV against JSON Schema emitted from the protocol definitions.
-- Clients (TS/Swift) consume generated types (TS directly; Swift via the repo’s generator).
-- Protocol definitions are the source of truth; regenerate schema/models with:
- - `pnpm protocol:gen`
- - `pnpm protocol:gen:swift`
-
-## Connection snapshot
-
-- `hello-ok` includes a `snapshot` with `presence`, `health`, `stateVersion`, and `uptimeMs` plus `policy {maxPayload,maxBufferedBytes,tickIntervalMs}` so clients can render immediately without extra requests.
-- `health`/`system-presence` remain available for manual refresh, but are not required at connect time.
-
-## Error codes (res.error shape)
-
-- Errors use `{ code, message, details?, retryable?, retryAfterMs? }`.
-- Standard codes:
- - `NOT_LINKED` — WhatsApp not authenticated.
- - `AGENT_TIMEOUT` — agent did not respond within the configured deadline.
- - `INVALID_REQUEST` — schema/param validation failed.
- - `UNAVAILABLE` — Gateway is shutting down or a dependency is unavailable.
-
-## Keepalive behavior
-
-- `tick` events (or WS ping/pong) are emitted periodically so clients know the Gateway is alive even when no traffic occurs.
-- Send/agent acknowledgements remain separate responses; do not overload ticks for sends.
-
-## Replay / gaps
-
-- Events are not replayed. Clients detect seq gaps and should refresh (`health` + `system-presence`) before continuing. WebChat and macOS clients now auto-refresh on gap.
-
-## Supervision (macOS example)
-
-- Use launchd to keep the service alive:
- - Program: path to `openclaw`
- - Arguments: `gateway`
- - KeepAlive: true
- - StandardOut/Err: file paths or `syslog`
-- On failure, launchd restarts; fatal misconfig should keep exiting so the operator notices.
-- LaunchAgents are per-user and require a logged-in session; for headless setups use a custom LaunchDaemon (not shipped).
- - `openclaw gateway install` writes `~/Library/LaunchAgents/bot.molt.gateway.plist`
- (or `bot.molt..plist`; legacy `com.openclaw.*` is cleaned up).
- - `openclaw doctor` audits the LaunchAgent config and can update it to current defaults.
-
-## Gateway service management (CLI)
-
-Use the Gateway CLI for install/start/stop/restart/status:
+### Dev profile quick path
```bash
-openclaw gateway status
-openclaw gateway install
-openclaw gateway stop
-openclaw gateway restart
-openclaw logs --follow
+openclaw --dev setup
+openclaw --dev gateway --allow-unconfigured
+openclaw --dev status
```
-Notes:
+Defaults include isolated state/config and base gateway port `19001`.
-- `gateway status` probes the Gateway RPC by default using the service’s resolved port/config (override with `--url`).
-- `gateway status --deep` adds system-level scans (LaunchDaemons/system units).
-- `gateway status --no-probe` skips the RPC probe (useful when networking is down).
-- `gateway status --json` is stable for scripts.
-- `gateway status` reports **supervisor runtime** (launchd/systemd running) separately from **RPC reachability** (WS connect + status RPC).
-- `gateway status` prints config path + probe target to avoid “localhost vs LAN bind” confusion and profile mismatches.
-- `gateway status` includes the last gateway error line when the service looks running but the port is closed.
-- `logs` tails the Gateway file log via RPC (no manual `tail`/`grep` needed).
-- If other gateway-like services are detected, the CLI warns unless they are OpenClaw profile services.
- We still recommend **one gateway per machine** for most setups; use isolated profiles/ports for redundancy or a rescue bot. See [Multiple gateways](/gateway/multiple-gateways).
- - Cleanup: `openclaw gateway uninstall` (current service) and `openclaw doctor` (legacy migrations).
-- `gateway install` is a no-op when already installed; use `openclaw gateway install --force` to reinstall (profile/env/path changes).
+## Protocol quick reference (operator view)
-Bundled mac app:
+- First client frame must be `connect`.
+- Gateway returns `hello-ok` snapshot (`presence`, `health`, `stateVersion`, `uptimeMs`, limits/policy).
+- Requests: `req(method, params)` → `res(ok/payload|error)`.
+- Common events: `connect.challenge`, `agent`, `chat`, `presence`, `tick`, `health`, `heartbeat`, `shutdown`.
-- OpenClaw.app can bundle a Node-based gateway relay and install a per-user LaunchAgent labeled
- `bot.molt.gateway` (or `bot.molt.`; legacy `com.openclaw.*` labels still unload cleanly).
-- To stop it cleanly, use `openclaw gateway stop` (or `launchctl bootout gui/$UID/bot.molt.gateway`).
-- To restart, use `openclaw gateway restart` (or `launchctl kickstart -k gui/$UID/bot.molt.gateway`).
- - `launchctl` only works if the LaunchAgent is installed; otherwise use `openclaw gateway install` first.
- - Replace the label with `bot.molt.` when running a named profile.
+Agent runs are two-stage:
-## Supervision (systemd user unit)
+1. Immediate accepted ack (`status:"accepted"`)
+2. Final completion response (`status:"ok"|"error"`), with streamed `agent` events in between.
-OpenClaw installs a **systemd user service** by default on Linux/WSL2. We
-recommend user services for single-user machines (simpler env, per-user config).
-Use a **system service** for multi-user or always-on servers (no lingering
-required, shared supervision).
-
-`openclaw gateway install` writes the user unit. `openclaw doctor` audits the
-unit and can update it to match the current recommended defaults.
-
-Create `~/.config/systemd/user/openclaw-gateway[-].service`:
-
-```
-[Unit]
-Description=OpenClaw Gateway (profile: , v)
-After=network-online.target
-Wants=network-online.target
-
-[Service]
-ExecStart=/usr/local/bin/openclaw gateway --port 18789
-Restart=always
-RestartSec=5
-Environment=OPENCLAW_GATEWAY_TOKEN=
-WorkingDirectory=/home/youruser
-
-[Install]
-WantedBy=default.target
-```
-
-Enable lingering (required so the user service survives logout/idle):
-
-```
-sudo loginctl enable-linger youruser
-```
-
-Onboarding runs this on Linux/WSL2 (may prompt for sudo; writes `/var/lib/systemd/linger`).
-Then enable the service:
-
-```
-systemctl --user enable --now openclaw-gateway[-].service
-```
-
-**Alternative (system service)** - for always-on or multi-user servers, you can
-install a systemd **system** unit instead of a user unit (no lingering needed).
-Create `/etc/systemd/system/openclaw-gateway[-].service` (copy the unit above,
-switch `WantedBy=multi-user.target`, set `User=` + `WorkingDirectory=`), then:
-
-```
-sudo systemctl daemon-reload
-sudo systemctl enable --now openclaw-gateway[-].service
-```
-
-## Windows (WSL2)
-
-Windows installs should use **WSL2** and follow the Linux systemd section above.
+See full protocol docs: [Gateway Protocol](/gateway/protocol).
## Operational checks
-- Liveness: open WS and send `req:connect` → expect `res` with `payload.type="hello-ok"` (with snapshot).
-- Readiness: call `health` → expect `ok: true` and a linked channel in `linkChannel` (when applicable).
-- Debug: subscribe to `tick` and `presence` events; ensure `status` shows linked/auth age; presence entries show Gateway host and connected clients.
+### Liveness
+
+- Open WS and send `connect`.
+- Expect `hello-ok` response with snapshot.
+
+### Readiness
+
+```bash
+openclaw gateway status
+openclaw channels status --probe
+openclaw health
+```
+
+### Gap recovery
+
+Events are not replayed. On sequence gaps, refresh state (`health`, `system-presence`) before continuing.
+
+## Common failure signatures
+
+| Signature | Likely issue |
+| -------------------------------------------------------------- | ---------------------------------------- |
+| `refusing to bind gateway ... without auth` | Non-loopback bind without token/password |
+| `another gateway instance is already listening` / `EADDRINUSE` | Port conflict |
+| `Gateway start blocked: set gateway.mode=local` | Config set to remote mode |
+| `unauthorized` during connect | Auth mismatch between client and gateway |
+
+For full diagnosis ladders, use [Gateway Troubleshooting](/gateway/troubleshooting).
## Safety guarantees
-- Assume one Gateway per host by default; if you run multiple profiles, isolate ports/state and target the right instance.
-- No fallback to direct Baileys connections; if the Gateway is down, sends fail fast.
-- Non-connect first frames or malformed JSON are rejected and the socket is closed.
-- Graceful shutdown: emit `shutdown` event before closing; clients must handle close + reconnect.
+- Gateway protocol clients fail fast when Gateway is unavailable (no implicit direct-channel fallback).
+- Invalid/non-connect first frames are rejected and closed.
+- Graceful shutdown emits `shutdown` event before socket close.
-## CLI helpers
+---
-- `openclaw gateway health|status` — request health/status over the Gateway WS.
-- `openclaw message send --target --message "hi" [--media ...]` — send via Gateway (idempotent for WhatsApp).
-- `openclaw agent --message "hi" --to ` — run an agent turn (waits for final by default).
-- `openclaw gateway call --params '{"k":"v"}'` — raw method invoker for debugging.
-- `openclaw gateway stop|restart` — stop/restart the supervised gateway service (launchd/systemd).
-- Gateway helper subcommands assume a running gateway on `--url`; they no longer auto-spawn one.
+Related:
-## Migration guidance
-
-- Retire uses of `openclaw gateway` and the legacy TCP control port.
-- Update clients to speak the WS protocol with mandatory connect and structured presence.
+- [Troubleshooting](/gateway/troubleshooting)
+- [Background Process](/gateway/background-process)
+- [Configuration](/gateway/configuration)
+- [Health](/gateway/health)
+- [Doctor](/gateway/doctor)
+- [Authentication](/gateway/authentication)
diff --git a/docs/start/getting-started.md b/docs/start/getting-started.md
index 7d852be828e..c4bed93d33f 100644
--- a/docs/start/getting-started.md
+++ b/docs/start/getting-started.md
@@ -34,6 +34,11 @@ Check your Node version with `node --version` if you are unsure.
```bash
curl -fsSL https://openclaw.ai/install.sh | bash
```
+
```powershell