Skip to content

Configure OpenClaw Channels: DM and Group Access Guide

Each channel you configure starts automatically as soon as its config section exists, unless you explicitly set enabled: false.

All channels follow DM and group policies to keep your setup secure. Here is how they behave:

DM policyBehavior
pairing (default)Unknown senders get a one-time pairing code; owner must approve
allowlistOnly senders in allowFrom (or paired allow store)
openAllow all inbound DMs (requires allowFrom: ["*"])
disabledIgnore all inbound DMs
Group policyBehavior
allowlist (default)Only groups matching the configured allowlist
openBypass group allowlists (mention-gating still applies)
disabledBlock all group/room messages

Note that channels.defaults.groupPolicy sets the fallback when a provider’s groupPolicy is missing. Pairing codes expire after 1 hour, and pending requests are capped at 3 per channel. If a provider block is missing, the system falls back to allowlist and gives you a warning.

You can use channels.modelByChannel to pin specific channel IDs to a specific model. This is great if you want your Telegram group to use a cheaper model while your DMs use a more powerful one.

{
channels: {
modelByChannel: {
discord: {
"123456789012345678": "anthropic/claude-opus-4-6",
},
slack: {
C1234567890: "openai/gpt-4.1",
},
telegram: {
"-1001234567890": "openai/gpt-4.1-mini",
"-1001234567890:topic:99": "anthropic/claude-sonnet-4-6",
},
},
},
}

Use channels.defaults to manage shared behavior across all your providers:

{
channels: {
defaults: {
groupPolicy: "allowlist", // open | allowlist | disabled
heartbeat: {
showOk: false,
showAlerts: true,
useIndicator: true,
},
},
},
}

WhatsApp runs through the gateway’s web channel. It starts up automatically when you have a linked session.

{
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,
},
},
}

If you need to run multiple WhatsApp accounts, you can use the accounts block:

{
channels: {
whatsapp: {
accounts: {
default: {},
personal: {},
biz: {
// authDir: "~/.openclaw/credentials/whatsapp/biz",
},
},
},
},
}

Telegram supports both bot tokens and webhooks. You can even set up custom commands for the bot menu.

{
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,
streaming: "partial", // off | partial | block | progress (default: off)
actions: { reactions: true, sendMessage: true },
reactionNotifications: "own", // off | own | all
mediaMaxMb: 100,
retry: {
attempts: 3,
minDelayMs: 400,
maxDelayMs: 30000,
jitter: 0.1,
},
network: {
autoSelectFamily: true,
dnsResultOrder: "ipv4first",
},
proxy: "socks5://localhost:9050",
webhookUrl: "https://example.com/telegram-webhook",
webhookSecret: "secret",
webhookPath: "/telegram-webhook",
},
},
}

Discord is packed with features like voice support, thread bindings, and detailed action permissions.

{
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
dmPolicy: "pairing",
allowFrom: ["1234567890", "123456789012345678"],
dm: { enabled: true, groupEnabled: false, groupChannels: ["openclaw-dm"] },
guilds: {
"123456789012345678": {
slug: "friends-of-openclaw",
requireMention: false,
ignoreOtherMentions: true,
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
streaming: "off", // off | partial | block | progress (progress maps to partial on Discord)
maxLinesPerMessage: 17,
ui: {
components: {
accentColor: "#5865F2",
},
},
threadBindings: {
enabled: true,
idleHours: 24,
maxAgeHours: 0,
spawnSubagentSessions: false, // opt-in for sessions_spawn({ thread: true })
},
voice: {
enabled: true,
autoJoin: [
{
guildId: "123456789012345678",
channelId: "234567890123456789",
},
],
daveEncryption: true,
decryptionFailureTolerance: 24,
tts: {
provider: "openai",
openai: { voice: "alloy" },
},
},
retry: {
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1,
},
},
},
}

You can connect Google Chat using a service account. It supports both DMs and spaces.

{
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,
},
},
}

Slack can run in Socket mode or HTTP mode. It also includes a typingReaction feature to show the bot is working.

{
channels: {
slack: {
enabled: true,
botToken: "xoxb-...",
appToken: "xapp-...",
dmPolicy: "pairing",
allowFrom: ["U123", "U456", "*"],
dm: { enabled: true, 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,
},
typingReaction: "hourglass_flowing_sand",
textChunkLimit: 4000,
chunkMode: "length",
streaming: "partial", // off | partial | block | progress (preview mode)
nativeStreaming: true, // use Slack native streaming API when streaming=partial
mediaMaxMb: 20,
},
},
}

Mattermost is available as a plugin. You can choose different chat modes like oncall or onmessage.

{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
chatmode: "oncall", // oncall | onmessage | onchar
oncharPrefixes: [">", "!"],
commands: {
native: true, // opt-in
nativeSkills: true,
callbackPath: "/api/channels/mattermost/command",
// Optional explicit URL for reverse-proxy/public deployments
callbackUrl: "https://gateway.example.com/api/channels/mattermost/command",
},
textChunkLimit: 4000,
chunkMode: "length",
},
},
}

Signal configuration is straightforward, focusing on account binding and reaction notifications.

{
channels: {
signal: {
enabled: true,
account: "+15555550123", // optional account binding
dmPolicy: "pairing",
allowFrom: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
configWrites: true,
reactionNotifications: "own", // off | own | all | allowlist
reactionAllowlist: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
historyLimit: 50,
},
},
}

BlueBubbles is the best way to use iMessage with OpenClaw.

{
channels: {
bluebubbles: {
enabled: true,
dmPolicy: "pairing",
// serverUrl, password, webhookPath, group controls, and advanced actions:
// see /channels/bluebubbles
},
},
}

If you prefer a direct connection, the iMessage channel uses imsg rpc and requires Full Disk Access to your Messages database.

{
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,
attachmentRoots: ["/Users/*/Library/Messages/Attachments"],
remoteAttachmentRoots: ["/Users/*/Library/Messages/Attachments"],
mediaMaxMb: 16,
service: "auto",
region: "US",
},
},
}

You can even use an SSH wrapper for remote access:

#!/usr/bin/env bash
exec ssh -T gateway-host imsg "$@"

Teams is extension-backed and handles complex enterprise policies.

{
channels: {
msteams: {
enabled: true,
configWrites: true,
// appId, appPassword, tenantId, webhook, team/channel policies:
// see /channels/msteams
},
},
}

For the classic chat experience, IRC is also supported through an extension.

{
channels: {
irc: {
enabled: true,
dmPolicy: "pairing",
configWrites: true,
nickserv: {
enabled: true,
service: "NickServ",
password: "${IRC_NICKSERV_PASSWORD}",
register: false,
registerEmail: "bot@example.com",
},
},
},
}

You can run multiple accounts for any channel by using the accounts object.

{
channels: {
telegram: {
accounts: {
default: {
name: "Primary bot",
botToken: "123456:ABC...",
},
alerts: {
name: "Alerts bot",
botToken: "987654:XYZ...",
},
},
},
},
}

By default, group messages require a mention before the bot responds. This applies to WhatsApp, Telegram, Discord, Google Chat, and iMessage.

{
messages: {
groupChat: { historyLimit: 50 },
},
agents: {
list: [{ id: "main", groupChat: { mentionPatterns: ["@openclaw", "openclaw"] } }],
},
}

You can manage history limits per DM as well:

{
channels: {
telegram: {
dmHistoryLimit: 30,
dms: {
"123456789": { historyLimit: 50 },
},
},
},
}

If you want the bot to respond to your own messages in a self-chat, use this pattern:

{
channels: {
whatsapp: {
allowFrom: ["+15555550123"],
groups: { "*": { requireMention: true } },
},
},
agents: {
list: [
{
id: "main",
groupChat: { mentionPatterns: ["reisponde", "@openclaw"] },
},
],
},
}

Commands allow you to interact with the gateway directly through chat messages.

{
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,
},
}

These settings define the baseline behavior for your agents, from where they store files to which models they use.

This is where your agent’s files live.

{
agents: { defaults: { workspace: "~/.openclaw/workspace" } },
}

You can set an optional repository root for the system prompt.

{
agents: { defaults: { repoRoot: "~/Projects/openclaw" } },
}

If you don’t want OpenClaw to create the default workspace files, set this to true.

{
agents: { defaults: { skipBootstrap: true } },
}

Limits the size of each bootstrap file to prevent context bloat.

{
agents: { defaults: { bootstrapMaxChars: 20000 } },
}

Limits the total size of all bootstrap files combined.

{
agents: { defaults: { bootstrapTotalMaxChars: 150000 } },
}

agents.defaults.bootstrapPromptTruncationWarning

Section titled “agents.defaults.bootstrapPromptTruncationWarning”

Decide how the agent is warned when context is cut off.

{
agents: { defaults: { bootstrapPromptTruncationWarning: "once" } }, // off | once | always
}

Controls the size of images sent to vision models.

{
agents: { defaults: { imageMaxDimensionPx: 1200 } },
}

Sets the timezone for the agent’s context.

{
agents: { defaults: { userTimezone: "America/Chicago" } },
}

Choose between 12-hour or 24-hour formats.

{
agents: { defaults: { timeFormat: "auto" } }, // auto | 12 | 24
}

This is the core model configuration. You can set primary models and fallbacks for text, images, and PDFs.

{
agents: {
defaults: {
models: {
"anthropic/claude-opus-4-6": { alias: "opus" },
"minimax/MiniMax-M2.5": { alias: "minimax" },
},
model: {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["minimax/MiniMax-M2.5"],
},
imageModel: {
primary: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
fallbacks: ["openrouter/google/gemini-2.0-flash-vision:free"],
},
pdfModel: {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["openai/gpt-5-mini"],
},
pdfMaxBytesMb: 10,
pdfMaxPages: 20,
thinkingDefault: "low",
verboseDefault: "off",
elevatedDefault: "on",
timeoutSeconds: 600,
mediaMaxMb: 5,
contextTokens: 200000,
maxConcurrent: 3,
},
},
}

If your API providers fail, you can use local CLI tools as backends.

{
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",
},
},
},
},
}

Set up periodic tasks for your agents to perform automatically.

{
agents: {
defaults: {
heartbeat: {
every: "30m", // 0m disables
model: "openai/gpt-5.2-mini",
includeReasoning: false,
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
session: "main",
to: "+15555550123",
directPolicy: "allow", // allow (default) | block
target: "none", // default: none | options: last | whatsapp | telegram | discord | ...
prompt: "Read HEARTBEAT.md if it exists...",
ackMaxChars: 300,
suppressToolErrorWarnings: false,
},
},
},
}

Compaction helps manage long conversation histories by summarizing them.

{
agents: {
defaults: {
compaction: {
mode: "safeguard", // default | safeguard
reserveTokensFloor: 24000,
identifierPolicy: "strict", // strict | off | custom
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
postCompactionSections: ["Session Startup", "Red Lines"], // [] disables reinjection
model: "openrouter/anthropic/claude-sonnet-4-5", // optional compaction-only model override
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.",
},
},
},
},
}

Pruning removes old tool results from the active context to save tokens.

{
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"] },
},
},
},
}

You can configure how the bot sends messages in chunks.

{
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)
},
},
}

Control how the bot shows it is “typing” or “thinking”.

{
agents: {
defaults: {
typingMode: "instant", // never | instant | thinking | message
typingIntervalSeconds: 6,
},
},
}

For security, you can run your agents inside Docker containers.

{
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",
network: "openclaw-sandbox-browser",
cdpPort: 9222,
cdpSourceRange: "172.21.0.1/32",
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"],
},
},
},
}

To use this, you will need to build the images:

Terminal window
scripts/sandbox-setup.sh # main sandbox image
scripts/sandbox-browser-setup.sh # optional browser image

You can define specific agents with their own identities, tools, and workspaces.

{
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 }
params: { cacheRetention: "none" }, // overrides matching defaults.models params by key
identity: {
name: "Samantha",
theme: "helpful sloth",
emoji: "🦥",
avatar: "avatars/samantha.png",
},
groupChat: { mentionPatterns: ["@openclaw"] },
sandbox: { mode: "off" },
runtime: {
type: "acp",
acp: {
agent: "codex",
backend: "acpx",
mode: "persistent",
cwd: "/workspace/openclaw",
},
},
subagents: { allowAgents: ["*"] },
tools: {
profile: "coding",
allow: ["browser"],
deny: ["canvas"],
elevated: { enabled: true },
},
},
],
},
}

You can run multiple isolated agents inside one Gateway and route them based on the channel or account.

{
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" } },
],
}

You can create different profiles for different levels of access.

Full access (no sandbox):

{
agents: {
list: [
{
id: "personal",
workspace: "~/.openclaw/workspace-personal",
sandbox: { mode: "off" },
},
],
},
}

Read-only tools + workspace:

{
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 only):

{
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",
],
},
},
],
},
}

Session settings control how conversations are grouped, stored, and reset.

{
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",
parentForkMaxTokens: 100000, // skip parent-thread fork above this token count (0 disables)
maintenance: {
mode: "warn", // warn | enforce
pruneAfter: "30d",
maxEntries: 500,
rotateBytes: "10mb",
resetArchiveRetention: "30d", // duration or false
maxDiskBytes: "500mb", // optional hard budget
highWaterBytes: "400mb", // optional cleanup target
},
threadBindings: {
enabled: true,
idleHours: 24, // default inactivity auto-unfocus in hours (`0` disables)
maxAgeHours: 0, // default hard max age in hours (`0` disables)
},
mainKey: "main", // legacy (runtime always uses "main")
agentToAgent: { maxPingPongTurns: 5 },
sendPolicy: {
rules: [{ action: "deny", match: { channel: "discord", chatType: "group" } }],
default: "allow",
},
},
}

AI Setup Assistant

You can control how your agent interacts with you through the messages configuration. This section handles everything from visual cues to how the agent batches your incoming texts.

{
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,
},
},
},
}

If you want to know exactly which agent is replying, use a response prefix. You can set these globally, per channel, or per account. The system resolves these by looking at the most specific setting first: account → channel → global.

Setting this to "" disables it. If you use "auto", the agent uses its identity name in brackets, like [{identity.name}].

Template variables:

VariableDescriptionExample
{model}Short model nameclaude-opus-4-6
{modelFull}Full model identifieranthropic/claude-opus-4-6
{provider}Provider nameanthropic
{thinkingLevel}Current thinking levelhigh, low, off
{identity.name}Agent identity name(same as "auto")

These variables are case-insensitive. You can also use {think} as a shortcut for {thinkingLevel}.

The agent can react to your messages to show it is listening. By default, it uses the emoji from its identity or "👀". You can change this per channel or account.

The scope determines when the agent reacts: group-mentions is the default, but you can choose group-all, direct, or all. If you are on Slack, Discord, Telegram, or Google Chat, you can set removeAckAfterReply to true to clean up the reaction once the agent responds.

Rapid-fire texting can confuse an agent. Inbound debounce batches quick text messages from the same sender into a single turn. Media and attachments will still flush immediately, and control commands always bypass this delay.

If you want your agent to talk back, you can configure Text-to-Speech using ElevenLabs or OpenAI.

{
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",
baseUrl: "https://api.openai.com/v1",
model: "gpt-4o-mini-tts",
voice: "alloy",
},
},
},
}

The auto setting controls when TTS triggers. You can also use the /tts command to override this during a session. If you use a non-OpenAI endpoint for openai.baseUrl, the system treats it as an OpenAI-compatible server and skips strict validation for models and voices.

Talk mode is designed for voice interactions on macOS, iOS, and Android. It has its own set of defaults to keep the conversation fluid.

{
talk: {
voiceId: "elevenlabs_voice_id",
voiceAliases: {
Clawd: "EXAVITQu4vr4xnSDxMaL",
Roger: "CwhRBWXzGAHq8TQ4Fs17",
},
modelId: "eleven_v3",
outputFormat: "mp3_44100_128",
apiKey: "elevenlabs_api_key",
silenceTimeoutMs: 1500,
interruptOnSpeech: true,
},
}

You can use voiceAliases to give your agents friendly names. The silenceTimeoutMs determines how long the system waits after you stop talking before it sends the transcript. If you don’t set it, the app uses platform defaults: 700 ms for macOS/Android and 900 ms for iOS.

Tools are how your agent interacts with the world. You can manage these through profiles, groups, and specific allow/deny lists.

A profile is the easiest way to set a base level of access. If you are setting up a new local config, it defaults to the coding profile.

ProfileIncludes
minimalsession_status only
codinggroup:fs, group:runtime, group:sessions, group:memory, image
messaginggroup:messaging, sessions_list, sessions_history, sessions_send, session_status
fullNo restriction (same as unset)

Groups make it easy to manage related tools at once:

GroupTools
group:runtimeexec, process (bash is accepted as an alias for exec)
group:fsread, write, edit, apply_patch
group:sessionssessions_list, sessions_history, sessions_send, sessions_spawn, session_status
group:memorymemory_search, memory_get
group:webweb_search, web_fetch
group:uibrowser, canvas
group:automationcron, gateway
group:messagingmessage
group:nodesnodes
group:openclawAll built-in tools (excludes provider plugins)

You can explicitly allow or deny tools using wildcards. If a tool is in the deny list, it will not run, even if the Docker sandbox is turned off.

{
tools: { deny: ["browser", "canvas"] },
}

Sometimes you want to restrict tools based on which model is being used. The system applies these in order: base profile → provider profile → allow/deny.

{
tools: {
profile: "coding",
byProvider: {
"google-antigravity": { profile: "minimal" },
"openai/gpt-5.2": { allow: ["group:fs", "sessions_list"] },
},
},
}

Elevated access allows the agent to run exec directly on your host machine, bypassing the sandbox. You should restrict this to specific users on platforms like WhatsApp or Discord.

{
tools: {
elevated: {
enabled: true,
allowFrom: {
whatsapp: ["+15555550123"],
discord: ["1234567890123", "987654321098765432"],
},
},
},
}

This handles how the agent runs commands. You can set timeouts and decide if you want notifications when a process finishes.

{
tools: {
exec: {
backgroundMs: 10000,
timeoutSec: 1800,
cleanupMs: 1800000,
notifyOnExit: true,
notifyOnExitEmptySuccess: false,
applyPatch: {
enabled: false,
allowModels: ["gpt-5.2"],
},
},
},
}

To prevent your agent from getting stuck in a loop, you can enable safety checks. These are disabled by default.

{
tools: {
loopDetection: {
enabled: true,
historySize: 30,
warningThreshold: 10,
criticalThreshold: 20,
globalCircuitBreakerThreshold: 30,
detectors: {
genericRepeat: true,
knownPollNoProgress: true,
pingPong: true,
},
},
},
}

These settings control how the agent searches the web and fetches content. You can set a custom User-Agent and manage cache times.

{
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",
},
},
},
}

This section configures how the agent understands images, audio, and video. You can use API providers or local CLI tools like Whisper.

{
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" }],
},
},
},
}
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-profiles.json 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.

This allows agents to talk to each other. You must explicitly enable it and list which agents are allowed to interact.

{
tools: {
agentToAgent: {
enabled: false,
allow: ["home", "work"],
},
},
}

You can control which sessions an agent can see or send messages to. The default is tree, which includes the current session and any subagents it spawned.

{
tools: {
sessions: {
// "self" | "tree" | "agent" | "all"
visibility: "tree",
},
},
}

If you want to allow subagents to receive file attachments when they are spawned, use these settings. This is an opt-in feature.

{
tools: {
sessions_spawn: {
attachments: {
enabled: false, // opt-in: set true to allow inline file attachments
maxTotalBytes: 5242880, // 5 MB total across all files
maxFiles: 50,
maxFileBytes: 1048576, // 1 MB per file
retainOnSessionKeep: false, // keep attachments when cleanup="keep"
},
},
},
}

Set the default behavior for subagents, including which model they use and how long they can run.

{
agents: {
defaults: {
subagents: {
model: "minimax/MiniMax-M2.5",
maxConcurrent: 1,
runTimeoutSeconds: 900,
archiveAfterMinutes: 60,
},
},
},
}

OpenClaw is flexible with where your models come from. You can add custom providers or change base URLs to point to proxies or local servers.

{
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,
},
],
},
},
},
}

If you use models.mode: "merge", your custom settings will be added to the existing catalog. Use replace if you want your config to be the only source of truth.

Cerebras (GLM 4.6 / 4.7)

{
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)" },
],
},
},
},
}

OpenCode

{
agents: {
defaults: {
model: { primary: "opencode/claude-opus-4-6" },
models: { "opencode/claude-opus-4-6": { alias: "Opus" } },
},
},
}

Z.AI (GLM-4.7)

{
agents: {
defaults: {
model: { primary: "zai/glm-4.7" },
models: { "zai/glm-4.7": {} },
},
},
}

Moonshot AI (Kimi)

{
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,
},
],
},
},
},
}

Kimi Coding

{
env: { KIMI_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "kimi-coding/k2p5" },
models: { "kimi-coding/k2p5": { alias: "Kimi K2.5" } },
},
},
}

Synthetic (Anthropic-compatible)

{
env: { SYNTHETIC_API_KEY: "sk-..." },
agents: {
defaults: {
model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.5" },
models: { "synthetic/hf:MiniMaxAI/MiniMax-M2.5": { alias: "MiniMax M2.5" } },
},
},
models: {
mode: "merge",
providers: {
synthetic: {
baseUrl: "https://api.synthetic.new/anthropic",
apiKey: "${SYNTHETIC_API_KEY}",
api: "anthropic-messages",
models: [
{
id: "hf:MiniMaxAI/MiniMax-M2.5",
name: "MiniMax M2.5",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 192000,
maxTokens: 65536,
},
],
},
},
},
}

MiniMax M2.5 (direct)

{
agents: {
defaults: {
model: { primary: "minimax/MiniMax-M2.5" },
models: {
"minimax/MiniMax-M2.5": { alias: "Minimax" },
},
},
},
models: {
mode: "merge",
providers: {
minimax: {
baseUrl: "https://api.minimax.io/anthropic",
apiKey: "${MINIMAX_API_KEY}",
api: "anthropic-messages",
models: [
{
id: "MiniMax-M2.5",
name: "MiniMax M2.5",
reasoning: false,
input: ["text"],
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
contextWindow: 200000,
maxTokens: 8192,
},
],
},
},
},
}
Local models (LM Studio)

See Local Models. TL;DR: run MiniMax M2.5 via LM Studio Responses API on serious hardware; keep hosted models merged for fallback.

AI Setup Assistant

You can manage how your agent handles specific tasks by configuring the skills block. This is where you control bundled tools and point the system to your own local scripts.

{
skills: {
allowBundled: ["gemini", "peekaboo"],
load: {
extraDirs: ["~/Projects/agent-scripts/skills"],
},
install: {
preferBrew: true,
nodeManager: "npm", // npm | pnpm | yarn
},
entries: {
"nano-banana-pro": {
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY" }, // or plaintext string
env: { GEMINI_API_KEY: "GEMINI_KEY_HERE" },
},
peekaboo: { enabled: true },
sag: { enabled: false },
},
},
}
  • allowBundled: This is an optional allowlist for bundled skills. Workspace skills are not affected by this setting.
  • entries.<skillKey>.enabled: false: Use this to disable a skill even if it is already bundled or installed.
  • entries.<skillKey>.apiKey: This is a convenience field for skills that need a primary environment variable. You can use a plaintext string or a SecretRef object.
  • load and install: These settings let you define extra directories for scripts and choose your preferred node manager, such as npm, pnpm, or yarn.

Plugins allow you to extend the core functionality. The system loads them from ~/.openclaw/extensions, <workspace>/.openclaw/extensions, and any custom paths you define in plugins.load.paths.

{
plugins: {
enabled: true,
allow: ["voice-call"],
deny: [],
load: {
paths: ["~/Projects/oss/voice-call-extension"],
},
entries: {
"voice-call": {
enabled: true,
hooks: {
allowPromptInjection: false,
},
config: { provider: "twilio" },
},
},
},
}

Keep in mind that config changes require a gateway restart.

  • allow: This is an optional allowlist where only listed plugins will load. Note that the deny list always wins.
  • plugins.entries.<id>.apiKey: A convenience field for plugin-level API keys when the plugin supports it.
  • plugins.entries.<id>.env: A map for plugin-scoped environment variables.
  • plugins.entries.<id>.hooks.allowPromptInjection: When set to false, the core blocks before_prompt_build and ignores prompt-mutating fields from legacy before_agent_start. It still keeps legacy modelOverride and providerOverride.
  • plugins.entries.<id>.config: This is a plugin-defined config object that the plugin schema validates.
  • plugins.slots.memory: Use this to pick the active memory plugin id, or set it to "none" to disable memory plugins.
  • plugins.slots.contextEngine: This selects the active context engine plugin. It defaults to "legacy" unless you install and select a different engine.
  • plugins.installs: This section contains CLI-managed metadata for openclaw plugins update, including versioning and installation paths.
  • Managed state: You should treat plugins.installs.* as managed state. It is better to use CLI commands than to edit these fields manually.

See Plugins.

The browser settings control how the agent interacts with web pages and handles network security.

{
browser: {
enabled: true,
evaluateEnabled: true,
defaultProfile: "chrome",
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: true, // default trusted-network mode
// allowPrivateNetwork: true, // legacy alias
// hostnameAllowlist: ["*.example.com", "example.com"],
// allowedHostnames: ["localhost"],
},
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,
// extraArgs: [],
// relayBindHost: "0.0.0.0", // only when the extension relay must be reachable across namespaces (for example WSL2)
// executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
// attachOnly: false,
},
}
  • evaluateEnabled: false: Disabling this will turn off act:evaluate and wait --fn.
  • ssrfPolicy.dangerouslyAllowPrivateNetwork: This defaults to true for a trusted-network model. Set it to false for strict public-only navigation.
  • ssrfPolicy.allowPrivateNetwork: This is supported as a legacy alias for the private network setting.
  • Strict mode exceptions: If you use strict mode, use ssrfPolicy.hostnameAllowlist and ssrfPolicy.allowedHostnames for explicit exceptions.
  • Remote profiles: These are attach-only, meaning start, stop, and reset functions are disabled.
  • Auto-detect order: The system looks for a default Chromium-based browser, then Chrome, Brave, Edge, Chromium, and finally Chrome Canary.
  • Control service: This uses loopback only. The port is derived from gateway.port, with a default of 18791.
  • extraArgs: You can use this to append extra launch flags like --disable-gpu or specific window sizes to the local Chromium startup.
  • relayBindHost: This changes the listening address for the Chrome extension relay. Leave it unset for loopback-only access. Only set it to 0.0.0.0 if the relay must cross a namespace boundary, like WSL2, and the network is trusted.

The ui block lets you customize the visual identity of the application.

{
ui: {
seamColor: "#FF4500",
assistant: {
name: "OpenClaw",
avatar: "CB", // emoji, short text, image URL, or data URI
},
},
}
  • seamColor: This is the accent color for the native app UI chrome, such as the Talk Mode bubble tint.
  • assistant: Use this to override the Control UI identity. If not set, it falls back to the active agent identity.

The Gateway is the core of your setup. It acts as the single entry point for both WebSocket and HTTP traffic. You can run it in local mode to host your own gateway or remote mode to connect to one running elsewhere.

{
gateway: {
mode: "local", // local | remote
port: 18789,
bind: "loopback",
auth: {
mode: "token", // none | token | password | trusted-proxy
token: "your-token",
// password: "your-password", // or OPENCLAW_GATEWAY_PASSWORD
// trustedProxy: { userHeader: "x-forwarded-user" }, // for mode=trusted-proxy; see /gateway/trusted-proxy-auth
allowTailscale: true,
rateLimit: {
maxAttempts: 10,
windowMs: 60000,
lockoutMs: 300000,
exemptLoopback: true,
},
},
tailscale: {
mode: "off", // off | serve | funnel
resetOnExit: false,
},
controlUi: {
enabled: true,
basePath: "/openclaw",
// root: "dist/control-ui",
// allowedOrigins: ["https://control.example.com"], // required for non-loopback Control UI
// dangerouslyAllowHostHeaderOriginFallback: false, // dangerous Host-header origin fallback mode
// 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"],
// Optional. Default false.
allowRealIpFallback: false,
tools: {
// Additional /tools/invoke HTTP denies
deny: ["browser"],
// Remove tools from the default HTTP deny list
allow: ["gateway"],
},
},
}

When you configure the port, keep in mind it handles both WS and HTTP. The precedence for setting this is --port flags first, then the OPENCLAW_GATEWAY_PORT environment variable, followed by gateway.port in your config, and finally the default 18789.

For the bind setting, you have a few options: auto, loopback (the default), lan (0.0.0.0), tailnet (Tailscale IP only), or custom. If you are using Docker, the default loopback listens on 127.0.0.1 inside the container. This means if you use Docker bridge networking with -p 18789:18789, the traffic hitting eth0 won’t reach the gateway. You should use --network host or set bind: "lan" to fix this.

Authentication is required by default for any non-loopback binds. You can use a token, a password, or delegate to a Trusted Proxy Auth. If you set both a token and a password, you must explicitly set gateway.auth.mode to avoid startup failures. For local testing where you trust the loopback, you can set mode: "none".

If you use Tailscale, gateway.auth.allowTailscale lets Tailscale Serve identity headers handle auth for the Control UI and WebSockets. This is verified via tailscale whois. However, standard HTTP API endpoints still need a token or password.

You can also enable OpenAI-compatible endpoints if you need them. These are off by default, but you can turn on Chat Completions or the Responses API in your config.

If you need to run multiple gateways on a single host, you can isolate them by using unique ports and state directories.

Terminal window
OPENCLAW_CONFIG_PATH=~/.openclaw/a.json \
OPENCLAW_STATE_DIR=~/.openclaw-a \
openclaw gateway --port 19001

You can use the --dev flag to quickly use ~/.openclaw-dev on port 19001, or use --profile <name> for other setups. Check out Multiple Gateways for more details.

Hooks provide a way to trigger agent actions through HTTP POST requests. This is useful for connecting external services like Gmail or custom webhooks to your agents.

{
hooks: {
enabled: true,
token: "shared-secret",
path: "/hooks",
maxBodyBytes: 262144,
defaultSessionKey: "hook:ingress",
allowRequestSessionKey: false,
allowedSessionKeyPrefixes: ["hook:"],
allowedAgentIds: ["hooks", "main"],
presets: ["gmail"],
transformsDir: "~/.openclaw/hooks/transforms",
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",
},
],
},
}

To authenticate your hook requests, use Authorization: Bearer <token> or the x-openclaw-token: <token> header.

The available endpoints include:

  • POST /hooks/wake: Wakes up the system with specific text.
  • POST /hooks/agent: Sends a message directly to an agent.
  • POST /hooks/<name>: Uses the logic defined in your hooks.mappings.

Mappings allow you to route requests based on the URL path or payload fields. You can use templates like {{messages[0].subject}} to extract data from the incoming JSON. If you need complex logic, you can point a transform to a JS/TS module in your transformsDir.

The Gmail preset helps you watch for new emails and process them with an agent.

{
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",
},
},
}

The Gateway automatically starts the Gmail watcher on boot. You should not run a separate gog gmail watch serve command if this is configured.

The Canvas host serves web content that agents can edit, including HTML, CSS, JS, and A2UI. It runs on the same port as your Gateway.

{
canvasHost: {
root: "~/.openclaw/workspace/canvas",
liveReload: true,
// enabled: false, // or OPENCLAW_SKIP_CANVAS_HOST=1
},
}

You can access these surfaces at:

  • http://<gateway-host>:<gateway.port>/__openclaw__/canvas/
  • http://<gateway-host>:<gateway.port>/__openclaw__/a2ui/

If you aren’t using a loopback bind, these routes require the same authentication as the rest of the Gateway. For Node WebViews that can’t easily send auth headers, the Gateway provides temporary capability URLs that are tied to the active session.

The host includes a live-reload client and will create a starter index.html if the directory is empty. If you are working with very large directories and see EMFILE errors, you might want to disable live reload.

Discovery settings control how your OpenClaw instances find each other on the network.

This is used for local network discovery.

{
discovery: {
mdns: {
mode: "minimal", // minimal | full | off
},
},
}

The minimal mode is the default and hides the cliPath and sshPort from the TXT records. If you need that info shared, switch to full. You can change the default openclaw hostname by setting OPENCLAW_MDNS_HOSTNAME.

For discovery across different networks, you can use Wide-area DNS-SD.

{
discovery: {
wideArea: { enabled: true },
},
}

This writes a DNS-SD zone to ~/.openclaw/dns/. To get this working across networks, you should pair it with a DNS server like CoreDNS and use Tailscale split DNS. You can set this up by running openclaw dns setup --apply.

Managing your environment variables shouldn’t be a headache. You can set them directly in your config using the env key.

{
env: {
OPENROUTER_API_KEY: "sk-or-...",
vars: {
GROQ_API_KEY: "gsk-...",
},
shellEnv: {
enabled: true,
timeoutMs: 15000,
},
},
}

These inline variables only kick in if the key is missing from your process environment. For local files, the system looks at the .env in your current directory and ~/.openclaw/.env, but neither will override what is already there. If you use shellEnv, it pulls in missing keys from your login shell profile. Check out Environment for the full precedence rules.

You can reference environment variables in any config string with ${VAR_NAME}:

{
gateway: {
auth: { token: "${OPENCLAW_GATEWAY_TOKEN}" },
},
}

Keep in mind that only uppercase names like [A-Z_][A-Z0-9_]* are matched. If a variable is missing or empty, the config loader throws an error. If you need a literal ${VAR}, escape it as $${VAR}. This also works with $include.

Handling sensitive data requires a bit more care. Secret references are additive, so your plaintext values still work fine.

When you need to point to a secret, use this object shape:

{ source: "env" | "file" | "exec", provider: "default", id: "..." }

There are a few validation rules you need to follow:

  • The provider pattern must match ^[a-z][a-z0-9_-]{0,63}$.
  • For source: "env", the ID follows ^[A-Z][A-Z0-9_]{0,127}$.
  • If you use source: "file", the ID is an absolute JSON pointer, like "/providers/openai/apiKey".
  • For source: "exec", the ID matches ^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$.
  • Exec IDs cannot contain . or .. path segments, so something like a/../b will be rejected.

You can find the full list of where these work in the SecretRef Credential Surface. The secrets apply command targets supported paths in openclaw.json. Your auth-profiles.json references are also part of the runtime resolution and audit coverage.

Here is how you set up your providers:

{
secrets: {
providers: {
default: { source: "env" }, // optional explicit env provider
filemain: {
source: "file",
path: "~/.openclaw/secrets.json",
mode: "json",
timeoutMs: 5000,
},
vault: {
source: "exec",
command: "/usr/local/bin/openclaw-vault-resolver",
passEnv: ["PATH", "VAULT_ADDR"],
},
},
defaults: {
env: "default",
file: "filemain",
exec: "vault",
},
},
}

The file provider works with mode: "json" or mode: "singleValue". If you use singleValue, the ID must be "value". For the exec provider, you need an absolute command path. It uses protocol payloads on stdin and stdout.

By default, symlink command paths are blocked. You can set allowSymlinkCommand: true to allow them, but the system still validates the final resolved target path. If you have trustedDirs set up, that check applies to the resolved path too. The environment for exec is kept minimal, so use passEnv to send specific variables.

Secrets are resolved when you activate the config and stored in an in-memory snapshot. Requests then read from that snapshot only. If a reference on an active surface cannot be resolved, startup or reload will fail. Inactive surfaces are skipped, though you will still see diagnostics for them.

Managing your authentication doesn’t have to be a mess. You can organize your credentials using profiles for different providers or accounts. Here is how that configuration looks:

{
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"],
},
},
}

You can find your per-agent profiles stored at <agentDir>/auth-profiles.json. This file is flexible because it supports value-level refs, specifically keyRef for your api_key or tokenRef for your token.

When the system runs, static runtime credentials come from in-memory resolved snapshots. If the system discovers legacy static auth.json entries, it scrubs them automatically. If you are moving from an older setup, legacy OAuth imports are handled from ~/.openclaw/credentials/oauth.json.

You can find more details in the OAuth documentation. For information on secrets runtime behavior and tools like audit, configure, apply, and related management, check out Secrets Management.

Keeping track of what your agent is doing is vital for debugging. You can control the verbosity and format of your logs with these settings:

{
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"],
},
}

The default log file is usually located at /tmp/openclaw/openclaw-YYYY-MM-DD.log. You should set logging.file if you want a stable path that doesn’t change every day. One handy tip: your consoleLevel automatically bumps to debug whenever you use the --verbose flag.

{
cli: {
banner: {
taglineMode: "off", // random | default | off
},
},
}

You can adjust the CLI banner to fit your style. The cli.banner.taglineMode setting handles the text shown below the version info.

The "random" setting is the default and shows funny or seasonal taglines. You can also use "default" for a fixed neutral message: All your chats, one OpenClaw.. If you prefer a cleaner look, use "off" to hide the tagline text while keeping the banner title and version. To hide the entire banner, just set the OPENCLAW_HIDE_BANNER=1 environment variable.

Metadata is automatically written by CLI wizards like onboard, configure, doctor, and other setup tools. This helps the system keep track of your configuration history.

{
wizard: {
lastRunAt: "2026-01-01T00:00:00.000Z",
lastRunVersion: "2026.1.4",
lastRunCommit: "abc1234",
lastRunCommand: "configure",
lastRunMode: "local",
},
}

This block records the lastRunAt timestamp, the lastRunVersion, the lastRunCommit, and the lastRunCommand. It also stores the lastRunMode to help you identify how the tool was executed during its last run.

You define your agent’s personality in the identity section. This part of the configuration is usually handled by the macOS onboarding assistant, which sets up the basics for you.

{
agents: {
list: [
{
id: "main",
identity: {
name: "Samantha",
theme: "helpful sloth",
emoji: "🦥",
avatar: "avatars/samantha.png",
},
},
],
},
}

The system uses these values to derive other settings automatically. Your messages.ackReaction is pulled directly from identity.emoji, though it uses 👀 if that is missing. Similarly, mentionPatterns are built using your identity.name and identity.emoji. When setting up your avatar, you can use a workspace-relative path, a full http(s) URL, or a data: URI.

The TCP bridge is no longer part of current builds. Nodes now connect over the Gateway WebSocket instead.

Because of this change, bridge.* keys are no longer in the config schema. If you leave them in, your config validation will fail until they are removed. You can use openclaw doctor --fix to automatically strip these unknown keys from your file.

Legacy bridge config (historical reference)

{
"bridge": {
"enabled": true,
"port": 18790,
"bind": "tailnet",
"tls": {
"enabled": true,
"autoGenerate": true
}
}
}

If you want to automate your workflows, the cron section is where you define how background jobs behave. You can control how long the system remembers finished tasks and how it handles logging.

{
cron: {
enabled: true,
maxConcurrentRuns: 2,
webhook: "https://example.invalid/legacy", // deprecated fallback for stored notify:true jobs
webhookToken: "replace-with-dedicated-token", // optional bearer token for outbound webhook auth
sessionRetention: "24h", // duration string or false
runLog: {
maxBytes: "2mb", // default 2_000_000 bytes
keepLines: 2000, // default 2000
},
},
}

Here is the breakdown of these settings:

  • sessionRetention: This tells the system how long to keep completed cron run sessions before they are pruned from sessions.json. It also manages the cleanup of archived transcripts. The default is 24h, but you can set it to false to turn off pruning.
  • runLog.maxBytes: This sets the maximum size for each run log file (cron/runs/<jobId>.jsonl). Once it hits this limit, pruning kicks in. The default is 2_000_000 bytes.
  • runLog.keepLines: When a log is pruned, this is the number of newest lines that will be kept. It defaults to 2000.
  • webhookToken: If you are using delivery.mode = "webhook", this bearer token is used for outbound POST requests. If you don’t provide one, no auth header is sent.
  • webhook: This is a legacy fallback URL for stored jobs that still have notify: true. It is deprecated, so you should avoid relying on it for new setups.

See Cron Jobs.

When you are working with tools.media.models[].args, you can use template placeholders to pull in specific data. These variables expand automatically to give your models the context they need:

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

You don’t have to keep your entire configuration in one giant file. You can split it into smaller, manageable pieces using the $include key:

~/.openclaw/openclaw.json
{
gateway: { port: 18789 },
agents: { $include: "./agents.json5" },
broadcast: {
$include: ["./clients/mueller.json5", "./clients/schmidt.json5"],
},
}

Here is how the merging works when you use includes:

  • If you include a single file, it replaces the object that contains the $include key.
  • If you use an array of files, they are deep-merged in order. This means files listed later will override values from earlier ones.
  • Sibling keys (keys at the same level as $include) are merged last, so they will override any values brought in by the includes.
  • You can nest includes up to 10 levels deep if you need a complex structure.
  • Paths are resolved relative to the file that calls them. They must stay inside the top-level config directory. You can use absolute paths or ../, but only if the final path is still within that boundary.
  • If there is a missing file, a parse error, or a circular include, the system will give you a clear error message.

Related: Configuration · Configuration Examples · Doctor

OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.