Files
openclaw/docs/gateway/configuration-reference.md
Seb Slight 3ed06c6f36 docs: modernize gateway configuration page (Phase 1) (#14111)
* docs(configuration): split into overview + full reference with Mintlify components

* docs(configuration): use tooltip for JSON5 format note

* docs(configuration): fix Accordion closing tags inside list contexts

* docs(configuration): expand intro to reflect full config surface

* docs(configuration): trim intro to three concise bullets

* docs(configuration-examples): revert all branch changes

* docs(configuration): improve hot-reload section with tabs and accordion

* docs(configuration): uncramp hot-reload — subheadings, bullet list, warning

* docs(configuration): restore hot-apply vs restart table

* docs(configuration): fix hot-reload table against codebase

* docs: add configuration-reference.md — full field-by-field reference

* docs(gateway): refresh runbook and align config reference

* docs: include pending docs updates and install graphic
2026-02-11 10:44:34 -05:00

2319 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Configuration Reference"
description: "Complete field-by-field reference for ~/.openclaw/openclaw.json"
---
# Configuration Reference
Every field available in `~/.openclaw/openclaw.json`. For a task-oriented overview, see [Configuration](/gateway/configuration).
Config format is **JSON5** (comments + trailing commas allowed). All fields are optional — OpenClaw uses safe defaults when omitted.
---
## Channels
Each channel starts automatically when its config section exists (unless `enabled: false`).
### DM and group access
All channels support DM policies and group policies:
| DM policy | Behavior |
| ------------------- | --------------------------------------------------------------- |
| `pairing` (default) | Unknown senders get a one-time pairing code; owner must approve |
| `allowlist` | Only senders in `allowFrom` (or paired allow store) |
| `open` | Allow all inbound DMs (requires `allowFrom: ["*"]`) |
| `disabled` | Ignore all inbound DMs |
| Group policy | Behavior |
| --------------------- | ------------------------------------------------------ |
| `allowlist` (default) | Only groups matching the configured allowlist |
| `open` | Bypass group allowlists (mention-gating still applies) |
| `disabled` | Block all group/room messages |
<Note>
`channels.defaults.groupPolicy` sets the default when a provider's `groupPolicy` is unset.
Pairing codes expire after 1 hour. Pending DM pairing requests are capped at **3 per channel**.
Slack/Discord have a special fallback: if their provider section is missing entirely, runtime group policy can resolve to `open` (with a startup warning).
</Note>
### WhatsApp
WhatsApp runs through the gateway's web channel (Baileys Web). It starts automatically when a linked session exists.
```json5
{
channels: {
whatsapp: {
dmPolicy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["+15555550123", "+447700900123"],
textChunkLimit: 4000,
chunkMode: "length", // length | newline
mediaMaxMb: 50,
sendReadReceipts: true, // blue ticks (false in self-chat mode)
groups: {
"*": { requireMention: true },
},
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"],
},
},
web: {
enabled: true,
heartbeatSeconds: 60,
reconnect: {
initialMs: 2000,
maxMs: 120000,
factor: 1.4,
jitter: 0.2,
maxAttempts: 0,
},
},
}
```
<Accordion title="Multi-account WhatsApp">
```json5
{
channels: {
whatsapp: {
accounts: {
default: {},
personal: {},
biz: {
// authDir: "~/.openclaw/credentials/whatsapp/biz",
},
},
},
},
}
```
- Outbound commands default to account `default` if present; otherwise the first configured account id (sorted).
- Legacy single-account Baileys auth dir is migrated by `openclaw doctor` into `whatsapp/default`.
- Per-account override: `channels.whatsapp.accounts.<id>.sendReadReceipts`.
</Accordion>
### Telegram
```json5
{
channels: {
telegram: {
enabled: true,
botToken: "your-bot-token",
dmPolicy: "pairing",
allowFrom: ["tg:123456789"],
groups: {
"*": { requireMention: true },
"-1001234567890": {
allowFrom: ["@admin"],
systemPrompt: "Keep answers brief.",
topics: {
"99": {
requireMention: false,
skills: ["search"],
systemPrompt: "Stay on topic.",
},
},
},
},
customCommands: [
{ command: "backup", description: "Git backup" },
{ command: "generate", description: "Create an image" },
],
historyLimit: 50,
replyToMode: "first", // off | first | all
linkPreview: true,
streamMode: "partial", // off | partial | block
draftChunk: {
minChars: 200,
maxChars: 800,
breakPreference: "paragraph", // paragraph | newline | sentence
},
actions: { reactions: true, sendMessage: true },
reactionNotifications: "own", // off | own | all
mediaMaxMb: 5,
retry: {
attempts: 3,
minDelayMs: 400,
maxDelayMs: 30000,
jitter: 0.1,
},
network: { autoSelectFamily: false },
proxy: "socks5://localhost:9050",
webhookUrl: "https://example.com/telegram-webhook",
webhookSecret: "secret",
webhookPath: "/telegram-webhook",
},
},
}
```
- Bot token: `channels.telegram.botToken` or `channels.telegram.tokenFile`, with `TELEGRAM_BOT_TOKEN` as fallback for the default account.
- `configWrites: false` blocks Telegram-initiated config writes (supergroup ID migrations, `/config set|unset`).
- Draft streaming uses Telegram `sendMessageDraft` (requires private chat topics).
- Retry policy: see [Retry policy](/concepts/retry).
### Discord
```json5
{
channels: {
discord: {
enabled: true,
token: "your-bot-token",
mediaMaxMb: 8,
allowBots: false,
actions: {
reactions: true,
stickers: true,
polls: true,
permissions: true,
messages: true,
threads: true,
pins: true,
search: true,
memberInfo: true,
roleInfo: true,
roles: false,
channelInfo: true,
voiceStatus: true,
events: true,
moderation: false,
},
replyToMode: "off", // off | first | all
dm: {
enabled: true,
policy: "pairing",
allowFrom: ["1234567890", "steipete"],
groupEnabled: false,
groupChannels: ["openclaw-dm"],
},
guilds: {
"123456789012345678": {
slug: "friends-of-openclaw",
requireMention: false,
reactionNotifications: "own",
users: ["987654321098765432"],
channels: {
general: { allow: true },
help: {
allow: true,
requireMention: true,
users: ["987654321098765432"],
skills: ["docs"],
systemPrompt: "Short answers only.",
},
},
},
},
historyLimit: 20,
textChunkLimit: 2000,
chunkMode: "length", // length | newline
maxLinesPerMessage: 17,
retry: {
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1,
},
},
},
}
```
- Token: `channels.discord.token`, with `DISCORD_BOT_TOKEN` as fallback for the default account.
- Use `user:<id>` (DM) or `channel:<id>` (guild channel) for delivery targets; bare numeric IDs are rejected.
- Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged name (no `#`). Prefer guild IDs.
- Bot-authored messages are ignored by default. `allowBots: true` enables them (own messages still filtered).
- `maxLinesPerMessage` (default 17) splits tall messages even when under 2000 chars.
**Reaction notification modes:** `off` (none), `own` (bot's messages, default), `all` (all messages), `allowlist` (from `guilds.<id>.users` on all messages).
### Google Chat
```json5
{
channels: {
googlechat: {
enabled: true,
serviceAccountFile: "/path/to/service-account.json",
audienceType: "app-url", // app-url | project-number
audience: "https://gateway.example.com/googlechat",
webhookPath: "/googlechat",
botUser: "users/1234567890",
dm: {
enabled: true,
policy: "pairing",
allowFrom: ["users/1234567890"],
},
groupPolicy: "allowlist",
groups: {
"spaces/AAAA": { allow: true, requireMention: true },
},
actions: { reactions: true },
typingIndicator: "message",
mediaMaxMb: 20,
},
},
}
```
- Service account JSON: inline (`serviceAccount`) or file-based (`serviceAccountFile`).
- Env fallbacks: `GOOGLE_CHAT_SERVICE_ACCOUNT` or `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE`.
- Use `spaces/<spaceId>` or `users/<userId|email>` for delivery targets.
### Slack
```json5
{
channels: {
slack: {
enabled: true,
botToken: "xoxb-...",
appToken: "xapp-...",
dm: {
enabled: true,
policy: "pairing",
allowFrom: ["U123", "U456", "*"],
groupEnabled: false,
groupChannels: ["G123"],
},
channels: {
C123: { allow: true, requireMention: true, allowBots: false },
"#general": {
allow: true,
requireMention: true,
allowBots: false,
users: ["U123"],
skills: ["docs"],
systemPrompt: "Short answers only.",
},
},
historyLimit: 50,
allowBots: false,
reactionNotifications: "own",
reactionAllowlist: ["U123"],
replyToMode: "off", // off | first | all
thread: {
historyScope: "thread", // thread | channel
inheritParent: false,
},
actions: {
reactions: true,
messages: true,
pins: true,
memberInfo: true,
emojiList: true,
},
slashCommand: {
enabled: true,
name: "openclaw",
sessionPrefix: "slack:slash",
ephemeral: true,
},
textChunkLimit: 4000,
chunkMode: "length",
mediaMaxMb: 20,
},
},
}
```
- **Socket mode** requires both `botToken` and `appToken` (`SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` for default account env fallback).
- **HTTP mode** requires `botToken` plus `signingSecret` (at root or per-account).
- `configWrites: false` blocks Slack-initiated config writes.
- Use `user:<id>` (DM) or `channel:<id>` for delivery targets.
**Reaction notification modes:** `off`, `own` (default), `all`, `allowlist` (from `reactionAllowlist`).
**Thread session isolation:** `thread.historyScope` is per-thread (default) or shared across channel. `thread.inheritParent` copies parent channel transcript to new threads.
| Action group | Default | Notes |
| ------------ | ------- | ---------------------- |
| reactions | enabled | React + list reactions |
| messages | enabled | Read/send/edit/delete |
| pins | enabled | Pin/unpin/list |
| memberInfo | enabled | Member info |
| emojiList | enabled | Custom emoji list |
### Mattermost
Mattermost ships as a plugin: `openclaw plugins install @openclaw/mattermost`.
```json5
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
chatmode: "oncall", // oncall | onmessage | onchar
oncharPrefixes: [">", "!"],
textChunkLimit: 4000,
chunkMode: "length",
},
},
}
```
Chat modes: `oncall` (respond on @-mention, default), `onmessage` (every message), `onchar` (messages starting with trigger prefix).
### Signal
```json5
{
channels: {
signal: {
reactionNotifications: "own", // off | own | all | allowlist
reactionAllowlist: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
historyLimit: 50,
},
},
}
```
**Reaction notification modes:** `off`, `own` (default), `all`, `allowlist` (from `reactionAllowlist`).
### iMessage
OpenClaw spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
```json5
{
channels: {
imessage: {
enabled: true,
cliPath: "imsg",
dbPath: "~/Library/Messages/chat.db",
remoteHost: "user@gateway-host",
dmPolicy: "pairing",
allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
historyLimit: 50,
includeAttachments: false,
mediaMaxMb: 16,
service: "auto",
region: "US",
},
},
}
```
- Requires Full Disk Access to the Messages DB.
- Prefer `chat_id:<id>` targets. Use `imsg chats --limit 20` to list chats.
- `cliPath` can point to an SSH wrapper; set `remoteHost` for SCP attachment fetching.
<Accordion title="iMessage SSH wrapper example">
```bash
#!/usr/bin/env bash
exec ssh -T gateway-host imsg "$@"
```
</Accordion>
### Multi-account (all channels)
Run multiple accounts per channel (each with its own `accountId`):
```json5
{
channels: {
telegram: {
accounts: {
default: {
name: "Primary bot",
botToken: "123456:ABC...",
},
alerts: {
name: "Alerts bot",
botToken: "987654:XYZ...",
},
},
},
},
}
```
- `default` is used when `accountId` is omitted (CLI + routing).
- Env tokens only apply to the **default** account.
- Base channel settings apply to all accounts unless overridden per account.
- Use `bindings[].match.accountId` to route each account to a different agent.
### Group chat mention gating
Group messages default to **require mention** (metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, Google Chat, and iMessage group chats.
**Mention types:**
- **Metadata mentions**: Native platform @-mentions. Ignored in WhatsApp self-chat mode.
- **Text patterns**: Regex patterns in `agents.list[].groupChat.mentionPatterns`. Always checked.
- Mention gating is enforced only when detection is possible (native mentions or at least one pattern).
```json5
{
messages: {
groupChat: { historyLimit: 50 },
},
agents: {
list: [{ id: "main", groupChat: { mentionPatterns: ["@openclaw", "openclaw"] } }],
},
}
```
`messages.groupChat.historyLimit` sets the global default. Channels can override with `channels.<channel>.historyLimit` (or per-account). Set `0` to disable.
#### DM history limits
```json5
{
channels: {
telegram: {
dmHistoryLimit: 30,
dms: {
"123456789": { historyLimit: 50 },
},
},
},
}
```
Resolution: per-DM override → provider default → no limit (all retained).
Supported: `telegram`, `whatsapp`, `discord`, `slack`, `signal`, `imessage`, `msteams`.
#### Self-chat mode
Include your own number in `allowFrom` to enable self-chat mode (ignores native @-mentions, only responds to text patterns):
```json5
{
channels: {
whatsapp: {
allowFrom: ["+15555550123"],
groups: { "*": { requireMention: true } },
},
},
agents: {
list: [
{
id: "main",
groupChat: { mentionPatterns: ["reisponde", "@openclaw"] },
},
],
},
}
```
### Commands (chat command handling)
```json5
{
commands: {
native: "auto", // register native commands when supported
text: true, // parse /commands in chat messages
bash: false, // allow ! (alias: /bash)
bashForegroundMs: 2000,
config: false, // allow /config
debug: false, // allow /debug
restart: false, // allow /restart + gateway restart tool
allowFrom: {
"*": ["user1"],
discord: ["user:123"],
},
useAccessGroups: true,
},
}
```
<Accordion title="Command details">
- Text commands must be **standalone** messages with leading `/`.
- `native: "auto"` turns on native commands for Discord/Telegram, leaves Slack off.
- Override per channel: `channels.discord.commands.native` (bool or `"auto"`). `false` clears previously registered commands.
- `channels.telegram.customCommands` adds extra Telegram bot menu entries.
- `bash: true` enables `! <cmd>` for host shell. Requires `tools.elevated.enabled` and sender in `tools.elevated.allowFrom.<channel>`.
- `config: true` enables `/config` (reads/writes `openclaw.json`).
- `channels.<provider>.configWrites` gates config mutations per channel (default: true).
- `allowFrom` is per-provider. When set, it is the **only** authorization source (channel allowlists/pairing and `useAccessGroups` are ignored).
- `useAccessGroups: false` allows commands to bypass access-group policies when `allowFrom` is not set.
</Accordion>
---
## Agent defaults
### `agents.defaults.workspace`
Default: `~/.openclaw/workspace`.
```json5
{
agents: { defaults: { workspace: "~/.openclaw/workspace" } },
}
```
### `agents.defaults.repoRoot`
Optional repository root shown in the system prompt's Runtime line. If unset, OpenClaw auto-detects by walking upward from the workspace.
```json5
{
agents: { defaults: { repoRoot: "~/Projects/openclaw" } },
}
```
### `agents.defaults.skipBootstrap`
Disables automatic creation of workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`).
```json5
{
agents: { defaults: { skipBootstrap: true } },
}
```
### `agents.defaults.bootstrapMaxChars`
Max characters per workspace bootstrap file before truncation. Default: `20000`.
```json5
{
agents: { defaults: { bootstrapMaxChars: 20000 } },
}
```
### `agents.defaults.userTimezone`
Timezone for system prompt context (not message timestamps). Falls back to host timezone.
```json5
{
agents: { defaults: { userTimezone: "America/Chicago" } },
}
```
### `agents.defaults.timeFormat`
Time format in system prompt. Default: `auto` (OS preference).
```json5
{
agents: { defaults: { timeFormat: "auto" } }, // auto | 12 | 24
}
```
### `agents.defaults.model`
```json5
{
agents: {
defaults: {
models: {
"anthropic/claude-opus-4-6": { alias: "opus" },
"minimax/MiniMax-M2.1": { alias: "minimax" },
},
model: {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["minimax/MiniMax-M2.1"],
},
imageModel: {
primary: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
fallbacks: ["openrouter/google/gemini-2.0-flash-vision:free"],
},
thinkingDefault: "low",
verboseDefault: "off",
elevatedDefault: "on",
timeoutSeconds: 600,
mediaMaxMb: 5,
contextTokens: 200000,
maxConcurrent: 3,
},
},
}
```
- `model.primary`: format `provider/model` (e.g. `anthropic/claude-opus-4-6`). If you omit the provider, OpenClaw assumes `anthropic` (deprecated).
- `models`: the configured model catalog and allowlist for `/model`. Each entry can include `alias` (shortcut) and `params` (provider-specific: `temperature`, `maxTokens`).
- `imageModel`: only used if the primary model lacks image input.
- `maxConcurrent`: max parallel agent runs across sessions (each session still serialized). Default: 1.
**Built-in alias shorthands** (only apply when the model is in `agents.defaults.models`):
| Alias | Model |
| -------------- | ------------------------------- |
| `opus` | `anthropic/claude-opus-4-6` |
| `sonnet` | `anthropic/claude-sonnet-4-5` |
| `gpt` | `openai/gpt-5.2` |
| `gpt-mini` | `openai/gpt-5-mini` |
| `gemini` | `google/gemini-3-pro-preview` |
| `gemini-flash` | `google/gemini-3-flash-preview` |
Your configured aliases always win over defaults.
Z.AI GLM-4.x models automatically enable thinking mode unless you set `--thinking off` or define `agents.defaults.models["zai/<model>"].params.thinking` yourself.
### `agents.defaults.cliBackends`
Optional CLI backends for text-only fallback runs (no tool calls). Useful as a backup when API providers fail.
```json5
{
agents: {
defaults: {
cliBackends: {
"claude-cli": {
command: "/opt/homebrew/bin/claude",
},
"my-cli": {
command: "my-cli",
args: ["--json"],
output: "json",
modelArg: "--model",
sessionArg: "--session",
sessionMode: "existing",
systemPromptArg: "--system",
systemPromptWhen: "first",
imageArg: "--image",
imageMode: "repeat",
},
},
},
},
}
```
- CLI backends are text-first; tools are always disabled.
- Sessions supported when `sessionArg` is set.
- Image pass-through supported when `imageArg` accepts file paths.
### `agents.defaults.heartbeat`
Periodic heartbeat runs.
```json5
{
agents: {
defaults: {
heartbeat: {
every: "30m", // 0m disables
model: "openai/gpt-5.2-mini",
includeReasoning: false,
session: "main",
to: "+15555550123",
target: "last", // last | whatsapp | telegram | discord | ... | none
prompt: "Read HEARTBEAT.md if it exists...",
ackMaxChars: 300,
},
},
},
}
```
- `every`: duration string (ms/s/m/h). Default: `30m`.
- Per-agent: set `agents.list[].heartbeat`. When any agent defines `heartbeat`, **only those agents** run heartbeats.
- Heartbeats run full agent turns — shorter intervals burn more tokens.
### `agents.defaults.compaction`
```json5
{
agents: {
defaults: {
compaction: {
mode: "safeguard", // default | safeguard
reserveTokensFloor: 24000,
memoryFlush: {
enabled: true,
softThresholdTokens: 6000,
systemPrompt: "Session nearing compaction. Store durable memories now.",
prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
},
},
},
},
}
```
- `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction).
- `memoryFlush`: silent agentic turn before auto-compaction to store durable memories. Skipped when workspace is read-only.
### `agents.defaults.contextPruning`
Prunes **old tool results** from in-memory context before sending to the LLM. Does **not** modify session history on disk.
```json5
{
agents: {
defaults: {
contextPruning: {
mode: "cache-ttl", // off | cache-ttl
ttl: "1h", // duration (ms/s/m/h), default unit: minutes
keepLastAssistants: 3,
softTrimRatio: 0.3,
hardClearRatio: 0.5,
minPrunableToolChars: 50000,
softTrim: { maxChars: 4000, headChars: 1500, tailChars: 1500 },
hardClear: { enabled: true, placeholder: "[Old tool result content cleared]" },
tools: { deny: ["browser", "canvas"] },
},
},
},
}
```
<Accordion title="cache-ttl mode behavior">
- `mode: "cache-ttl"` enables pruning passes.
- `ttl` controls how often pruning can run again (after the last cache touch).
- Pruning soft-trims oversized tool results first, then hard-clears older tool results if needed.
**Soft-trim** keeps beginning + end and inserts `...` in the middle.
**Hard-clear** replaces the entire tool result with the placeholder.
Notes:
- Image blocks are never trimmed/cleared.
- Ratios are character-based (approximate), not exact token counts.
- If fewer than `keepLastAssistants` assistant messages exist, pruning is skipped.
</Accordion>
See [Session Pruning](/concepts/session-pruning) for behavior details.
### Block streaming
```json5
{
agents: {
defaults: {
blockStreamingDefault: "off", // on | off
blockStreamingBreak: "text_end", // text_end | message_end
blockStreamingChunk: { minChars: 800, maxChars: 1200 },
blockStreamingCoalesce: { idleMs: 1000 },
humanDelay: { mode: "natural" }, // off | natural | custom (use minMs/maxMs)
},
},
}
```
- Non-Telegram channels require explicit `*.blockStreaming: true` to enable block replies.
- Channel overrides: `channels.<channel>.blockStreamingCoalesce` (and per-account variants). Signal/Slack/Discord/Google Chat default `minChars: 1500`.
- `humanDelay`: randomized pause between block replies. `natural` = 8002500ms. Per-agent override: `agents.list[].humanDelay`.
See [Streaming](/concepts/streaming) for behavior + chunking details.
### Typing indicators
```json5
{
agents: {
defaults: {
typingMode: "instant", // never | instant | thinking | message
typingIntervalSeconds: 6,
},
},
}
```
- Defaults: `instant` for direct chats/mentions, `message` for unmentioned group chats.
- Per-session overrides: `session.typingMode`, `session.typingIntervalSeconds`.
See [Typing Indicators](/concepts/typing-indicators).
### `agents.defaults.sandbox`
Optional **Docker sandboxing** for the embedded agent. See [Sandboxing](/gateway/sandboxing) for the full guide.
```json5
{
agents: {
defaults: {
sandbox: {
mode: "non-main", // off | non-main | all
scope: "agent", // session | agent | shared
workspaceAccess: "none", // none | ro | rw
workspaceRoot: "~/.openclaw/sandboxes",
docker: {
image: "openclaw-sandbox:bookworm-slim",
containerPrefix: "openclaw-sbx-",
workdir: "/workspace",
readOnlyRoot: true,
tmpfs: ["/tmp", "/var/tmp", "/run"],
network: "none",
user: "1000:1000",
capDrop: ["ALL"],
env: { LANG: "C.UTF-8" },
setupCommand: "apt-get update && apt-get install -y git curl jq",
pidsLimit: 256,
memory: "1g",
memorySwap: "2g",
cpus: 1,
ulimits: {
nofile: { soft: 1024, hard: 2048 },
nproc: 256,
},
seccompProfile: "/path/to/seccomp.json",
apparmorProfile: "openclaw-sandbox",
dns: ["1.1.1.1", "8.8.8.8"],
extraHosts: ["internal.service:10.0.0.5"],
binds: ["/home/user/source:/source:rw"],
},
browser: {
enabled: false,
image: "openclaw-sandbox-browser:bookworm-slim",
cdpPort: 9222,
vncPort: 5900,
noVncPort: 6080,
headless: false,
enableNoVnc: true,
allowHostControl: false,
autoStart: true,
autoStartTimeoutMs: 12000,
},
prune: {
idleHours: 24,
maxAgeDays: 7,
},
},
},
},
tools: {
sandbox: {
tools: {
allow: [
"exec",
"process",
"read",
"write",
"edit",
"apply_patch",
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
],
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"],
},
},
},
}
```
<Accordion title="Sandbox details">
**Workspace access:**
- `none`: per-scope sandbox workspace under `~/.openclaw/sandboxes`
- `ro`: sandbox workspace at `/workspace`, agent workspace mounted read-only at `/agent`
- `rw`: agent workspace mounted read/write at `/workspace`
**Scope:**
- `session`: per-session container + workspace
- `agent`: one container + workspace per agent (default)
- `shared`: shared container and workspace (no cross-session isolation)
**`setupCommand`** runs once after container creation (via `sh -lc`). Needs network egress, writable root, root user.
**Containers default to `network: "none"`** — set to `"bridge"` if the agent needs outbound access.
**Inbound attachments** are staged into `media/inbound/*` in the active workspace.
**`docker.binds`** mounts additional host directories; global and per-agent binds are merged.
**Sandboxed browser** (`sandbox.browser.enabled`): Chromium + CDP in a container. noVNC URL injected into system prompt. Does not require `browser.enabled` in main config.
- `allowHostControl: false` (default) blocks sandboxed sessions from targeting the host browser.
</Accordion>
Build images:
```bash
scripts/sandbox-setup.sh # main sandbox image
scripts/sandbox-browser-setup.sh # optional browser image
```
### `agents.list` (per-agent overrides)
```json5
{
agents: {
list: [
{
id: "main",
default: true,
name: "Main Agent",
workspace: "~/.openclaw/workspace",
agentDir: "~/.openclaw/agents/main/agent",
model: "anthropic/claude-opus-4-6", // or { primary, fallbacks }
identity: {
name: "Samantha",
theme: "helpful sloth",
emoji: "🦥",
avatar: "avatars/samantha.png",
},
groupChat: { mentionPatterns: ["@openclaw"] },
sandbox: { mode: "off" },
subagents: { allowAgents: ["*"] },
tools: {
profile: "coding",
allow: ["browser"],
deny: ["canvas"],
elevated: { enabled: true },
},
},
],
},
}
```
- `id`: stable agent id (required).
- `default`: when multiple are set, first wins (warning logged). If none set, first list entry is default.
- `model`: string form overrides `primary` only; object form `{ primary, fallbacks }` overrides both (`[]` disables global fallbacks).
- `identity.avatar`: workspace-relative path, `http(s)` URL, or `data:` URI.
- `identity` derives defaults: `ackReaction` from `emoji`, `mentionPatterns` from `name`/`emoji`.
- `subagents.allowAgents`: allowlist of agent ids for `sessions_spawn` (`["*"]` = any; default: same agent only).
---
## Multi-agent routing
Run multiple isolated agents inside one Gateway. See [Multi-Agent](/concepts/multi-agent).
```json5
{
agents: {
list: [
{ id: "home", default: true, workspace: "~/.openclaw/workspace-home" },
{ id: "work", workspace: "~/.openclaw/workspace-work" },
],
},
bindings: [
{ agentId: "home", match: { channel: "whatsapp", accountId: "personal" } },
{ agentId: "work", match: { channel: "whatsapp", accountId: "biz" } },
],
}
```
### Binding match fields
- `match.channel` (required)
- `match.accountId` (optional; `*` = any account; omitted = default account)
- `match.peer` (optional; `{ kind: direct|group|channel, id }`)
- `match.guildId` / `match.teamId` (optional; channel-specific)
**Deterministic match order:**
1. `match.peer`
2. `match.guildId`
3. `match.teamId`
4. `match.accountId` (exact, no peer/guild/team)
5. `match.accountId: "*"` (channel-wide)
6. Default agent
Within each tier, the first matching `bindings` entry wins.
### Per-agent access profiles
<Accordion title="Full access (no sandbox)">
```json5
{
agents: {
list: [
{
id: "personal",
workspace: "~/.openclaw/workspace-personal",
sandbox: { mode: "off" },
},
],
},
}
```
</Accordion>
<Accordion title="Read-only tools + workspace">
```json5
{
agents: {
list: [
{
id: "family",
workspace: "~/.openclaw/workspace-family",
sandbox: { mode: "all", scope: "agent", workspaceAccess: "ro" },
tools: {
allow: [
"read",
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
],
deny: ["write", "edit", "apply_patch", "exec", "process", "browser"],
},
},
],
},
}
```
</Accordion>
<Accordion title="No filesystem access (messaging only)">
```json5
{
agents: {
list: [
{
id: "public",
workspace: "~/.openclaw/workspace-public",
sandbox: { mode: "all", scope: "agent", workspaceAccess: "none" },
tools: {
allow: [
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
"whatsapp",
"telegram",
"slack",
"discord",
"gateway",
],
deny: [
"read",
"write",
"edit",
"apply_patch",
"exec",
"process",
"browser",
"canvas",
"nodes",
"cron",
"gateway",
"image",
],
},
},
],
},
}
```
</Accordion>
See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for precedence details.
---
## Session
```json5
{
session: {
scope: "per-sender",
dmScope: "main", // main | per-peer | per-channel-peer | per-account-channel-peer
identityLinks: {
alice: ["telegram:123456789", "discord:987654321012345678"],
},
reset: {
mode: "daily", // daily | idle
atHour: 4,
idleMinutes: 60,
},
resetByType: {
thread: { mode: "daily", atHour: 4 },
direct: { mode: "idle", idleMinutes: 240 },
group: { mode: "idle", idleMinutes: 120 },
},
resetTriggers: ["/new", "/reset"],
store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
maintenance: {
mode: "warn", // warn | enforce
pruneAfter: "30d",
maxEntries: 500,
rotateBytes: "10mb",
},
mainKey: "main", // legacy (runtime always uses "main")
agentToAgent: { maxPingPongTurns: 5 },
sendPolicy: {
rules: [{ action: "deny", match: { channel: "discord", chatType: "group" } }],
default: "allow",
},
},
}
```
<Accordion title="Session field details">
- **`dmScope`**: how DMs are grouped.
- `main`: all DMs share the main session.
- `per-peer`: isolate by sender id across channels.
- `per-channel-peer`: isolate per channel + sender (recommended for multi-user inboxes).
- `per-account-channel-peer`: isolate per account + channel + sender (recommended for multi-account).
- **`identityLinks`**: map canonical ids to provider-prefixed peers for cross-channel session sharing.
- **`reset`**: primary reset policy. `daily` resets at `atHour` local time; `idle` resets after `idleMinutes`. When both configured, whichever expires first wins.
- **`resetByType`**: per-type overrides (`direct`, `group`, `thread`). Legacy `dm` accepted as alias for `direct`.
- **`mainKey`**: legacy field. Runtime now always uses `"main"` for the main direct-chat bucket.
- **`sendPolicy`**: match by `channel`, `chatType` (`direct|group|channel`, with legacy `dm` alias), or `keyPrefix`. First deny wins.
- **`maintenance`**: `warn` warns the active session on eviction; `enforce` applies pruning and rotation.
</Accordion>
---
## Messages
```json5
{
messages: {
responsePrefix: "🦞", // or "auto"
ackReaction: "👀",
ackReactionScope: "group-mentions", // group-mentions | group-all | direct | all
removeAckAfterReply: false,
queue: {
mode: "collect", // steer | followup | collect | steer-backlog | steer+backlog | queue | interrupt
debounceMs: 1000,
cap: 20,
drop: "summarize", // old | new | summarize
byChannel: {
whatsapp: "collect",
telegram: "collect",
},
},
inbound: {
debounceMs: 2000, // 0 disables
byChannel: {
whatsapp: 5000,
slack: 1500,
},
},
},
}
```
### Response prefix
Per-channel/account overrides: `channels.<channel>.responsePrefix`, `channels.<channel>.accounts.<id>.responsePrefix`.
Resolution (most specific wins): account → channel → global. `""` disables and stops cascade. `"auto"` derives `[{identity.name}]`.
**Template variables:**
| Variable | Description | Example |
| ----------------- | ---------------------- | --------------------------- |
| `{model}` | Short model name | `claude-opus-4-6` |
| `{modelFull}` | Full model identifier | `anthropic/claude-opus-4-6` |
| `{provider}` | Provider name | `anthropic` |
| `{thinkingLevel}` | Current thinking level | `high`, `low`, `off` |
| `{identity.name}` | Agent identity name | (same as `"auto"`) |
Variables are case-insensitive. `{think}` is an alias for `{thinkingLevel}`.
### Ack reaction
- Defaults to active agent's `identity.emoji`, otherwise `"👀"`. Set `""` to disable.
- Scope: `group-mentions` (default), `group-all`, `direct`, `all`.
- `removeAckAfterReply`: removes ack after reply (Slack/Discord/Telegram/Google Chat only).
### Inbound debounce
Batches rapid text-only messages from the same sender into a single agent turn. Media/attachments flush immediately. Control commands bypass debouncing.
### TTS (text-to-speech)
```json5
{
messages: {
tts: {
auto: "always", // off | always | inbound | tagged
mode: "final", // final | all
provider: "elevenlabs",
summaryModel: "openai/gpt-4.1-mini",
modelOverrides: { enabled: true },
maxTextLength: 4000,
timeoutMs: 30000,
prefsPath: "~/.openclaw/settings/tts.json",
elevenlabs: {
apiKey: "elevenlabs_api_key",
baseUrl: "https://api.elevenlabs.io",
voiceId: "voice_id",
modelId: "eleven_multilingual_v2",
seed: 42,
applyTextNormalization: "auto",
languageCode: "en",
voiceSettings: {
stability: 0.5,
similarityBoost: 0.75,
style: 0.0,
useSpeakerBoost: true,
speed: 1.0,
},
},
openai: {
apiKey: "openai_api_key",
model: "gpt-4o-mini-tts",
voice: "alloy",
},
},
},
}
```
- `auto` controls auto-TTS. `/tts off|always|inbound|tagged` overrides per session.
- `summaryModel` overrides `agents.defaults.model.primary` for auto-summary.
- API keys fall back to `ELEVENLABS_API_KEY`/`XI_API_KEY` and `OPENAI_API_KEY`.
---
## Talk
Defaults for Talk mode (macOS/iOS/Android).
```json5
{
talk: {
voiceId: "elevenlabs_voice_id",
voiceAliases: {
Clawd: "EXAVITQu4vr4xnSDxMaL",
Roger: "CwhRBWXzGAHq8TQ4Fs17",
},
modelId: "eleven_v3",
outputFormat: "mp3_44100_128",
apiKey: "elevenlabs_api_key",
interruptOnSpeech: true,
},
}
```
- Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID`.
- `apiKey` falls back to `ELEVENLABS_API_KEY`.
- `voiceAliases` lets Talk directives use friendly names.
---
## Tools
### Tool profiles
`tools.profile` sets a base allowlist before `tools.allow`/`tools.deny`:
| Profile | Includes |
| ----------- | ----------------------------------------------------------------------------------------- |
| `minimal` | `session_status` only |
| `coding` | `group:fs`, `group:runtime`, `group:sessions`, `group:memory`, `image` |
| `messaging` | `group:messaging`, `sessions_list`, `sessions_history`, `sessions_send`, `session_status` |
| `full` | No restriction (same as unset) |
### Tool groups
| Group | Tools |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `group:runtime` | `exec`, `process` (`bash` is accepted as an alias for `exec`) |
| `group:fs` | `read`, `write`, `edit`, `apply_patch` |
| `group:sessions` | `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` |
| `group:memory` | `memory_search`, `memory_get` |
| `group:web` | `web_search`, `web_fetch` |
| `group:ui` | `browser`, `canvas` |
| `group:automation` | `cron`, `gateway` |
| `group:messaging` | `message` |
| `group:nodes` | `nodes` |
| `group:openclaw` | All built-in tools (excludes provider plugins) |
### `tools.allow` / `tools.deny`
Global tool allow/deny policy (deny wins). Case-insensitive, supports `*` wildcards. Applied even when Docker sandbox is off.
```json5
{
tools: { deny: ["browser", "canvas"] },
}
```
### `tools.byProvider`
Further restrict tools for specific providers or models. Order: base profile → provider profile → allow/deny.
```json5
{
tools: {
profile: "coding",
byProvider: {
"google-antigravity": { profile: "minimal" },
"openai/gpt-5.2": { allow: ["group:fs", "sessions_list"] },
},
},
}
```
### `tools.elevated`
Controls elevated (host) exec access:
```json5
{
tools: {
elevated: {
enabled: true,
allowFrom: {
whatsapp: ["+15555550123"],
discord: ["steipete", "1234567890123"],
},
},
},
}
```
- Per-agent override (`agents.list[].tools.elevated`) can only further restrict.
- `/elevated on|off|ask|full` stores state per session; inline directives apply to single message.
- Elevated `exec` runs on the host, bypasses sandboxing.
### `tools.exec`
```json5
{
tools: {
exec: {
backgroundMs: 10000,
timeoutSec: 1800,
cleanupMs: 1800000,
notifyOnExit: true,
applyPatch: {
enabled: false,
allowModels: ["gpt-5.2"],
},
},
},
}
```
### `tools.web`
```json5
{
tools: {
web: {
search: {
enabled: true,
apiKey: "brave_api_key", // or BRAVE_API_KEY env
maxResults: 5,
timeoutSeconds: 30,
cacheTtlMinutes: 15,
},
fetch: {
enabled: true,
maxChars: 50000,
maxCharsCap: 50000,
timeoutSeconds: 30,
cacheTtlMinutes: 15,
userAgent: "custom-ua",
},
},
},
}
```
### `tools.media`
Configures inbound media understanding (image/audio/video):
```json5
{
tools: {
media: {
concurrency: 2,
audio: {
enabled: true,
maxBytes: 20971520,
scope: {
default: "deny",
rules: [{ action: "allow", match: { chatType: "direct" } }],
},
models: [
{ provider: "openai", model: "gpt-4o-mini-transcribe" },
{ type: "cli", command: "whisper", args: ["--model", "base", "{{MediaPath}}"] },
],
},
video: {
enabled: true,
maxBytes: 52428800,
models: [{ provider: "google", model: "gemini-3-flash-preview" }],
},
},
},
}
```
<Accordion title="Media model entry fields">
**Provider entry** (`type: "provider"` or omitted):
- `provider`: API provider id (`openai`, `anthropic`, `google`/`gemini`, `groq`, etc.)
- `model`: model id override
- `profile` / `preferredProfile`: auth profile selection
**CLI entry** (`type: "cli"`):
- `command`: executable to run
- `args`: templated args (supports `{{MediaPath}}`, `{{Prompt}}`, `{{MaxChars}}`, etc.)
**Common fields:**
- `capabilities`: optional list (`image`, `audio`, `video`). Defaults: `openai`/`anthropic`/`minimax` → image, `google` → image+audio+video, `groq` → audio.
- `prompt`, `maxChars`, `maxBytes`, `timeoutSeconds`, `language`: per-entry overrides.
- Failures fall back to the next entry.
Provider auth follows standard order: auth profiles → env vars → `models.providers.*.apiKey`.
</Accordion>
### `tools.agentToAgent`
```json5
{
tools: {
agentToAgent: {
enabled: false,
allow: ["home", "work"],
},
},
}
```
### `tools.subagents`
```json5
{
agents: {
defaults: {
subagents: {
model: "minimax/MiniMax-M2.1",
maxConcurrent: 1,
archiveAfterMinutes: 60,
},
},
},
}
```
- `model`: default model for spawned sub-agents. If omitted, sub-agents inherit the caller's model.
- Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny`.
---
## Custom providers and base URLs
OpenClaw uses the pi-coding-agent model catalog. Add custom providers via `models.providers` in config or `~/.openclaw/agents/<agentId>/agent/models.json`.
```json5
{
models: {
mode: "merge", // merge (default) | replace
providers: {
"custom-proxy": {
baseUrl: "http://localhost:4000/v1",
apiKey: "LITELLM_KEY",
api: "openai-completions", // openai-completions | openai-responses | anthropic-messages | google-generative-ai
models: [
{
id: "llama-3.1-8b",
name: "Llama 3.1 8B",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 32000,
},
],
},
},
},
}
```
- Use `authHeader: true` + `headers` for custom auth needs.
- Override agent config root with `OPENCLAW_AGENT_DIR` (or `PI_CODING_AGENT_DIR`).
### Provider examples
<Accordion title="Cerebras (GLM 4.6 / 4.7)">
```json5
{
env: { CEREBRAS_API_KEY: "sk-..." },
agents: {
defaults: {
model: {
primary: "cerebras/zai-glm-4.7",
fallbacks: ["cerebras/zai-glm-4.6"],
},
models: {
"cerebras/zai-glm-4.7": { alias: "GLM 4.7 (Cerebras)" },
"cerebras/zai-glm-4.6": { alias: "GLM 4.6 (Cerebras)" },
},
},
},
models: {
mode: "merge",
providers: {
cerebras: {
baseUrl: "https://api.cerebras.ai/v1",
apiKey: "${CEREBRAS_API_KEY}",
api: "openai-completions",
models: [
{ id: "zai-glm-4.7", name: "GLM 4.7 (Cerebras)" },
{ id: "zai-glm-4.6", name: "GLM 4.6 (Cerebras)" },
],
},
},
},
}
```
Use `cerebras/zai-glm-4.7` for Cerebras; `zai/glm-4.7` for Z.AI direct.
</Accordion>
<Accordion title="OpenCode Zen">
```json5
{
agents: {
defaults: {
model: { primary: "opencode/claude-opus-4-6" },
models: { "opencode/claude-opus-4-6": { alias: "Opus" } },
},
},
}
```
Set `OPENCODE_API_KEY` (or `OPENCODE_ZEN_API_KEY`). Shortcut: `openclaw onboard --auth-choice opencode-zen`.
</Accordion>
<Accordion title="Z.AI (GLM-4.7)">
```json5
{
agents: {
defaults: {
model: { primary: "zai/glm-4.7" },
models: { "zai/glm-4.7": {} },
},
},
}
```
Set `ZAI_API_KEY`. `z.ai/*` and `z-ai/*` are accepted aliases. Shortcut: `openclaw onboard --auth-choice zai-api-key`.
- General endpoint: `https://api.z.ai/api/paas/v4`
- Coding endpoint (default): `https://api.z.ai/api/coding/paas/v4`
- For the general endpoint, define a custom provider with the base URL override.
</Accordion>
<Accordion title="Moonshot AI (Kimi)">
```json5
{
env: { MOONSHOT_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "moonshot/kimi-k2.5" },
models: { "moonshot/kimi-k2.5": { alias: "Kimi K2.5" } },
},
},
models: {
mode: "merge",
providers: {
moonshot: {
baseUrl: "https://api.moonshot.ai/v1",
apiKey: "${MOONSHOT_API_KEY}",
api: "openai-completions",
models: [
{
id: "kimi-k2.5",
name: "Kimi K2.5",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 256000,
maxTokens: 8192,
},
],
},
},
},
}
```
For the China endpoint: `baseUrl: "https://api.moonshot.cn/v1"` or `openclaw onboard --auth-choice moonshot-api-key-cn`.
</Accordion>
<Accordion title="Kimi Coding">
```json5
{
env: { KIMI_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "kimi-coding/k2p5" },
models: { "kimi-coding/k2p5": { alias: "Kimi K2.5" } },
},
},
}
```
Anthropic-compatible, built-in provider. Shortcut: `openclaw onboard --auth-choice kimi-code-api-key`.
</Accordion>
<Accordion title="Synthetic (Anthropic-compatible)">
```json5
{
env: { SYNTHETIC_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.1" },
models: { "synthetic/hf:MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax M2.1" } },
},
},
models: {
mode: "merge",
providers: {
synthetic: {
baseUrl: "https://api.synthetic.new/anthropic",
apiKey: "${SYNTHETIC_API_KEY}",
api: "anthropic-messages",
models: [
{
id: "hf:MiniMaxAI/MiniMax-M2.1",
name: "MiniMax M2.1",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 192000,
maxTokens: 65536,
},
],
},
},
},
}
```
Base URL should omit `/v1` (Anthropic client appends it). Shortcut: `openclaw onboard --auth-choice synthetic-api-key`.
</Accordion>
<Accordion title="MiniMax M2.1 (direct)">
```json5
{
agents: {
defaults: {
model: { primary: "minimax/MiniMax-M2.1" },
models: {
"minimax/MiniMax-M2.1": { alias: "Minimax" },
},
},
},
models: {
mode: "merge",
providers: {
minimax: {
baseUrl: "https://api.minimax.io/anthropic",
apiKey: "${MINIMAX_API_KEY}",
api: "anthropic-messages",
models: [
{
id: "MiniMax-M2.1",
name: "MiniMax M2.1",
reasoning: false,
input: ["text"],
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
contextWindow: 200000,
maxTokens: 8192,
},
],
},
},
},
}
```
Set `MINIMAX_API_KEY`. Shortcut: `openclaw onboard --auth-choice minimax-api`.
</Accordion>
<Accordion title="Local models (LM Studio)">
See [Local Models](/gateway/local-models). TL;DR: run MiniMax M2.1 via LM Studio Responses API on serious hardware; keep hosted models merged for fallback.
</Accordion>
---
## Skills
```json5
{
skills: {
allowBundled: ["gemini", "peekaboo"],
load: {
extraDirs: ["~/Projects/agent-scripts/skills"],
},
install: {
preferBrew: true,
nodeManager: "npm", // npm | pnpm | yarn
},
entries: {
"nano-banana-pro": {
apiKey: "GEMINI_KEY_HERE",
env: { GEMINI_API_KEY: "GEMINI_KEY_HERE" },
},
peekaboo: { enabled: true },
sag: { enabled: false },
},
},
}
```
- `allowBundled`: optional allowlist for bundled skills only (managed/workspace skills unaffected).
- `entries.<skillKey>.enabled: false` disables a skill even if bundled/installed.
- `entries.<skillKey>.apiKey`: convenience for skills declaring a primary env var.
---
## Plugins
```json5
{
plugins: {
enabled: true,
allow: ["voice-call"],
deny: [],
load: {
paths: ["~/Projects/oss/voice-call-extension"],
},
entries: {
"voice-call": {
enabled: true,
config: { provider: "twilio" },
},
},
},
}
```
- Loaded from `~/.openclaw/extensions`, `<workspace>/.openclaw/extensions`, plus `plugins.load.paths`.
- **Config changes require a gateway restart.**
- `allow`: optional allowlist (only listed plugins load). `deny` wins.
See [Plugins](/tools/plugin).
---
## Browser
```json5
{
browser: {
enabled: true,
evaluateEnabled: true,
defaultProfile: "chrome",
profiles: {
openclaw: { cdpPort: 18800, color: "#FF4500" },
work: { cdpPort: 18801, color: "#0066CC" },
remote: { cdpUrl: "http://10.0.0.42:9222", color: "#00AA00" },
},
color: "#FF4500",
// headless: false,
// noSandbox: false,
// executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
// attachOnly: false,
},
}
```
- `evaluateEnabled: false` disables `act:evaluate` and `wait --fn`.
- Remote profiles are attach-only (start/stop/reset disabled).
- Auto-detect order: default browser if Chromium-based → Chrome → Brave → Edge → Chromium → Chrome Canary.
- Control service: loopback only (port derived from `gateway.port`, default `18791`).
---
## UI
```json5
{
ui: {
seamColor: "#FF4500",
assistant: {
name: "OpenClaw",
avatar: "CB", // emoji, short text, image URL, or data URI
},
},
}
```
- `seamColor`: accent color for native app UI chrome (Talk Mode bubble tint, etc.).
- `assistant`: Control UI identity override. Falls back to active agent identity.
---
## Gateway
```json5
{
gateway: {
mode: "local", // local | remote
port: 18789,
bind: "loopback",
auth: {
mode: "token", // token | password
token: "your-token",
// password: "your-password", // or OPENCLAW_GATEWAY_PASSWORD
allowTailscale: true,
},
tailscale: {
mode: "off", // off | serve | funnel
resetOnExit: false,
},
controlUi: {
enabled: true,
basePath: "/openclaw",
// root: "dist/control-ui",
// allowInsecureAuth: false,
// dangerouslyDisableDeviceAuth: false,
},
remote: {
url: "ws://gateway.tailnet:18789",
transport: "ssh", // ssh | direct
token: "your-token",
// password: "your-password",
},
trustedProxies: ["10.0.0.1"],
},
}
```
<Accordion title="Gateway field details">
- `mode`: `local` (run gateway) or `remote` (connect to remote gateway). Gateway refuses to start unless `local`.
- `port`: single multiplexed port for WS + HTTP. Precedence: `--port` > `OPENCLAW_GATEWAY_PORT` > `gateway.port` > `18789`.
- `bind`: `auto`, `loopback` (default), `lan` (`0.0.0.0`), `tailnet` (Tailscale IP only), or `custom`.
- **Auth**: required by default. Non-loopback binds require a shared token/password. Onboarding wizard generates a token by default.
- `auth.allowTailscale`: when `true`, Tailscale Serve identity headers satisfy auth (verified via `tailscale whois`). Defaults to `true` when `tailscale.mode = "serve"`.
- `tailscale.mode`: `serve` (tailnet only, loopback bind) or `funnel` (public, requires auth).
- `remote.transport`: `ssh` (default) or `direct` (ws/wss). For `direct`, `remote.url` must be `ws://` or `wss://`.
- `gateway.remote.token` is for remote CLI calls only; does not enable local gateway auth.
- `trustedProxies`: reverse proxy IPs that terminate TLS. Only list proxies you control.
</Accordion>
### OpenAI-compatible endpoints
- Chat Completions: disabled by default. Enable with `gateway.http.endpoints.chatCompletions.enabled: true`.
- Responses API: `gateway.http.endpoints.responses.enabled`.
### Multi-instance isolation
Run multiple gateways on one host with unique ports and state dirs:
```bash
OPENCLAW_CONFIG_PATH=~/.openclaw/a.json \
OPENCLAW_STATE_DIR=~/.openclaw-a \
openclaw gateway --port 19001
```
Convenience flags: `--dev` (uses `~/.openclaw-dev` + port `19001`), `--profile <name>` (uses `~/.openclaw-<name>`).
See [Multiple Gateways](/gateway/multiple-gateways).
---
## Hooks
```json5
{
hooks: {
enabled: true,
token: "shared-secret",
path: "/hooks",
maxBodyBytes: 262144,
allowedAgentIds: ["hooks", "main"],
presets: ["gmail"],
transformsDir: "~/.openclaw/hooks",
mappings: [
{
match: { path: "gmail" },
action: "agent",
agentId: "hooks",
wakeMode: "now",
name: "Gmail",
sessionKey: "hook:gmail:{{messages[0].id}}",
messageTemplate: "From: {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}",
deliver: true,
channel: "last",
model: "openai/gpt-5.2-mini",
},
],
},
}
```
Auth: `Authorization: Bearer <token>` or `x-openclaw-token: <token>`.
**Endpoints:**
- `POST /hooks/wake``{ text, mode?: "now"|"next-heartbeat" }`
- `POST /hooks/agent``{ message, name?, agentId?, sessionKey?, wakeMode?, deliver?, channel?, to?, model?, thinking?, timeoutSeconds? }`
- `POST /hooks/<name>` → resolved via `hooks.mappings`
<Accordion title="Mapping details">
- `match.path` matches sub-path after `/hooks` (e.g. `/hooks/gmail``gmail`).
- `match.source` matches a payload field for generic paths.
- Templates like `{{messages[0].subject}}` read from the payload.
- `transform` can point to a JS/TS module returning a hook action.
- `agentId` routes to a specific agent; unknown IDs fall back to default.
- `allowedAgentIds`: restricts explicit routing (`*` or omitted = allow all, `[]` = deny all).
- `deliver: true` sends final reply to a channel; `channel` defaults to `last`.
- `model` overrides LLM for this hook run (must be allowed if model catalog is set).
</Accordion>
### Gmail integration
```json5
{
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" },
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
thinking: "off",
},
},
}
```
- Gateway auto-starts `gog gmail watch serve` on boot when configured. Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to disable.
- Don't run a separate `gog gmail watch serve` alongside the Gateway.
---
## Canvas host
```json5
{
canvasHost: {
root: "~/.openclaw/workspace/canvas",
port: 18793,
liveReload: true,
// enabled: false, // or OPENCLAW_SKIP_CANVAS_HOST=1
},
}
```
- Serves HTML/CSS/JS over HTTP for iOS/Android nodes.
- Injects live-reload client into served HTML.
- Auto-creates starter `index.html` when empty.
- Also serves A2UI at `/__openclaw__/a2ui/`.
- Changes require a gateway restart.
- Disable live reload for large directories or `EMFILE` errors.
---
## Discovery
### mDNS (Bonjour)
```json5
{
discovery: {
mdns: {
mode: "minimal", // minimal | full | off
},
},
}
```
- `minimal` (default): omit `cliPath` + `sshPort` from TXT records.
- `full`: include `cliPath` + `sshPort`.
- Hostname defaults to `openclaw`. Override with `OPENCLAW_MDNS_HOSTNAME`.
### Wide-area (DNS-SD)
```json5
{
discovery: {
wideArea: { enabled: true },
},
}
```
Writes a unicast DNS-SD zone under `~/.openclaw/dns/`. For cross-network discovery, pair with a DNS server (CoreDNS recommended) + Tailscale split DNS.
Setup: `openclaw dns setup --apply`.
---
## Environment
### `env` (inline env vars)
```json5
{
env: {
OPENROUTER_API_KEY: "sk-or-...",
vars: {
GROQ_API_KEY: "gsk-...",
},
shellEnv: {
enabled: true,
timeoutMs: 15000,
},
},
}
```
- Inline env vars are only applied if the process env is missing the key.
- `.env` files: CWD `.env` + `~/.openclaw/.env` (neither overrides existing vars).
- `shellEnv`: imports missing expected keys from your login shell profile.
- See [Environment](/help/environment) for full precedence.
### Env var substitution
Reference env vars in any config string with `${VAR_NAME}`:
```json5
{
gateway: {
auth: { token: "${OPENCLAW_GATEWAY_TOKEN}" },
},
}
```
- Only uppercase names matched: `[A-Z_][A-Z0-9_]*`.
- Missing/empty vars throw an error at config load.
- Escape with `$${VAR}` for a literal `${VAR}`.
- Works with `$include`.
---
## Auth storage
```json5
{
auth: {
profiles: {
"anthropic:me@example.com": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
"anthropic:work": { provider: "anthropic", mode: "api_key" },
},
order: {
anthropic: ["anthropic:me@example.com", "anthropic:work"],
},
},
}
```
- Per-agent auth profiles stored at `<agentDir>/auth-profiles.json`.
- Legacy OAuth imports from `~/.openclaw/credentials/oauth.json`.
- See [OAuth](/concepts/oauth).
---
## Logging
```json5
{
logging: {
level: "info",
file: "/tmp/openclaw/openclaw.log",
consoleLevel: "info",
consoleStyle: "pretty", // pretty | compact | json
redactSensitive: "tools", // off | tools
redactPatterns: ["\\bTOKEN\\b\\s*[=:]\\s*([\"']?)([^\\s\"']+)\\1"],
},
}
```
- Default log file: `/tmp/openclaw/openclaw-YYYY-MM-DD.log`.
- Set `logging.file` for a stable path.
- `consoleLevel` bumps to `debug` when `--verbose`.
---
## Wizard
Metadata written by CLI wizards (`onboard`, `configure`, `doctor`):
```json5
{
wizard: {
lastRunAt: "2026-01-01T00:00:00.000Z",
lastRunVersion: "2026.1.4",
lastRunCommit: "abc1234",
lastRunCommand: "configure",
lastRunMode: "local",
},
}
```
---
## Identity
```json5
{
agents: {
list: [
{
id: "main",
identity: {
name: "Samantha",
theme: "helpful sloth",
emoji: "🦥",
avatar: "avatars/samantha.png",
},
},
],
},
}
```
Written by the macOS onboarding assistant. Derives defaults:
- `messages.ackReaction` from `identity.emoji` (falls back to 👀)
- `mentionPatterns` from `identity.name`/`identity.emoji`
- `avatar` accepts: workspace-relative path, `http(s)` URL, or `data:` URI
---
## Bridge (legacy, removed)
Current builds no longer include the TCP bridge. Nodes connect over the Gateway WebSocket. `bridge.*` keys are no longer part of the config schema (validation fails until removed; `openclaw doctor --fix` can strip unknown keys).
<Accordion title="Legacy bridge config (historical reference)">
```json
{
"bridge": {
"enabled": true,
"port": 18790,
"bind": "tailnet",
"tls": {
"enabled": true,
"autoGenerate": true
}
}
}
```
</Accordion>
---
## Cron
```json5
{
cron: {
enabled: true,
maxConcurrentRuns: 2,
sessionRetention: "24h", // duration string or false
},
}
```
- `sessionRetention`: how long to keep completed cron sessions before pruning. Default: `24h`.
See [Cron Jobs](/automation/cron-jobs).
---
## Media model template variables
Template placeholders expanded in `tools.media.*.models[].args`:
| Variable | Description |
| ------------------ | ------------------------------------------------- |
| `{{Body}}` | Full inbound message body |
| `{{RawBody}}` | Raw body (no history/sender wrappers) |
| `{{BodyStripped}}` | Body with group mentions stripped |
| `{{From}}` | Sender identifier |
| `{{To}}` | Destination identifier |
| `{{MessageSid}}` | Channel message id |
| `{{SessionId}}` | Current session UUID |
| `{{IsNewSession}}` | `"true"` when new session created |
| `{{MediaUrl}}` | Inbound media pseudo-URL |
| `{{MediaPath}}` | Local media path |
| `{{MediaType}}` | Media type (image/audio/document/…) |
| `{{Transcript}}` | Audio transcript |
| `{{Prompt}}` | Resolved media prompt for CLI entries |
| `{{MaxChars}}` | Resolved max output chars for CLI entries |
| `{{ChatType}}` | `"direct"` or `"group"` |
| `{{GroupSubject}}` | Group subject (best effort) |
| `{{GroupMembers}}` | Group members preview (best effort) |
| `{{SenderName}}` | Sender display name (best effort) |
| `{{SenderE164}}` | Sender phone number (best effort) |
| `{{Provider}}` | Provider hint (whatsapp, telegram, discord, etc.) |
---
## Config includes (`$include`)
Split config into multiple files:
```json5
// ~/.openclaw/openclaw.json
{
gateway: { port: 18789 },
agents: { $include: "./agents.json5" },
broadcast: {
$include: ["./clients/mueller.json5", "./clients/schmidt.json5"],
},
}
```
**Merge behavior:**
- Single file: replaces the containing object.
- Array of files: deep-merged in order (later overrides earlier).
- Sibling keys: merged after includes (override included values).
- Nested includes: up to 10 levels deep.
- Paths: relative (to the including file), absolute, or `../` parent references.
- Errors: clear messages for missing files, parse errors, and circular includes.
---
_Related: [Configuration](/gateway/configuration) · [Configuration Examples](/gateway/configuration-examples) · [Doctor](/gateway/doctor)_