> ## Documentation Index
> Fetch the complete documentation index at: https://openclaw.veiseule.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Cấu hình

# Configuration 🔧

OpenClaw reads an optional **JSON5** config from `~/.openclaw/openclaw.json` (comments + trailing commas allowed).

Nếu tệp bị thiếu, OpenClaw dùng các mặc định tương đối an toàn (agent Pi nhúng + phiên theo từng người gửi + workspace `~/.openclaw/workspace`). You usually only need a config to:

* 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`)

> **New to configuration?** Check out the [Configuration Examples](/gateway/configuration-examples) guide for complete examples with detailed explanations!

## Strict config validation

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.
Nó ghi một sentinel khởi động lại và ping phiên hoạt động gần nhất sau khi Gateway quay lại.

Cảnh báo: `config.apply` thay thế **toàn bộ cấu hình**. If you want to change only a few keys,
use `config.patch` or `openclaw config set`. Hãy giữ một bản sao lưu của `~/.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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
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": "<hash-from-config.get>",
  "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. Nó áp dụng ngữ nghĩa JSON merge patch:

* 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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
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": "<hash-from-config.get>",
  "sessionKey": "agent:main:whatsapp:dm:+15555550123",
  "restartDelayMs": 1000
}'
```

## Minimal config (recommended starting point)

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  agents: { defaults: { workspace: "~/.openclaw/workspace" } },
  channels: { whatsapp: { allowFrom: ["+15555550123"] } },
}
```

Build the default image once with:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
scripts/sandbox-setup.sh
```

## Self-chat mode (recommended for group control)

To prevent the bot from responding to WhatsApp @-mentions in groups (only respond to specific text triggers):

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  agents: {
    defaults: { workspace: "~/.openclaw/workspace" },
    list: [
      {
        id: "main",
        groupChat: { mentionPatterns: ["@openclaw", "reisponde"] },
      },
    ],
  },
  channels: {
    whatsapp: {
      // Allowlist is DMs only; including your own number enables self-chat mode.
      allowFrom: ["+15555550123"],
      groups: { "*": { requireMention: true } },
    },
  },
}
```

## Config Includes (`$include`)

Chia cấu hình của bạn thành nhiều tệp bằng directive `$include`. Các yêu cầu ghép cặp DM đang chờ được giới hạn **3 mỗi kênh** theo mặc định.

* Organizing large configs (e.g., per-client agent definitions)
* Sharing common settings across environments
* Keeping sensitive configs separate

### Basic usage

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
// ~/.openclaw/openclaw.json
{
  gateway: { port: 18789 },

  // Include a single file (replaces the key's value)
  agents: { $include: "./agents.json5" },

  // Include multiple files (deep-merged in order)
  broadcast: {
    $include: ["./clients/mueller.json5", "./clients/schmidt.json5"],
  },
}
```

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
// ~/.openclaw/agents.json5
{
  defaults: { sandbox: { mode: "all", scope: "session" } },
  list: [{ id: "main", workspace: "~/.openclaw/workspace" }],
}
```

### Merge behavior

* **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)

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
// Sibling keys override included values
{
  $include: "./base.json5", // { a: 1, b: 2 }
  b: 99, // Result: { a: 1, b: 99 }
}
```

### Nested includes

Included files can themselves contain `$include` directives (up to 10 levels deep):

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
// clients/mueller.json5
{
  agents: { $include: "./mueller/agents.json5" },
  broadcast: { $include: "./mueller/broadcast.json5" },
}
```

### Path resolution

* **Relative paths**: Resolved relative to the including file
* **Absolute paths**: Used as-is
* **Parent directories**: `../` references work as expected

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{ "$include": "./sub/config.json5" }      // relative
{ "$include": "/etc/openclaw/base.json5" } // absolute
{ "$include": "../shared/common.json5" }   // parent dir
```

### Error handling

* **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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
// ~/.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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
// ~/.openclaw/clients/mueller/agents.json5
[
  { id: "mueller-transcribe", workspace: "~/clients/mueller/transcribe" },
  { id: "mueller-docs", workspace: "~/clients/mueller/docs" },
]
```

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
// ~/.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:

* `.env` from the current working directory (if present)
* a global fallback `.env` from `~/.openclaw/.env` (aka `$OPENCLAW_STATE_DIR/.env`)

Neither `.env` file overrides existing env vars.

Bạn cũng có thể cung cấp biến môi trường inline trong cấu hình. Những biến này chỉ được áp dụng nếu
môi trường tiến trình thiếu khóa đó (cùng quy tắc không ghi đè):

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  env: {
    OPENROUTER_API_KEY: "sk-or-...",
    vars: {
      GROQ_API_KEY: "gsk-...",
    },
  },
}
```

See [/environment](/help/environment) for full precedence and sources.

### `env.shellEnv` (optional)

Tiện ích opt‑in: nếu được bật và chưa có khóa mong đợi nào được đặt, OpenClaw sẽ chạy login shell của bạn và chỉ nhập các khóa mong đợi còn thiếu (không bao giờ ghi đè).
Điều này về cơ bản là source hồ sơ shell của bạn.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  env: {
    shellEnv: {
      enabled: true,
      timeoutMs: 15000,
    },
  },
}
```

Env var equivalent:

* `OPENCLAW_LOAD_SHELL_ENV=1`
* `OPENCLAW_SHELL_ENV_TIMEOUT_MS=15000`

### Env var substitution in config

Bạn có thể tham chiếu trực tiếp các biến môi trường trong bất kỳ giá trị chuỗi cấu hình nào bằng cú pháp
`${VAR_NAME}`. Các biến được thay thế tại thời điểm tải cấu hình, trước khi xác thực.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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:

* `<agentDir>/auth-profiles.json` (default: `~/.openclaw/agents/<agentId>/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:

* `<agentDir>/auth.json` (managed automatically; don’t edit manually)

Legacy agent dir (pre multi-agent):

* `~/.openclaw/agent/*` (migrated by `openclaw doctor` into `~/.openclaw/agents/<defaultAgentId>/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`

Metadata tùy chọn cho các hồ sơ xác thực. Điều này **không** lưu trữ bí mật; nó ánh xạ
ID hồ sơ tới một nhà cung cấp + chế độ (và email tùy chọn) và xác định thứ tự
xoay vòng nhà cung cấp dùng cho failover.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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`

Danh tính theo từng agent (tùy chọn) dùng cho mặc định và UX. Được ghi bởi trợ lý onboarding trên macOS.

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` chấp nhận đường dẫn ảnh tương đối với workspace hoặc URL từ xa/data URL. Các tệp cục bộ phải nằm trong workspace của agent.

`identity.avatar` accepts:

* Workspace-relative path (must stay within the agent workspace)
* `http(s)` URL
* `data:` URI

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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

Mã ghép cặp hết hạn sau 1 giờ; bot chỉ gửi mã ghép cặp khi có yêu cầu mới được tạo. Allowlist các số điện thoại E.164 có thể kích hoạt trả lời tự động WhatsApp (**chỉ DM**).

Pairing approvals:

* `openclaw pairing list whatsapp`
* `openclaw pairing approve whatsapp <code>`

### `channels.whatsapp.allowFrom`

Xem [Messages](/concepts/messages) để biết về xếp hàng, phiên và ngữ cảnh streaming.
If empty and `channels.whatsapp.dmPolicy="pairing"`, unknown senders will receive a pairing code.
Đối với nhóm, dùng `channels.whatsapp.groupPolicy` + `channels.whatsapp.groupAllowFrom`.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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`

Kiểm soát việc các tin nhắn WhatsApp đến có được đánh dấu đã đọc hay không (dấu tick xanh). Mặc định: `true`.

Self-chat mode always skips read receipts, even when enabled.

Ghi đè theo từng tài khoản: `channels.whatsapp.accounts.<id>`.sendReadReceipts\`.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  channels: {
    whatsapp: { sendReadReceipts: false },
  },
}
```

### `channels.whatsapp.accounts` (multi-account)

Run multiple WhatsApp accounts in one gateway:

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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.
* Cài đặt kênh cơ sở (chính sách nhóm, kiểm soát đề cập, v.v.) 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). Bị bỏ qua trong chế độ tự chat WhatsApp (xem `channels.whatsapp.allowFrom`).
* **Text patterns**: Regex patterns defined in `agents.list[].groupChat.mentionPatterns`. Luôn được kiểm tra bất kể chế độ self-chat.
* Mention gating is enforced only when mention detection is possible (native mentions or at least one `mentionPattern`).

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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.<channel>.historyLimit` (hoặc `channels.<channel>.accounts.*.historyLimit` for multi-account). Đặt `0` để tắt việc gói lịch sử.

#### 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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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.<provider>.dms[userId].historyLimit`
2. Provider default: `channels.<provider>.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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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`). Khi `*.groups` được đặt, nó cũng hoạt động như một allowlist nhóm; bao gồm `"*"` để cho phép tất cả các nhóm.

To respond **only** to specific text triggers (ignoring native @-mentions):

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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.
Thông điệp đến được định tuyến tới một agent thông qua 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-<agentId>` (for `main`, falls back to `agents.defaults.workspace`).
  * `agentDir`: default `~/.openclaw/agents/<agentId>/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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  agents: {
    list: [
      {
        id: "personal",
        workspace: "~/.openclaw/workspace-personal",
        sandbox: { mode: "off" },
      },
    ],
  },
}
```

Read-only tools + read-only workspace:

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  tools: {
    agentToAgent: {
      enabled: false,
      allow: ["home", "work"],
    },
  },
}
```

### `messages.queue`

Controls how inbound messages behave when an agent run is already active.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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
    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.
* Đặt `commands.native: true|false` để ép cho tất cả, hoặc ghi đè theo kênh với `channels.discord.commands.native`, `channels.telegram.commands.native`, `channels.slack.commands.native` (bool hoặc `"auto"`). `false` sẽ xóa các lệnh đã đăng ký trước đó trên Discord/Telegram khi khởi động; lệnh Slack được quản lý trong ứng dụng Slack.
* `channels.telegram.customCommands` adds extra Telegram bot menu entries. Names are normalized; conflicts with native commands are ignored.
* `commands.bash: true` enables `! <cmd>` to run host shell commands (`/bash <cmd>` also works as an alias). Requires `tools.elevated.enabled` and allowlisting the sender in `tools.elevated.allowFrom.<channel>`.
* `commands.bashForegroundMs` controls how long bash waits before backgrounding. While a bash job is running, new `! <cmd>` requests are rejected (one at a time).
* `commands.config: true` enables `/config` (reads/writes `openclaw.json`).
* `channels.<provider>WhatsApp chạy thông qua kênh web của gateway (Baileys Web). 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.useAccessGroups: false` allows commands to bypass access-group allowlists/policies.
* Slash commands and directives are only honored for **authorized senders**. Authorization is derived from
  channel allowlists/pairing plus `commands.useAccessGroups`.

### `web` (WhatsApp web channel runtime)

Bot token được lấy từ `channels.telegram.botToken` (hoặc `channels.telegram.tokenFile`), với `TELEGRAM_BOT_TOKEN` làm phương án dự phòng cho tài khoản mặc định. It starts automatically when a linked session exists.
Set `web.enabled: false` to keep it off by default.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  web: {
    enabled: true,
    heartbeatSeconds: 60,
    reconnect: {
      initialMs: 2000,
      maxMs: 120000,
      factor: 1.4,
      jitter: 0.2,
      maxAttempts: 0,
    },
  },
}
```

### `channels.telegram` (bot transport)

OpenClaw chỉ khởi động Telegram khi tồn tại một mục cấu hình `channels.telegram`. The bot token is resolved from `channels.telegram.botToken` (or `channels.telegram.tokenFile`), with `TELEGRAM_BOT_TOKEN` as a fallback for the default account.
`/reasoning stream` stream phần lập luận vào bản nháp, sau đó gửi câu trả lời cuối cùng.
Multi-account support lives under `channels.telegram.accounts` (see the multi-account section above). Env tokens only apply to the default account.
Token môi trường chỉ áp dụng cho tài khoản mặc định.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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). Slug guild là chữ thường với khoảng trắng được thay bằng `-`; khóa kênh dùng tên kênh đã slug hóa (không có `#` ở đầu).

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 chỉ khởi động Discord khi tồn tại một mục cấu hình `channels.discord`. 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`). Bật bằng `channels.discord.allowBots` (tin nhắn của chính bot vẫn bị lọc để ngăn vòng lặp tự trả lời).
Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged channel name (no leading `#`). Văn bản gửi đi được chia khúc theo `channels.discord.textChunkLimit` (mặc định 2000).
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.<id>.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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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/<spaceId>` or `users/<userId|email>` when setting delivery targets.

### `channels.slack` (socket mode)

Slack runs in Socket Mode and requires both a bot token and app token:

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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:<id>` (DM) or `channel:<id>` 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.<id>.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 được phân phối dưới dạng plugin và không được gộp sẵn trong bản cài đặt lõi.
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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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). Biến môi trường chỉ áp dụng cho tài khoản mặc định.
Use `channel:<id>` or `user:<id>` (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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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). Không cần daemon hay cổng (port).

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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.
* Ưu tiên các mục tiêu `chat_id:<id>`. 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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
#!/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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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. Nếu không được đặt, OpenClaw sẽ cố gắng phát hiện thư mục `.git` bằng cách đi ngược lên từ workspace (và thư mục làm việc hiện tại). The path must exist to be used.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  agents: { defaults: { skipBootstrap: true } },
}
```

### `agents.defaults.bootstrapMaxChars`

Số ký tự tối đa của mỗi tệp bootstrap trong workspace được chèn vào system prompt trước khi bị cắt ngắn. Mặc định: `20000`.

When a file exceeds this limit, OpenClaw logs a warning and injects a truncated
head/tail with a marker.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  agents: { defaults: { bootstrapMaxChars: 20000 } },
}
```

### `agents.defaults.userTimezone`

Đặt múi giờ của người dùng cho **ngữ cảnh system prompt** (không áp dụng cho dấu thời gian trong phong bì thông điệp). If unset, OpenClaw uses the host timezone at runtime.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  agents: { defaults: { userTimezone: "America/Chicago" } },
}
```

### `agents.defaults.timeFormat`

Controls the **time format** shown in the system prompt’s Current Date & Time section.
Mặc định: `auto` (ưu tiên của hệ điều hành).

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  agents: { defaults: { timeFormat: "auto" } }, // auto | 12 | 24
}
```

### `messages`

Điều khiển các tiền tố inbound/outbound và phản ứng ack tùy chọn.
`params` cũng được áp dụng cho các lần chạy streaming (agent nhúng + nén).

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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.<channel>`.responsePrefix\`
* `channels.<channel>.accounts.<id>.responsePrefix`

Resolution order (most specific wins):

1. `channels.<channel>`.accounts.\<id>.responsePrefix\`
2. `channels.<channel>.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.

Nếu `messages.responsePrefix` không được đặt, sẽ không áp dụng tiền tố nào theo mặc định. Các trả lời self-chat của WhatsApp là ngoại lệ: mặc định là `[{identity.name}]` khi được đặt, nếu không thì `[openclaw]`, để các cuộc trò chuyện cùng số điện thoại dễ đọc.
Đặt thành `"auto"` để suy ra `[{identity.name}]` cho agent được định tuyến (khi được đặt).

#### 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)     |

Các biến không phân biệt chữ hoa/thường (`{MODEL}` = `{model}`). `{think}` is an alias for `{thinkingLevel}`.
Unresolved variables remain as literal text.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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). Khi sử dụng
`"[openclaw]"`, OpenClaw sẽ thay vào đó dùng `[{identity.name}]` khi agent được định tuyến có thiết lập `identity.name`.

`ackReaction` gửi một phản ứng emoji theo cơ chế best-effort để xác nhận các tin nhắn đến
trên những kênh hỗ trợ phản ứng (Slack/Discord/Telegram/Google Chat). Defaults to the
active agent’s `identity.emoji` when set, otherwise `"👀"`. 1. Đặt thành `""` để vô hiệu hóa.

`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

2. `removeAckAfterReply` sẽ xóa phản ứng xác nhận (ack) của bot sau khi gửi xong phản hồi
   (Slack/Discord/Telegram/Google Chat בלבד). 3. Mặc định: `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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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). ID giọng nói sẽ fallback về `ELEVENLABS_VOICE_ID` hoặc `SAG_VOICE_ID` khi không được thiết lập.
`apiKey` falls back to `ELEVENLABS_API_KEY` (or the gateway’s shell profile) when unset.
4\. `voiceAliases` cho phép các chỉ thị Talk dùng tên thân thiện (ví dụ: `"voice":"Clawd"`).

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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.
5\. `agents.defaults.imageModel` là tùy chọn và **chỉ được dùng nếu mô hình chính không hỗ trợ đầu vào hình ảnh**.
6\. Mỗi mục trong `agents.defaults.models` có thể bao gồm:

* `alias` (optional model shortcut, e.g. `/opus`).
* `params` (optional provider-specific API params passed through to the model request).

Nếu xác thực gateway bị vô hiệu hóa, các kết nối đó sẽ bị từ chối. Supported keys today: `temperature`, `maxTokens`. These merge with call-time options; caller-supplied values win. `temperature` là một nút điều chỉnh nâng cao—hãy để trống trừ khi bạn hiểu rõ mặc định của model và cần thay đổi.

Example:

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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/<model>"].params.thinking` yourself.

OpenClaw also ships a few built-in alias shorthands. Giá trị mặc định chỉ áp dụng khi model
đã có sẵn trong `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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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). Chúng hữu ích như một
đường dự phòng khi các nhà cung cấp API gặp sự cố. 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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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` cắt tỉa **các kết quả công cụ cũ** khỏi ngữ cảnh trong bộ nhớ ngay trước khi một yêu cầu được gửi tới 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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  agents: { defaults: { contextPruning: { mode: "adaptive" } } },
}
```

To disable:

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  agents: { defaults: { contextPruning: { mode: "aggressive" } } },
}
```

Example (adaptive tuned):

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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` chọn chiến lược tóm tắt compaction. 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`). Đặt thành `0` để tắt ngưỡng sàn.

`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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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).

* Ghi đè theo kênh: `*.blockStreaming` (và các biến thể theo tài khoản) để buộc bật/tắt block streaming.
  Các kênh không phải Telegram yêu cầu thiết lập rõ ràng `*.blockStreaming: true` để bật trả lời theo khối.

* `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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
  {
    agents: { defaults: { blockStreamingChunk: { minChars: 800, maxChars: 1200 } } },
  }
  ```

* `agents.defaults.blockStreamingCoalesce`: gộp các block được stream trước khi gửi.
  Defaults to `{ idleMs: 1000 }` and inherits `minChars` from `blockStreamingChunk`
  with `maxChars` capped to the channel text limit. Signal/Slack/Discord/Google Chat mặc định
  là `minChars: 1500` trừ khi được ghi đè.
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
  {
    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/<model>` (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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  tools: {
    profile: "messaging",
    allow: ["slack", "discord"],
  },
}
```

Example (coding profile, but deny exec/process everywhere):

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  tools: {
    profile: "coding",
    byProvider: {
      "google-antigravity": { profile: "minimal" },
    },
  },
}
```

Example (provider/model-specific allowlist):

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  tools: {
    elevated: {
      enabled: true,
      allowFrom: {
        whatsapp: ["+15555550123"],
        discord: ["steipete", "1234567890123"],
      },
    },
  },
}
```

Per-agent override (further restrict):

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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`).1) Đối với việc cài đặt gói, hãy đảm bảo có quyền egress mạng, hệ thống tệp gốc (root FS) có thể ghi, và người dùng root.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
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.

2. Lưu ý: các tệp đính kèm đến được dàn dựng vào workspace đang hoạt động tại `media/inbound/*`. 3. Với `workspaceAccess: "rw"`, điều đó có nghĩa là các tệp được ghi vào workspace của agent.

Note: `docker.binds` mounts additional host directories; global and per-agent binds are merged.

Build the optional browser image with:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
scripts/sandbox-browser-setup.sh
```

4. Khi `agents.defaults.sandbox.browser.enabled=true`, công cụ trình duyệt sử dụng một phiên bản Chromium được sandbox (CDP). 5. Nếu noVNC được bật (mặc định khi headless=false),
   URL noVNC sẽ được chèn vào system prompt để agent có thể tham chiếu đến nó.

5. Điều này không yêu cầu `browser.enabled` trong cấu hình chính; URL điều khiển sandbox được chèn theo từng phiên.

6. `agents.defaults.sandbox.browser.allowHostControl` (mặc định: false) cho phép các phiên được sandbox nhắm mục tiêu rõ ràng tới máy chủ điều khiển trình duyệt **host** thông qua công cụ trình duyệt (`target: "host"`). 8. Hãy tắt tùy chọn này nếu bạn muốn cách ly sandbox nghiêm ngặt.

Allowlists for remote control:

* `allowedControlUrls`: exact control URLs permitted for `target: "custom"`.
* `allowedControlHosts`: hostnames permitted (hostname only, no port).
* 9. `allowedControlPorts`: các cổng được phép (mặc định: http=80, https=443).
  10. Mặc định: tất cả các allowlist đều chưa được đặt (không hạn chế). 11. `allowHostControl` mặc định là false.

### `models` (custom providers + base URLs)

12. OpenClaw sử dụng danh mục mô hình **pi-coding-agent**. 13. Bạn có thể thêm các nhà cung cấp tùy chỉnh
    (LiteLLM, máy chủ tương thích OpenAI cục bộ, proxy Anthropic, v.v.) 14. bằng cách ghi
    `~/.openclaw/agents/<agentId>/agent/models.json` hoặc bằng cách định nghĩa cùng schema trong cấu hình OpenClaw của bạn dưới `models.providers`.
13. Tổng quan theo từng nhà cung cấp + ví dụ: [/concepts/model-providers](/concepts/model-providers).

When `models.providers` is present, OpenClaw writes/merges a `models.json` into
`~/.openclaw/agents/<agentId>/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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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)

16. OpenCode Zen là một cổng đa mô hình với các endpoint riêng cho từng mô hình. 17. OpenClaw sử dụng nhà cung cấp tích hợp sẵn `opencode` từ pi-ai; hãy đặt `OPENCODE_API_KEY` (hoặc
    `OPENCODE_ZEN_API_KEY`) từ [https://opencode.ai/auth](https://opencode.ai/auth).

Notes:

* Model refs use `opencode/<modelId>` (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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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

18. Các mô hình Z.AI khả dụng thông qua nhà cung cấp tích hợp sẵn `zai`. 19. Hãy đặt `ZAI_API_KEY`
    trong môi trường của bạn và tham chiếu mô hình theo provider/model.

Shortcut: `openclaw onboard --auth-choice zai-api-key`.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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".`
* 20. Endpoint API chung của Z.AI là `https://api.z.ai/api/paas/v4`. 21. Các yêu cầu
      lập trình GLM sử dụng endpoint Coding chuyên dụng `https://api.z.ai/api/coding/paas/v4`.
  21. Nhà cung cấp tích hợp sẵn `zai` sử dụng endpoint Coding. 23. Nếu bạn cần endpoint
      chung, hãy định nghĩa một nhà cung cấp tùy chỉnh trong `models.providers` với ghi đè base URL
      (xem phần nhà cung cấp tùy chỉnh ở trên).
* Use a fake placeholder in docs/configs; never commit real API keys.

### Moonshot AI (Kimi)

Use Moonshot's OpenAI-compatible endpoint:

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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

24. Xem [/gateway/local-models](/gateway/local-models) để biết hướng dẫn cục bộ hiện tại. 25. TL;DR: chạy MiniMax M2.1 thông qua LM Studio Responses API trên phần cứng mạnh; giữ các mô hình hosted được gộp để dự phòng.

### MiniMax M2.1

Use MiniMax M2.1 directly without LM Studio:

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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/<agentId>/sessions/sessions.json
    // You can override with {agentId} templating:
    store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
    // Direct chats collapse to agent:<agentId>:<mainKey> (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:

* 26. `mainKey`: khóa bucket direct-chat (mặc định: `"main"`). 27. Hữu ích khi bạn muốn “đổi tên” luồng DM chính mà không thay đổi `agentId`.
  * 28) Ghi chú sandbox: `agents.defaults.sandbox.mode: "non-main"` sử dụng khóa này để phát hiện phiên chính. 29. Bất kỳ khóa phiên nào không khớp với `mainKey` (group/channel) đều bị sandbox.
* `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"]`.
* 30. `reset`: chính sách reset chính. 31. Mặc định là reset hằng ngày lúc 4:00 sáng theo giờ địa phương trên máy chủ gateway.
  * `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.
* 34. `sendPolicy.rules[]`: khớp theo `channel`, `chatType` (`direct|group|room`), hoặc `keyPrefix` (ví dụ: `cron:`). 35. Từ chối đầu tiên sẽ thắng; nếu không thì cho phép.

### `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).
* 40. `entries.<skillKey>`: 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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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)

42. Điều khiển việc phát hiện plugin, cho phép/từ chối, và cấu hình theo từng plugin. Plugins are loaded
    from `~/.openclaw/extensions`, `<workspace>/.openclaw/extensions`, plus any
    `plugins.load.paths` entries. 44. **Thay đổi cấu hình yêu cầu khởi động lại gateway.**
    Xem [/plugin](/tools/plugin) để biết cách sử dụng đầy đủ.

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.<pluginId>`: per-plugin overrides.
  * `enabled`: set `false` to disable.
  * `config`: plugin-specific config object (validated by the plugin if provided).

Example:

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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.<name>.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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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). Mặc định: `false`. Ưu tiên HTTPS
  (Tailscale Serve) hoặc `127.0.0.1`.
* `gateway.controlUi.dangerouslyDisableDeviceAuth` disables device identity checks for the
  Control UI (token/password only). Mặc định: `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). Các bind không phải loopback yêu cầu token/mật khẩu dùng chung.
* 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` bị bỏ qua.

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. Khi `true`, các yêu cầu Serve không cần
  token/mật khẩu; đặt `false` để yêu cầu thông tin xác thực rõ ràng. Mặc định là
  `true` khi `tailscale.mode = "serve"` và chế độ xác thực không phải `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). Khi `direct`, `gateway.remote.url` phải là `ws://` hoặc `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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  gateway: {
    mode: "remote",
    remote: {
      url: "ws://gateway.tailnet:18789",
      token: "your-token",
      password: "your-password",
    },
  },
}
```

Direct transport example (macOS app):

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 <name> …` → uses `~/.openclaw-<name>` (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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  hooks: {
    enabled: true,
    token: "shared-secret",
    path: "/hooks",
    presets: ["gmail"],
    transformsDir: "~/.openclaw/hooks",
    mappings: [
      {
        match: { path: "gmail" },
        action: "agent",
        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 <token>` **or**
* `x-openclaw-token: <token>`

Endpoints:

* `POST /hooks/wake` → `{ text, mode?: "now"|"next-heartbeat" }`
* `POST /hooks/agent` → `{ message, name?, sessionKey?, wakeMode?, deliver?, channel?, to?, model?, thinking?, timeoutSeconds? 23. }` }\`
* `POST /hooks/<name>` → 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.
* `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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  hooks: {
    gmail: {
      account: "openclaw@gmail.com",
      topic: "projects/<project-id>/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).
Các bản build hiện tại không còn bao gồm listener TCP bridge; các khóa cấu hình `bridge.*` bị bỏ qua.

### `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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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. Khi TLS được bật, Gateway quảng bá `bridgeTls=1` và `bridgeTlsSha256` trong các bản ghi TXT discovery
để các node có thể ghim (pin) chứng chỉ.

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

Khi TLS được bật, Gateway quảng bá `bridgeTls=1` và `bridgeTlsSha256` trong các bản ghi TXT khám phá để các node có thể ghim (pin) chứng chỉ. Chứng chỉ được tạo tự động yêu cầu `openssl` có trong PATH; nếu tạo thất bại, bridge sẽ không khởi động.
Hostname: mặc định là `openclaw` (quảng bá `openclaw.local`).

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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
* Tên máy chủ: mặc định là `openclaw` (quảng bá `openclaw.local`). | Variable           | Description                                                                     |
  \| ------------------ | ------------------------------------------------------------------------------- | -------- | ------- | ---------- | ----- | ------ | -------- | ------- | ------- | --- |
  \| `{{Body}}`         | Toàn bộ nội dung thông điệp đến                                                  |
  \| `{{RawBody}}`      | Nội dung thô của thông điệp đến (không có wrapper lịch sử/người gửi; tốt nhất cho phân tích lệnh) |
  \| `{{BodyStripped}}` | Nội dung đã loại bỏ mention nhóm (mặc định tốt nhất cho agent)                  |
  \| `{{From}}`         | Định danh người gửi (E.164 cho WhatsApp; có thể khác theo kênh)                 |
  \| `{{To}}`           | Định danh đích                                                                  |
  \| `{{MessageSid}}`   | ID thông điệp của kênh (khi có)                                                 |
  \| `{{SessionId}}`    | UUID của phiên hiện tại                                                         |
  \| `{{IsNewSession}}` | `"true"` khi một phiên mới được tạo                                            |
  \| `{{MediaUrl}}`     | Pseudo-URL media đến (nếu có)                                                   |
  \| `{{MediaPath}}`    | Đường dẫn media cục bộ (nếu đã tải xuống)                                      |
  \| `{{MediaType}}`    | Loại media (image/audio/document/…)

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  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 theme={"theme":{"light":"min-light","dark":"min-dark"}}
openclaw dns setup --apply
```

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  discovery: { wideArea: { enabled: true } },
}
```

## Media model template variables

Template placeholders are expanded in `tools.media.*.models[].args` and `tools.media.models[].args` (and any future templated argument fields).

|
\| `{{Transcript}}`   | Bản chép lời âm thanh (khi được bật)                                            |
\| `{{Prompt}}`       | Prompt media đã được phân giải cho các mục CLI                                 |
\| `{{MaxChars}}`     | Số ký tự đầu ra tối đa đã được phân giải cho các mục CLI                        |
\| `{{ChatType}}`     | `"direct"` hoặc `"group"`                                                    |
\| `{{GroupSubject}}` | Chủ đề nhóm (cố gắng hết mức)                                                   |
\| `{{GroupMembers}}` | Xem trước thành viên nhóm (cố gắng hết mức)                                    |
\| `{{SenderName}}`   | Tên hiển thị của người gửi (cố gắng hết mức)                                   |
\| `{{SenderE164}}`   | Số điện thoại người gửi (cố gắng hết mức)                                      |
\| `{{Provider}}`     | Gợi ý nhà cung cấp (whatsapp                                                         | telegram | discord | googlechat | slack | signal | imessage | msteams | webchat | …)                                             |
\| `{{Transcript}}`   | Bản ghi âm thanh (khi được bật)                                                 |
\| `{{Prompt}}`       | Prompt media đã được phân giải cho các mục CLI                                  |
\| `{{MaxChars}}`     | Số ký tự đầu ra tối đa đã được phân giải cho các mục CLI                        |
\| `{{ChatType}}`     | `"direct"` hoặc `"group"`                                                   |
\| `{{GroupSubject}}` | Chủ đề nhóm (nỗ lực tốt nhất)                                                   |
\| `{{GroupMembers}}` | Xem trước thành viên nhóm (nỗ lực tốt nhất)                                    |
\| `{{SenderName}}`   | Tên hiển thị người gửi (nỗ lực tốt nhất)                                       |
\| `{{SenderE164}}`   | Số điện thoại người gửi (nỗ lực tốt nhất)                                      |
\| `{{Provider}}`     | Gợi ý nhà cung cấp (whatsapp                                                         | telegram | discord | googlechat | slack | signal | imessage | msteams | webchat | …)  Cron là bộ lập lịch do Gateway sở hữu cho các tác vụ đánh thức và công việc theo lịch.

## Cron (Gateway scheduler)

Cron là bộ lập lịch do Gateway sở hữu cho các lần đánh thức và các tác vụ theo lịch. **Gateway**: một tiến trình gateway chạy dài hạn duy nhất, sở hữu trạng thái (phiên, ghép cặp, sổ đăng ký node) và chạy các kênh.

```json5 theme={"theme":{"light":"min-light","dark":"min-dark"}}
{
  cron: {
    enabled: true,
    maxConcurrentRuns: 2,
  },
}
```

***

*Next: [Agent Runtime](/concepts/agent)* 🦞
