38 KiB
summary, read_when
| summary | read_when | |
|---|---|---|
| All configuration options for ~/.clawdbot/clawdbot.json with examples |
|
Configuration 🔧
CLAWDBOT reads an optional JSON5 config from ~/.clawdbot/clawdbot.json (comments + trailing commas allowed).
If the file is missing, CLAWDBOT uses safe-ish defaults (embedded Pi agent + per-sender sessions + workspace ~/clawd). You usually only need a config to:
- restrict who can trigger the bot (
whatsapp.allowFrom,telegram.allowFrom, etc.) - control group mention behavior (
whatsapp.groups,telegram.groups,discord.guilds,routing.groupChat) - customize message prefixes (
messages) - set the agent's workspace (
agent.workspace) - tune the embedded agent (
agent) and session behavior (session) - set the agent's identity (
identity)
Schema + UI hints
The Gateway exposes a JSON Schema representation of the config via config.schema for UI editors.
The Control UI renders a form from this schema, with a Raw JSON editor as an escape hatch.
Hints (labels, grouping, sensitive fields) ship alongside the schema so clients can render better forms without hard-coding config knowledge.
Minimal config (recommended starting point)
{
agent: { workspace: "~/clawd" },
whatsapp: { allowFrom: ["+15555550123"] }
}
Build the default image once with:
scripts/sandbox-setup.sh
Self-chat mode (recommended for group control)
To prevent the bot from responding to WhatsApp @-mentions in groups (only respond to specific text triggers):
{
agent: { workspace: "~/clawd" },
whatsapp: {
// Allowlist is DMs only; including your own number enables self-chat mode.
allowFrom: ["+15555550123"],
groups: { "*": { requireMention: true } }
},
routing: {
groupChat: {
mentionPatterns: ["@clawd", "reisponde"]
}
}
}
Common options
Env vars + .env
CLAWDBOT reads env vars from the parent process (shell, launchd/systemd, CI, etc.).
Additionally, it loads:
.envfrom the current working directory (if present)- a global fallback
.envfrom~/.clawdbot/.env(aka$CLAWDBOT_STATE_DIR/.env)
Neither .env file overrides existing env vars.
env.shellEnv (optional)
Opt-in convenience: if enabled and none of the expected keys are set yet, CLAWDBOT runs your login shell and imports only the missing expected keys (never overrides). This effectively sources your shell profile.
{
env: {
shellEnv: {
enabled: true,
timeoutMs: 15000
}
}
}
Env var equivalent:
CLAWDBOT_LOAD_SHELL_ENV=1CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000
Auth storage (OAuth + API keys)
Clawdbot stores OAuth credentials in:
~/.clawdbot/credentials/oauth.json(or$CLAWDBOT_STATE_DIR/credentials/oauth.json)
Clawdbot stores API keys in the agent auth store:
~/.clawdbot/agent/auth.json
Overrides:
- OAuth dir:
CLAWDBOT_OAUTH_DIR - Agent dir:
CLAWDBOT_AGENT_DIR(preferred),PI_CODING_AGENT_DIR(legacy)
On first use, Clawdbot imports oauth.json entries into auth.json so the embedded
agent can use them. oauth.json remains the source of truth for OAuth refresh.
identity
Optional agent identity used for defaults and UX. This is written by the macOS onboarding assistant.
If set, CLAWDBOT derives defaults (only when you haven’t set them explicitly):
messages.responsePrefixfromidentity.emojirouting.groupChat.mentionPatternsfromidentity.name(so “@Samantha” works in groups)
{
identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" }
}
wizard
Metadata written by CLI wizards (onboard, configure, doctor, update).
{
wizard: {
lastRunAt: "2026-01-01T00:00:00.000Z",
lastRunVersion: "2026.1.4",
lastRunCommit: "abc1234",
lastRunCommand: "configure",
lastRunMode: "local"
}
}
logging
- Default log file:
/tmp/clawdbot/clawdbot-YYYY-MM-DD.log - If you want a stable path, set
logging.fileto/tmp/clawdbot/clawdbot.log. - Console output can be tuned separately via:
logging.consoleLevel(defaults toinfo, bumps todebugwhen--verbose)logging.consoleStyle(pretty|compact|json)
{
logging: {
level: "info",
file: "/tmp/clawdbot/clawdbot.log",
consoleLevel: "info",
consoleStyle: "pretty"
}
}
whatsapp.allowFrom
Allowlist of E.164 phone numbers that may trigger WhatsApp auto-replies (DMs only). If empty, the default allowlist is your own WhatsApp number (self-chat mode).
{
whatsapp: {
allowFrom: ["+15555550123", "+447700900123"],
textChunkLimit: 4000 // optional outbound chunk size (chars)
}
}
routing.groupChat
Group messages default to require mention (either metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, and iMessage group chats.
Mention types:
- Metadata mentions: Native platform @-mentions (e.g., WhatsApp tap-to-mention). Ignored in WhatsApp self-chat mode (see
whatsapp.allowFrom). - Text patterns: Regex patterns defined in
mentionPatterns. Always checked regardless of self-chat mode.
{
routing: {
groupChat: {
mentionPatterns: ["@clawd", "clawdbot", "clawd"],
historyLimit: 50
}
}
}
Mention gating defaults live per provider (whatsapp.groups, telegram.groups, imessage.groups, discord.guilds).
To respond only to specific text triggers (ignoring native @-mentions):
{
whatsapp: {
// Include your own number to enable self-chat mode (ignore native @-mentions).
allowFrom: ["+15555550123"],
groups: { "*": { requireMention: true } }
},
routing: {
groupChat: {
// Only these text patterns will trigger responses
mentionPatterns: ["reisponde", "@clawd"]
}
}
}
routing.queue
Controls how inbound messages behave when an agent run is already active.
{
routing: {
queue: {
mode: "collect", // steer | followup | collect | steer-backlog (steer+backlog ok) | interrupt (queue=steer legacy)
debounceMs: 1000,
cap: 20,
drop: "summarize", // old | new | summarize
bySurface: {
whatsapp: "collect",
telegram: "collect",
discord: "collect",
imessage: "collect",
webchat: "collect"
}
}
}
}
web (WhatsApp web provider)
WhatsApp runs through the gateway’s web provider. It starts automatically when a linked session exists.
Set web.enabled: false to keep it off by default.
{
web: {
enabled: true,
heartbeatSeconds: 60,
reconnect: {
initialMs: 2000,
maxMs: 120000,
factor: 1.4,
jitter: 0.2,
maxAttempts: 0
}
}
}
telegram (bot transport)
Clawdbot starts Telegram only when a telegram config section exists. The bot token is resolved from TELEGRAM_BOT_TOKEN or telegram.botToken.
Set telegram.enabled: false to disable automatic startup.
{
telegram: {
enabled: true,
botToken: "your-bot-token",
requireMention: true,
allowFrom: ["123456789"],
mediaMaxMb: 5,
proxy: "socks5://localhost:9050",
webhookUrl: "https://example.com/telegram-webhook",
webhookSecret: "secret",
webhookPath: "/telegram-webhook"
}
}
discord (bot transport)
Configure the Discord bot by setting the bot token and optional gating:
{
discord: {
enabled: true,
token: "your-bot-token",
mediaMaxMb: 8, // clamp inbound media size
actions: { // tool action gates (false disables)
reactions: true,
stickers: true,
polls: true,
permissions: true,
messages: true,
threads: true,
pins: true,
search: true,
memberInfo: true,
roleInfo: true,
roles: false,
channelInfo: true,
voiceStatus: true,
events: true,
moderation: false
},
replyToMode: "off", // off | first | all
slashCommand: { // user-installed app slash commands
enabled: true,
name: "clawd",
sessionPrefix: "discord:slash",
ephemeral: true
},
dm: {
enabled: true, // disable all DMs when false
allowFrom: ["1234567890", "steipete"], // optional DM allowlist (ids or names)
groupEnabled: false, // enable group DMs
groupChannels: ["clawd-dm"] // optional group DM allowlist
},
guilds: {
"123456789012345678": { // guild id (preferred) or slug
slug: "friends-of-clawd",
requireMention: false, // per-guild default
reactionNotifications: "own", // off | own | all | allowlist
users: ["987654321098765432"], // optional per-guild user allowlist
channels: {
general: { allow: true },
help: { allow: true, requireMention: true }
}
}
},
historyLimit: 20 // include last N guild messages as context
}
}
Clawdbot starts Discord only when a discord config section exists. The token is resolved from DISCORD_BOT_TOKEN or discord.token (unless discord.enabled is false). Use user:<id> (DM) or channel:<id> (guild channel) when specifying delivery targets for cron/CLI commands.
Guild slugs are lowercase with spaces replaced by -; channel keys use the slugged channel name (no leading #). Prefer guild ids as keys to avoid rename ambiguity.
Reaction notification modes:
off: no reaction events.own: reactions on the bot's own messages (default).all: all reactions on all messages.allowlist: reactions fromguilds.<id>.userson all messages (empty list disables).
slack (socket mode)
Slack runs in Socket Mode and requires both a bot token and app token:
{
slack: {
enabled: true,
botToken: "xoxb-...",
appToken: "xapp-...",
dm: {
enabled: true,
allowFrom: ["U123", "U456", "*"],
groupEnabled: false,
groupChannels: ["G123"]
},
channels: {
C123: { allow: true, requireMention: true },
"#general": { allow: true, requireMention: false }
},
reactionNotifications: "own", // off | own | all | allowlist
reactionAllowlist: ["U123"],
actions: {
reactions: true,
messages: true,
pins: true,
memberInfo: true,
emojiList: true
},
slashCommand: {
enabled: true,
name: "clawd",
sessionPrefix: "slack:slash",
ephemeral: true
},
textChunkLimit: 4000,
mediaMaxMb: 20
}
}
Clawdbot starts Slack when the provider is enabled and both tokens are set (via config or SLACK_BOT_TOKEN + SLACK_APP_TOKEN). Use user:<id> (DM) or channel:<id> when specifying delivery targets for cron/CLI commands.
Reaction notification modes:
off: no reaction events.own: reactions on the bot's own messages (default).all: all reactions on all messages.allowlist: reactions fromslack.reactionAllowliston all messages (empty list disables).
Slack action groups (gate slack tool actions):
| Action group | Default | Notes |
|---|---|---|
| reactions | enabled | React + list reactions |
| messages | enabled | Read/send/edit/delete |
| pins | enabled | Pin/unpin/list |
| memberInfo | enabled | Member info |
| emojiList | enabled | Custom emoji list |
imessage (imsg CLI)
Clawdbot spawns imsg rpc (JSON-RPC over stdio). No daemon or port required.
{
imessage: {
enabled: true,
cliPath: "imsg",
dbPath: "~/Library/Messages/chat.db",
allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
includeAttachments: false,
mediaMaxMb: 16,
service: "auto",
region: "US"
}
}
Notes:
- Requires Full Disk Access to the Messages DB.
- The first send will prompt for Messages automation permission.
- Prefer
chat_id:<id>targets. Useimsg chats --limit 20to list chats.
agent.workspace
Sets the single global workspace directory used by the agent for file operations.
Default: ~/clawd.
{
agent: { workspace: "~/clawd" }
}
If agent.sandbox is enabled, non-main sessions can override this with their
own per-session workspaces under agent.sandbox.workspaceRoot.
agent.userTimezone
Sets the user’s timezone for system prompt context (not for timestamps in message envelopes). If unset, Clawdbot uses the host timezone at runtime.
{
agent: { userTimezone: "America/Chicago" }
}
messages
Controls inbound/outbound prefixes.
{
messages: {
messagePrefix: "[clawdbot]",
responsePrefix: "🦞"
}
}
responsePrefix is applied to all outbound replies (tool summaries, block
streaming, final replies) across providers unless already present.
talk
Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to ELEVENLABS_VOICE_ID or SAG_VOICE_ID when unset.
apiKey falls back to ELEVENLABS_API_KEY (or the gateway’s shell profile) when unset.
voiceAliases lets Talk directives use friendly names (e.g. "voice":"Clawd").
{
talk: {
voiceId: "elevenlabs_voice_id",
voiceAliases: {
Clawd: "EXAVITQu4vr4xnSDxMaL",
Roger: "CwhRBWXzGAHq8TQ4Fs17"
},
modelId: "eleven_v3",
outputFormat: "mp3_44100_128",
apiKey: "elevenlabs_api_key",
interruptOnSpeech: true
}
}
agent
Controls the embedded agent runtime (model/thinking/verbose/timeouts).
allowedModels lets /model list/filter and enforce a per-session allowlist
(omit to show the full catalog).
modelAliases adds short names for /model (alias -> provider/model).
modelFallbacks lists ordered fallback models to try when the default fails.
imageModel selects an image-capable model for the image tool.
imageModelFallbacks lists ordered fallback image models for the image tool.
Clawdbot also ships a few built-in modelAliases shorthands (when an agent section exists):
opus->anthropic/claude-opus-4-5sonnet->anthropic/claude-sonnet-4-5gpt->openai/gpt-5.2gpt-mini->openai/gpt-5-minigemini->google/gemini-3-pro-previewgemini-flash->google/gemini-3-flash-preview
If you configure the same alias name (case-insensitive) yourself, your value wins (defaults never override).
{
agent: {
model: "anthropic/claude-opus-4-5",
allowedModels: [
"anthropic/claude-opus-4-5",
"anthropic/claude-sonnet-4-1"
],
modelAliases: {
Opus: "anthropic/claude-opus-4-5",
Sonnet: "anthropic/claude-sonnet-4-1"
},
modelFallbacks: [
"openrouter/deepseek/deepseek-r1:free",
"openrouter/meta-llama/llama-3.3-70b-instruct:free"
],
imageModel: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
imageModelFallbacks: [
"openrouter/google/gemini-2.0-flash-vision:free"
],
thinkingDefault: "low",
verboseDefault: "off",
elevatedDefault: "on",
timeoutSeconds: 600,
mediaMaxMb: 5,
heartbeat: {
every: "30m",
target: "last"
},
maxConcurrent: 3,
bash: {
backgroundMs: 10000,
timeoutSec: 1800,
cleanupMs: 1800000
},
contextTokens: 200000
}
}
Block streaming:
agent.blockStreamingDefault:"on"/"off"(default on).agent.blockStreamingBreak:"text_end"or"message_end"(default: text_end).agent.blockStreamingChunk: soft chunking for streamed blocks. Defaults to 800–1200 chars, prefers paragraph breaks (\n\n), then newlines, then sentences. Example:{ agent: { blockStreamingChunk: { minChars: 800, maxChars: 1200 } } }
agent.model should be set as provider/model (e.g. anthropic/claude-opus-4-5).
If modelAliases is configured, you may also use the alias key (e.g. Opus).
If you omit the provider, CLAWDBOT currently assumes anthropic as a temporary
deprecation fallback.
Z.AI models are available as zai/<model> (e.g. zai/glm-4.7) and require
ZAI_API_KEY (or legacy Z_AI_API_KEY) in the environment.
agent.heartbeat configures periodic heartbeat runs:
every: duration string (ms,s,m,h); default unit minutes. Omit or set0mto disable.model: optional override model for heartbeat runs (provider/model).target: optional delivery channel (last,whatsapp,telegram,discord,imessage,none). Default:last.to: optional recipient override (E.164 for WhatsApp, chat id for Telegram).prompt: optional override for the heartbeat body (default:HEARTBEAT).ackMaxChars: max chars allowed afterHEARTBEAT_OKbefore delivery (default: 30).
agent.bash configures background bash defaults:
backgroundMs: time before auto-background (ms, default 10000)timeoutSec: auto-kill after this runtime (seconds, default 1800)cleanupMs: how long to keep finished sessions in memory (ms, default 1800000)
agent.tools configures a global tool allow/deny policy (deny wins).
This is applied even when the Docker sandbox is off.
Example (disable browser/canvas everywhere):
{
agent: {
tools: {
deny: ["browser", "canvas"]
}
}
}
agent.elevated controls elevated (host) bash access:
enabled: allow elevated mode (default true)allowFrom: per-surface allowlists (empty = disabled)whatsapp: E.164 numberstelegram: chat ids or usernamesdiscord: user ids or usernames (falls back todiscord.dm.allowFromif omitted)signal: E.164 numbersimessage: handles/chat idswebchat: session ids or usernames
Example:
{
agent: {
elevated: {
enabled: true,
allowFrom: {
whatsapp: ["+15555550123"],
discord: ["steipete", "1234567890123"]
}
}
}
}
agent.maxConcurrent sets the maximum number of embedded agent runs that can
execute in parallel across sessions. Each session is still serialized (one run
per session key at a time). Default: 1.
agent.sandbox
Optional per-session Docker sandboxing for the embedded agent. Intended for non-main sessions so they cannot access your host system.
Defaults (if enabled):
- one container per session
- Debian bookworm-slim based image
- workspace per session under
~/.clawdbot/sandboxes - auto-prune: idle > 24h OR age > 7d
- tools: allow only
bash,process,read,write,edit(deny wins) - optional sandboxed browser (Chromium + CDP, noVNC observer)
- hardening knobs:
network,user,pidsLimit,memory,cpus,ulimits,seccompProfile,apparmorProfile
{
agent: {
sandbox: {
mode: "non-main", // off | non-main | all
perSession: true,
workspaceRoot: "~/.clawdbot/sandboxes",
docker: {
image: "clawdbot-sandbox:bookworm-slim",
containerPrefix: "clawdbot-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: "clawdbot-sandbox",
dns: ["1.1.1.1", "8.8.8.8"],
extraHosts: ["internal.service:10.0.0.5"]
},
browser: {
enabled: false,
image: "clawdbot-sandbox-browser:bookworm-slim",
containerPrefix: "clawdbot-sbx-browser-",
cdpPort: 9222,
vncPort: 5900,
noVncPort: 6080,
headless: false,
enableNoVnc: true
},
tools: {
allow: ["bash", "process", "read", "write", "edit"],
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
},
prune: {
idleHours: 24, // 0 disables idle pruning
maxAgeDays: 7 // 0 disables max-age pruning
}
}
}
}
Build the default sandbox image once with:
scripts/sandbox-setup.sh
Note: sandbox containers default to network: "none"; set agent.sandbox.docker.network
to "bridge" (or your custom network) if the agent needs outbound access.
Build the optional browser image with:
scripts/sandbox-browser-setup.sh
When agent.sandbox.browser.enabled=true, the browser tool uses a sandboxed
Chromium instance (CDP). If noVNC is enabled (default when headless=false),
the noVNC URL is injected into the system prompt so the agent can reference it.
This does not require browser.enabled in the main config; the sandbox control
URL is injected per session.
models (custom providers + base URLs)
Clawdbot uses the pi-coding-agent model catalog. You can add custom providers
(LiteLLM, local OpenAI-compatible servers, Anthropic proxies, etc.) by writing
~/.clawdbot/agent/models.json or by defining the same schema inside your
Clawdbot config under models.providers.
When models.providers is present, Clawdbot writes/merges a models.json into
~/.clawdbot/agent/ on startup:
- default behavior: merge (keeps existing providers, overrides on name)
- set
models.mode: "replace"to overwrite the file contents
Select the model via agent.model (provider/model).
{
agent: { model: "custom-proxy/llama-3.1-8b" },
models: {
mode: "merge",
providers: {
"custom-proxy": {
baseUrl: "http://localhost:4000/v1",
apiKey: "LITELLM_KEY",
api: "openai-completions",
models: [
{
id: "llama-3.1-8b",
name: "Llama 3.1 8B",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 32000
}
]
}
}
}
}
Local models (LM Studio) — recommended setup
Best current local setup (what we’re running): MiniMax M2.1 on a beefy Mac Studio via LM Studio using the Responses API.
{
agent: {
model: "Minimax",
allowedModels: [
"anthropic/claude-opus-4-5",
"lmstudio/minimax-m2.1-gs32"
],
modelAliases: {
Opus: "anthropic/claude-opus-4-5",
Minimax: "lmstudio/minimax-m2.1-gs32"
}
},
models: {
mode: "merge",
providers: {
lmstudio: {
baseUrl: "http://127.0.0.1:1234/v1",
apiKey: "lmstudio",
api: "openai-responses",
models: [
{
id: "minimax-m2.1-gs32",
name: "MiniMax M2.1 GS32",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 196608,
maxTokens: 8192
}
]
}
}
}
}
Notes:
- LM Studio must have the model loaded and the local server enabled (default URL above).
- Responses API enables clean reasoning/output separation; WhatsApp sees only final text.
- Adjust
contextWindow/maxTokensif your LM Studio context length differs.
Notes:
- Supported APIs:
openai-completions,openai-responses,anthropic-messages,google-generative-ai - Use
authHeader: true+headersfor custom auth needs. - Override the agent config root with
CLAWDBOT_AGENT_DIR(orPI_CODING_AGENT_DIR) if you wantmodels.jsonstored elsewhere.
session
Controls session scoping, idle expiry, reset triggers, and where the session store is written.
{
session: {
scope: "per-sender",
idleMinutes: 60,
resetTriggers: ["/new", "/reset"],
store: "~/.clawdbot/sessions/sessions.json",
// mainKey is ignored; primary key is fixed to "main"
agentToAgent: {
// Max ping-pong reply turns between requester/target (0–5).
maxPingPongTurns: 5
},
sendPolicy: {
rules: [
{ action: "deny", match: { surface: "discord", chatType: "group" } }
],
default: "allow"
}
}
}
Fields:
agentToAgent.maxPingPongTurns: max reply-back turns between requester/target (0–5, default 5).sendPolicy.default:allowordenyfallback when no rule matches.sendPolicy.rules[]: match bysurface(provider),chatType(direct|group|room), orkeyPrefix(e.g.cron:). First deny wins; otherwise allow.
skills (skills config)
Controls bundled allowlist, install preferences, extra skill folders, and per-skill
overrides. Applies to bundled skills and ~/.clawdbot/skills (workspace skills
still win on name conflicts).
Fields:
allowBundled: optional allowlist for bundled skills only. If set, only those bundled skills are eligible (managed/workspace skills unaffected).load.extraDirs: additional skill directories to scan (lowest precedence).install.preferBrew: prefer brew installers when available (default: true).install.nodeManager: node installer preference (npm|pnpm|yarn, default: npm).entries.<skillKey>: per-skill config overrides.
Per-skill fields:
enabled: setfalseto disable a skill even if it’s bundled/installed.env: environment variables injected for the agent run (only if not already set).apiKey: optional convenience for skills that declare a primary env var (e.g.nano-banana-pro→GEMINI_API_KEY).
Example:
{
skills: {
allowBundled: ["brave-search", "gemini"],
load: {
extraDirs: [
"~/Projects/agent-scripts/skills",
"~/Projects/oss/some-skill-pack/skills"
]
},
install: {
preferBrew: true,
nodeManager: "npm"
},
entries: {
"nano-banana-pro": {
apiKey: "GEMINI_KEY_HERE",
env: {
GEMINI_API_KEY: "GEMINI_KEY_HERE"
}
},
peekaboo: { enabled: true },
sag: { enabled: false }
}
}
}
browser (clawd-managed Chrome)
Clawdbot can start a dedicated, isolated Chrome/Chromium instance for clawd and expose a small loopback control server.
Profiles can point at a remote Chrome via profiles.<name>.cdpUrl. Remote
profiles are attach-only (start/stop/reset are disabled).
browser.cdpUrl remains for legacy single-profile configs and as the base
scheme/host for profiles that only set cdpPort.
Defaults:
- enabled:
true - control URL:
http://127.0.0.1:18791(CDP uses18792) - CDP URL:
http://127.0.0.1:18792(control URL + 1, legacy single-profile) - profile color:
#FF4500(lobster-orange) - Note: the control server is started by the running gateway (Clawdbot.app menubar, or
clawdbot gateway).
{
browser: {
enabled: true,
controlUrl: "http://127.0.0.1:18791",
// cdpUrl: "http://127.0.0.1:18792", // legacy single-profile override
defaultProfile: "clawd",
profiles: {
clawd: { cdpPort: 18800, color: "#FF4500" },
work: { cdpPort: 18801, color: "#0066CC" },
remote: { cdpUrl: "http://10.0.0.42:9222", color: "#00AA00" }
},
color: "#FF4500",
// Advanced:
// headless: false,
// noSandbox: false,
// executablePath: "/usr/bin/chromium",
// attachOnly: false, // set true when tunneling a remote CDP to localhost
}
}
ui (Appearance)
Optional accent color used by the native apps for UI chrome (e.g. Talk Mode bubble tint).
If unset, clients fall back to a muted light-blue.
{
ui: {
seamColor: "#FF4500" // hex (RRGGBB or #RRGGBB)
}
}
gateway (Gateway server mode + bind)
Use gateway.mode to explicitly declare whether this machine should run the Gateway.
Defaults:
- mode: unset (treated as “do not auto-start”)
- bind:
loopback - port:
18789(single port for WS + HTTP)
{
gateway: {
mode: "local", // or "remote"
port: 18789, // WS + HTTP multiplex
bind: "loopback",
// controlUi: { enabled: true, basePath: "/clawdbot" }
// auth: { mode: "token", token: "your-token" } // token is for multi-machine CLI access
// tailscale: { mode: "off" | "serve" | "funnel" }
}
}
Control UI base path:
gateway.controlUi.basePathsets the URL prefix where the Control UI is served.- Examples:
"/ui","/clawdbot","/apps/clawdbot". - Default: root (
/) (unchanged).
Notes:
clawdbot gatewayrefuses to start unlessgateway.modeis set tolocal(or you pass the override flag).gateway.portcontrols the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).- Precedence:
--port>CLAWDBOT_GATEWAY_PORT>gateway.port> default18789.
Auth and Tailscale:
gateway.auth.modesets the handshake requirements (tokenorpassword).gateway.auth.tokenstores the shared token for token auth (used by the CLI on the same machine).- When
gateway.auth.modeis set, only that method is accepted (plus optional Tailscale headers). gateway.auth.passwordcan be set here, or viaCLAWDBOT_GATEWAY_PASSWORD(recommended).gateway.auth.allowTailscalecontrols whether Tailscale identity headers can satisfy auth.gateway.tailscale.mode: "serve"uses Tailscale Serve (tailnet only, loopback bind).gateway.tailscale.mode: "funnel"exposes the dashboard publicly; requires auth.gateway.tailscale.resetOnExitresets Serve/Funnel config on shutdown.
Remote client defaults (CLI):
gateway.remote.urlsets the default Gateway WebSocket URL for CLI calls whengateway.mode = "remote".gateway.remote.tokensupplies the token for remote calls (leave unset for no auth).gateway.remote.passwordsupplies the password for remote calls (leave unset for no auth).
macOS app behavior:
- Clawdbot.app watches
~/.clawdbot/clawdbot.jsonand switches modes live whengateway.modeorgateway.remote.urlchanges. - If
gateway.modeis unset butgateway.remote.urlis set, the macOS app treats it as remote mode. - When you change connection mode in the macOS app, it writes
gateway.mode(andgateway.remote.urlin remote mode) back to the config file.
{
gateway: {
mode: "remote",
remote: {
url: "ws://gateway.tailnet:18789",
token: "your-token",
password: "your-password"
}
}
}
gateway.reload (Config hot reload)
The Gateway watches ~/.clawdbot/clawdbot.json (or CLAWDBOT_CONFIG_PATH) and applies changes automatically.
Modes:
hybrid(default): hot-apply safe changes; restart the Gateway for critical changes.hot: only apply hot-safe changes; log when a restart is required.restart: restart the Gateway on any config change.off: disable hot reload.
{
gateway: {
reload: {
mode: "hybrid",
debounceMs: 300
}
}
}
Hot reload matrix (files + impact)
Files watched:
~/.clawdbot/clawdbot.json(orCLAWDBOT_CONFIG_PATH)
Hot-applied (no full gateway restart):
hooks(webhook auth/path/mappings) +hooks.gmail(Gmail watcher restarted)browser(browser control server restart)cron(cron service restart + concurrency update)agent.heartbeat(heartbeat runner restart)web(WhatsApp web provider restart)telegram,discord,signal,imessage(provider restarts)agent,models,routing,messages,session,whatsapp,logging,skills,ui,talk,identity,wizard(dynamic reads)
Requires full Gateway restart:
gateway(port/bind/auth/control UI/tailscale)bridgediscoverycanvasHost- Any unknown/unsupported config path (defaults to restart for safety)
Multi-instance isolation
To run multiple gateways on one host, isolate per-instance state + config and use unique ports:
CLAWDBOT_CONFIG_PATH(per-instance config)CLAWDBOT_STATE_DIR(sessions/creds)agent.workspace(memories)gateway.port(unique per instance)
Convenience flags (CLI):
clawdbot --dev …→ uses~/.clawdbot-dev+ shifts ports from base19001clawdbot --profile <name> …→ uses~/.clawdbot-<name>(port via config/env/flags)
See docs/gateway.md for the derived port mapping (gateway/bridge/browser/canvas).
Example:
CLAWDBOT_CONFIG_PATH=~/.clawdbot/a.json \
CLAWDBOT_STATE_DIR=~/.clawdbot-a \
clawdbot gateway --port 19001
hooks (Gateway webhooks)
Enable a simple HTTP webhook surface on the Gateway HTTP server.
Defaults:
- enabled:
false - path:
/hooks - maxBodyBytes:
262144(256 KB)
{
hooks: {
enabled: true,
token: "shared-secret",
path: "/hooks",
presets: ["gmail"],
transformsDir: "~/.clawdbot/hooks",
mappings: [
{
match: { path: "gmail" },
action: "agent",
wakeMode: "now",
name: "Gmail",
sessionKey: "hook:gmail:{{messages[0].id}}",
messageTemplate:
"From: {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}",
},
],
}
}
Requests must include the hook token:
Authorization: Bearer <token>orx-clawdbot-token: <token>or?token=<token>
Endpoints:
POST /hooks/wake→{ text, mode?: "now"|"next-heartbeat" }POST /hooks/agent→{ message, name?, sessionKey?, wakeMode?, deliver?, channel?, to?, thinking?, timeoutSeconds? }POST /hooks/<name>→ resolved viahooks.mappings
/hooks/agent always posts a summary into the main session (and can optionally trigger an immediate heartbeat via wakeMode: "now").
Mapping notes:
match.pathmatches the sub-path after/hooks(e.g./hooks/gmail→gmail).match.sourcematches a payload field (e.g.{ source: "gmail" }) so you can use a generic/hooks/ingestpath.- Templates like
{{messages[0].subject}}read from the payload. transformcan point to a JS/TS module that returns a hook action.
Gmail helper config (used by clawdbot hooks gmail setup / run):
{
hooks: {
gmail: {
account: "clawdbot@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" },
}
}
}
Gateway auto-start:
- If
hooks.enabled=trueandhooks.gmail.accountis set, the Gateway startsgog gmail watch serveon boot and auto-renews the watch. - Set
CLAWDBOT_SKIP_GMAIL_WATCHER=1to disable the auto-start (for manual runs). - Avoid running a separate
gog gmail watch servealongside the Gateway; it will fail withlisten tcp 127.0.0.1:8788: bind: address already in use.
Note: when tailscale.mode is on, Clawdbot defaults serve.path to / so
Tailscale can proxy /gmail-pubsub correctly (it strips the set-path prefix).
canvasHost (LAN/tailnet Canvas file server + live reload)
The Gateway serves a directory of HTML/CSS/JS over HTTP so iOS/Android nodes can simply canvas.navigate to it.
Default root: ~/clawd/canvas
Default port: 18793 (chosen to avoid the clawd browser CDP port 18792)
The server listens on the bridge bind host (LAN or Tailnet) so nodes can reach it.
The server:
- serves files under
canvasHost.root - injects a tiny live-reload client into served HTML
- watches the directory and broadcasts reloads over a WebSocket endpoint at
/__clawdbot/ws - auto-creates a starter
index.htmlwhen the directory is empty (so you see something immediately) - also serves A2UI at
/__clawdbot__/a2ui/and is advertised to nodes ascanvasHostUrl(always used by nodes for Canvas/A2UI)
Disable live reload (and file watching) if the directory is large or you hit EMFILE:
- config:
canvasHost: { liveReload: false }
{
canvasHost: {
root: "~/clawd/canvas",
port: 18793,
liveReload: true
}
}
Changes to canvasHost.* require a gateway restart (config reload will restart).
Disable with:
- config:
canvasHost: { enabled: false } - env:
CLAWDBOT_SKIP_CANVAS_HOST=1
bridge (node bridge server)
The Gateway can expose a simple TCP bridge for nodes (iOS/Android), typically on port 18790.
Defaults:
- enabled:
true - port:
18790 - bind:
lan(binds to0.0.0.0)
Bind modes:
lan:0.0.0.0(reachable on any interface, including LAN/Wi‑Fi and Tailscale)tailnet: bind only to the machine’s Tailscale IP (recommended for Vienna ⇄ London)loopback:127.0.0.1(local only)auto: prefer tailnet IP if present, elselan
{
bridge: {
enabled: true,
port: 18790,
bind: "tailnet"
}
}
discovery.wideArea (Wide-Area Bonjour / unicast DNS‑SD)
When enabled, the Gateway writes a unicast DNS-SD zone for _clawdbot-bridge._tcp under ~/.clawdbot/dns/ using the standard discovery domain clawdbot.internal.
To make iOS/Android discover across networks (Vienna ⇄ London), pair this with:
- a DNS server on the gateway host serving
clawdbot.internal.(CoreDNS is recommended) - Tailscale split DNS so clients resolve
clawdbot.internalvia that server
One-time setup helper (gateway host):
clawdbot dns setup --apply
{
discovery: { wideArea: { enabled: true } }
}
Template variables
Template placeholders are expanded in routing.transcribeAudio.command (and any future templated command fields).
| Variable | Description |
|---|---|
{{Body}} |
Full inbound message body |
{{BodyStripped}} |
Body with group mentions stripped (best default for agents) |
{{From}} |
Sender identifier (E.164 for WhatsApp; may differ per surface) |
{{To}} |
Destination identifier |
{{MessageSid}} |
Provider message id (when available) |
{{SessionId}} |
Current session UUID |
{{IsNewSession}} |
"true" when a new session was created |
{{MediaUrl}} |
Inbound media pseudo-URL (if present) |
{{MediaPath}} |
Local media path (if downloaded) |
{{MediaType}} |
Media type (image/audio/document/…) |
{{Transcript}} |
Audio transcript (when enabled) |
{{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) |
{{Surface}} |
Surface hint (whatsapp |
Cron (Gateway scheduler)
Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See Cron + wakeups for the full RFC and CLI examples.
{
cron: {
enabled: true,
maxConcurrentRuns: 2
}
}
Next: Agent Runtime 🦞