2188 lines
74 KiB
Markdown
2188 lines
74 KiB
Markdown
---
|
||
summary: "All configuration options for ~/.clawdbot/clawdbot.json with examples"
|
||
read_when:
|
||
- Adding or modifying config fields
|
||
---
|
||
# 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 allowlists + mention behavior (`whatsapp.groups`, `telegram.groups`, `discord.guilds`, `agents.list[].groupChat`)
|
||
- customize message prefixes (`messages`)
|
||
- set the agent's workspace (`agents.defaults.workspace` or `agents.list[].workspace`)
|
||
- tune the embedded agent defaults (`agents.defaults`) and session behavior (`session`)
|
||
- set per-agent identity (`agents.list[].identity`)
|
||
|
||
> **New to configuration?** Check out the [Configuration Examples](/gateway/configuration-examples) guide for complete examples with detailed explanations!
|
||
|
||
## 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.
|
||
|
||
## Apply + restart (RPC)
|
||
|
||
Use `config.apply` to validate + write the full config and restart the Gateway in one step.
|
||
It writes a restart sentinel and pings the last active session after the Gateway comes back.
|
||
|
||
Params:
|
||
- `raw` (string) — JSON5 payload for the entire config
|
||
- `sessionKey` (optional) — last active session key for the wake-up ping
|
||
- `restartDelayMs` (optional) — delay before restart (default 2000)
|
||
|
||
Example (via `gateway call`):
|
||
|
||
```bash
|
||
clawdbot gateway call config.apply --params '{
|
||
"raw": "{\\n agents: { defaults: { workspace: \\"~/clawd\\" } }\\n}\\n",
|
||
"sessionKey": "agent:main:whatsapp:dm:+15555550123",
|
||
"restartDelayMs": 1000
|
||
}'
|
||
```
|
||
|
||
## Minimal config (recommended starting point)
|
||
|
||
```json5
|
||
{
|
||
agents: { defaults: { workspace: "~/clawd" } },
|
||
whatsapp: { allowFrom: ["+15555550123"] }
|
||
}
|
||
```
|
||
|
||
Build the default image once with:
|
||
```bash
|
||
scripts/sandbox-setup.sh
|
||
```
|
||
|
||
## Self-chat mode (recommended for group control)
|
||
|
||
To prevent the bot from responding to WhatsApp @-mentions in groups (only respond to specific text triggers):
|
||
|
||
```json5
|
||
{
|
||
agents: {
|
||
defaults: { workspace: "~/clawd" },
|
||
list: [
|
||
{
|
||
id: "main",
|
||
groupChat: { mentionPatterns: ["@clawd", "reisponde"] }
|
||
}
|
||
]
|
||
},
|
||
whatsapp: {
|
||
// Allowlist is DMs only; including your own number enables self-chat mode.
|
||
allowFrom: ["+15555550123"],
|
||
groups: { "*": { requireMention: true } }
|
||
}
|
||
}
|
||
```
|
||
|
||
## Common options
|
||
|
||
### Env vars + `.env`
|
||
|
||
Clawdbot reads env vars from the parent process (shell, launchd/systemd, CI, etc.).
|
||
|
||
Additionally, it loads:
|
||
- `.env` from the current working directory (if present)
|
||
- a global fallback `.env` from `~/.clawdbot/.env` (aka `$CLAWDBOT_STATE_DIR/.env`)
|
||
|
||
Neither `.env` file overrides existing env vars.
|
||
|
||
You can also provide inline env vars in config. These are only applied if the
|
||
process env is missing the key (same non-overriding rule):
|
||
|
||
```json5
|
||
{
|
||
env: {
|
||
OPENROUTER_API_KEY: "sk-or-...",
|
||
vars: {
|
||
GROQ_API_KEY: "gsk-..."
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
See [/environment](/environment) for full precedence and sources.
|
||
|
||
### `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.
|
||
|
||
```json5
|
||
{
|
||
env: {
|
||
shellEnv: {
|
||
enabled: true,
|
||
timeoutMs: 15000
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Env var equivalent:
|
||
- `CLAWDBOT_LOAD_SHELL_ENV=1`
|
||
- `CLAWDBOT_SHELL_ENV_TIMEOUT_MS=15000`
|
||
|
||
### Auth storage (OAuth + API keys)
|
||
|
||
Clawdbot stores **per-agent** auth profiles (OAuth + API keys) in:
|
||
- `<agentDir>/auth-profiles.json` (default: `~/.clawdbot/agents/<agentId>/agent/auth-profiles.json`)
|
||
|
||
See also: [/concepts/oauth](/concepts/oauth)
|
||
|
||
Legacy OAuth imports:
|
||
- `~/.clawdbot/credentials/oauth.json` (or `$CLAWDBOT_STATE_DIR/credentials/oauth.json`)
|
||
|
||
The embedded Pi agent maintains a runtime cache at:
|
||
- `<agentDir>/auth.json` (managed automatically; don’t edit manually)
|
||
|
||
Legacy agent dir (pre multi-agent):
|
||
- `~/.clawdbot/agent/*` (migrated by `clawdbot doctor` into `~/.clawdbot/agents/<defaultAgentId>/agent/*`)
|
||
|
||
Overrides:
|
||
- OAuth dir (legacy import only): `CLAWDBOT_OAUTH_DIR`
|
||
- Agent dir (default agent root override): `CLAWDBOT_AGENT_DIR` (preferred), `PI_CODING_AGENT_DIR` (legacy)
|
||
|
||
On first use, Clawdbot imports `oauth.json` entries into `auth-profiles.json`.
|
||
|
||
Clawdbot also auto-syncs OAuth tokens from external CLIs into `auth-profiles.json` (when present on the gateway host):
|
||
- Claude Code → `anthropic:claude-cli`
|
||
- macOS: Keychain item "Claude Code-credentials" (choose "Always Allow" to avoid launchd prompts)
|
||
- Linux/Windows: `~/.claude/.credentials.json`
|
||
- `~/.codex/auth.json` (Codex CLI) → `openai-codex:codex-cli`
|
||
|
||
### `auth`
|
||
|
||
Optional metadata for auth profiles. This does **not** store secrets; it maps
|
||
profile IDs to a provider + mode (and optional email) and defines the provider
|
||
rotation order used for failover.
|
||
|
||
```json5
|
||
{
|
||
auth: {
|
||
profiles: {
|
||
"anthropic:me@example.com": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
|
||
"anthropic:work": { provider: "anthropic", mode: "api_key" }
|
||
},
|
||
order: {
|
||
anthropic: ["anthropic:me@example.com", "anthropic:work"]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### `agents.list[].identity`
|
||
|
||
Optional per-agent identity used for defaults and UX. This is written by the macOS onboarding assistant.
|
||
|
||
If set, Clawdbot derives defaults (only when you haven’t set them explicitly):
|
||
- `messages.ackReaction` from the **active agent**’s `identity.emoji` (falls back to 👀)
|
||
- `agents.list[].groupChat.mentionPatterns` from the agent’s `identity.name`/`identity.emoji` (so “@Samantha” works in groups across Telegram/Slack/Discord/iMessage/WhatsApp)
|
||
|
||
```json5
|
||
{
|
||
agents: {
|
||
list: [
|
||
{ id: "main", identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" } }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### `wizard`
|
||
|
||
Metadata written by CLI wizards (`onboard`, `configure`, `doctor`).
|
||
|
||
```json5
|
||
{
|
||
wizard: {
|
||
lastRunAt: "2026-01-01T00:00:00.000Z",
|
||
lastRunVersion: "2026.1.4",
|
||
lastRunCommit: "abc1234",
|
||
lastRunCommand: "configure",
|
||
lastRunMode: "local"
|
||
}
|
||
}
|
||
```
|
||
|
||
### `logging`
|
||
|
||
- Default log file: `/tmp/clawdbot/clawdbot-YYYY-MM-DD.log`
|
||
- If you want a stable path, set `logging.file` to `/tmp/clawdbot/clawdbot.log`.
|
||
- Console output can be tuned separately via:
|
||
- `logging.consoleLevel` (defaults to `info`, bumps to `debug` when `--verbose`)
|
||
- `logging.consoleStyle` (`pretty` | `compact` | `json`)
|
||
- Tool summaries can be redacted to avoid leaking secrets:
|
||
- `logging.redactSensitive` (`off` | `tools`, default: `tools`)
|
||
- `logging.redactPatterns` (array of regex strings; overrides defaults)
|
||
|
||
```json5
|
||
{
|
||
logging: {
|
||
level: "info",
|
||
file: "/tmp/clawdbot/clawdbot.log",
|
||
consoleLevel: "info",
|
||
consoleStyle: "pretty",
|
||
redactSensitive: "tools",
|
||
redactPatterns: [
|
||
// Example: override defaults with your own rules.
|
||
"\\bTOKEN\\b\\s*[=:]\\s*([\"']?)([^\\s\"']+)\\1",
|
||
"/\\bsk-[A-Za-z0-9_-]{8,}\\b/gi"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### `whatsapp.dmPolicy`
|
||
|
||
Controls how WhatsApp direct chats (DMs) are handled:
|
||
- `"pairing"` (default): unknown senders get a pairing code; owner must approve
|
||
- `"allowlist"`: only allow senders in `whatsapp.allowFrom` (or paired allow store)
|
||
- `"open"`: allow all inbound DMs (**requires** `whatsapp.allowFrom` to include `"*"`)
|
||
- `"disabled"`: ignore all inbound DMs
|
||
|
||
Pairing codes expire after 1 hour; the bot only sends a pairing code when a new request is created. Pending DM pairing requests are capped at **3 per provider** by default.
|
||
|
||
Pairing approvals:
|
||
- `clawdbot pairing list whatsapp`
|
||
- `clawdbot pairing approve whatsapp <code>`
|
||
|
||
### `whatsapp.allowFrom`
|
||
|
||
Allowlist of E.164 phone numbers that may trigger WhatsApp auto-replies (**DMs only**).
|
||
If empty and `whatsapp.dmPolicy="pairing"`, unknown senders will receive a pairing code.
|
||
For groups, use `whatsapp.groupPolicy` + `whatsapp.groupAllowFrom`.
|
||
|
||
```json5
|
||
{
|
||
whatsapp: {
|
||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||
allowFrom: ["+15555550123", "+447700900123"],
|
||
textChunkLimit: 4000, // optional outbound chunk size (chars)
|
||
mediaMaxMb: 50 // optional inbound media cap (MB)
|
||
}
|
||
}
|
||
```
|
||
|
||
### `whatsapp.accounts` (multi-account)
|
||
|
||
Run multiple WhatsApp accounts in one gateway:
|
||
|
||
```json5
|
||
{
|
||
whatsapp: {
|
||
accounts: {
|
||
default: {}, // optional; keeps the default id stable
|
||
personal: {},
|
||
biz: {
|
||
// Optional override. Default: ~/.clawdbot/credentials/whatsapp/biz
|
||
// authDir: "~/.clawdbot/credentials/whatsapp/biz",
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Notes:
|
||
- Outbound commands default to account `default` if present; otherwise the first configured account id (sorted).
|
||
- The legacy single-account Baileys auth dir is migrated by `clawdbot doctor` into `whatsapp/default`.
|
||
|
||
### `telegram.accounts` / `discord.accounts` / `slack.accounts` / `signal.accounts` / `imessage.accounts`
|
||
|
||
Run multiple accounts per provider (each account has its own `accountId` and optional `name`):
|
||
|
||
```json5
|
||
{
|
||
telegram: {
|
||
accounts: {
|
||
default: {
|
||
name: "Primary bot",
|
||
botToken: "123456:ABC..."
|
||
},
|
||
alerts: {
|
||
name: "Alerts bot",
|
||
botToken: "987654:XYZ..."
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Notes:
|
||
- `default` is used when `accountId` is omitted (CLI + routing).
|
||
- Env tokens only apply to the **default** account.
|
||
- Base provider settings (group policy, mention gating, etc.) apply to all accounts unless overridden per account.
|
||
- Use `bindings[].match.accountId` to route each account to a different agents.defaults.
|
||
|
||
### Group chat mention gating (`agents.list[].groupChat` + `messages.groupChat`)
|
||
|
||
Group messages default to **require mention** (either metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, 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 `agents.list[].groupChat.mentionPatterns`. Always checked regardless of self-chat mode.
|
||
- Mention gating is enforced only when mention detection is possible (native mentions or at least one `mentionPattern`).
|
||
|
||
```json5
|
||
{
|
||
messages: {
|
||
groupChat: { historyLimit: 50 }
|
||
},
|
||
agents: {
|
||
list: [
|
||
{ id: "main", groupChat: { mentionPatterns: ["@clawd", "clawdbot", "clawd"] } }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
`messages.groupChat.historyLimit` sets the global default for group history context. Providers can override with `<provider>.historyLimit` (or `<provider>.accounts.*.historyLimit` for multi-account). Set `0` to disable history wrapping.
|
||
|
||
Per-agent override (takes precedence when set, even `[]`):
|
||
```json5
|
||
{
|
||
agents: {
|
||
list: [
|
||
{ id: "work", groupChat: { mentionPatterns: ["@workbot", "\\+15555550123"] } },
|
||
{ id: "personal", groupChat: { mentionPatterns: ["@homebot", "\\+15555550999"] } }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
Mention gating defaults live per provider (`whatsapp.groups`, `telegram.groups`, `imessage.groups`, `discord.guilds`). When `*.groups` is set, it also acts as a group allowlist; include `"*"` to allow all groups.
|
||
|
||
To respond **only** to specific text triggers (ignoring native @-mentions):
|
||
```json5
|
||
{
|
||
whatsapp: {
|
||
// Include your own number to enable self-chat mode (ignore native @-mentions).
|
||
allowFrom: ["+15555550123"],
|
||
groups: { "*": { requireMention: true } }
|
||
},
|
||
agents: {
|
||
list: [
|
||
{
|
||
id: "main",
|
||
groupChat: {
|
||
// Only these text patterns will trigger responses
|
||
mentionPatterns: ["reisponde", "@clawd"]
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### Group policy (per provider)
|
||
|
||
Use `*.groupPolicy` to control whether group/room messages are accepted at all:
|
||
|
||
```json5
|
||
{
|
||
whatsapp: {
|
||
groupPolicy: "allowlist",
|
||
groupAllowFrom: ["+15551234567"]
|
||
},
|
||
telegram: {
|
||
groupPolicy: "allowlist",
|
||
groupAllowFrom: ["tg:123456789", "@alice"]
|
||
},
|
||
signal: {
|
||
groupPolicy: "allowlist",
|
||
groupAllowFrom: ["+15551234567"]
|
||
},
|
||
imessage: {
|
||
groupPolicy: "allowlist",
|
||
groupAllowFrom: ["chat_id:123"]
|
||
},
|
||
discord: {
|
||
groupPolicy: "allowlist",
|
||
guilds: {
|
||
"GUILD_ID": {
|
||
channels: { help: { allow: true } }
|
||
}
|
||
}
|
||
},
|
||
slack: {
|
||
groupPolicy: "allowlist",
|
||
channels: { "#general": { allow: true } }
|
||
}
|
||
}
|
||
```
|
||
|
||
Notes:
|
||
- `"open"` (default): groups bypass allowlists; mention-gating still applies.
|
||
- `"disabled"`: block all group/room messages.
|
||
- `"allowlist"`: only allow groups/rooms that match the configured allowlist.
|
||
- WhatsApp/Telegram/Signal/iMessage use `groupAllowFrom` (fallback: explicit `allowFrom`).
|
||
- Discord/Slack use channel allowlists (`discord.guilds.*.channels`, `slack.channels`).
|
||
- Group DMs (Discord/Slack) are still controlled by `dm.groupEnabled` + `dm.groupChannels`.
|
||
|
||
### Multi-agent routing (`agents.list` + `bindings`)
|
||
|
||
Run multiple isolated agents (separate workspace, `agentDir`, sessions) inside one Gateway.
|
||
Inbound messages are routed to an agent via bindings.
|
||
|
||
- `agents.list[]`: per-agent overrides.
|
||
- `id`: stable agent id (required).
|
||
- `default`: optional; when multiple are set, the first wins and a warning is logged.
|
||
If none are set, the **first entry** in the list is the default agent.
|
||
- `name`: display name for the agent.
|
||
- `workspace`: default `~/clawd-<agentId>` (for `main`, falls back to `agents.defaults.workspace`).
|
||
- `agentDir`: default `~/.clawdbot/agents/<agentId>/agent`.
|
||
- `model`: per-agent default model (provider/model), overrides `agents.defaults.model` for that agent.
|
||
- `identity`: per-agent name/theme/emoji (used for mention patterns + ack reactions).
|
||
- `groupChat`: per-agent mention-gating (`mentionPatterns`).
|
||
- `sandbox`: per-agent sandbox config (overrides `agents.defaults.sandbox`).
|
||
- `mode`: `"off"` | `"non-main"` | `"all"`
|
||
- `workspaceAccess`: `"none"` | `"ro"` | `"rw"`
|
||
- `scope`: `"session"` | `"agent"` | `"shared"`
|
||
- `workspaceRoot`: custom sandbox workspace root
|
||
- `docker`: per-agent docker overrides (e.g. `image`, `network`, `env`, `setupCommand`, limits; ignored when `scope: "shared"`)
|
||
- `browser`: per-agent sandboxed browser overrides (ignored when `scope: "shared"`)
|
||
- `prune`: per-agent sandbox pruning overrides (ignored when `scope: "shared"`)
|
||
- `subagents`: per-agent sub-agent defaults.
|
||
- `allowAgents`: allowlist of agent ids for `sessions_spawn` from this agent (`["*"]` = allow any; default: only same agent)
|
||
- `tools`: per-agent tool restrictions (applied before sandbox tool policy).
|
||
- `allow`: array of allowed tool names
|
||
- `deny`: array of denied tool names (deny wins)
|
||
- `agents.defaults`: shared agent defaults (model, workspace, sandbox, etc.).
|
||
- `bindings[]`: routes inbound messages to an `agentId`.
|
||
- `match.provider` (required)
|
||
- `match.accountId` (optional; `*` = any account; omitted = default account)
|
||
- `match.peer` (optional; `{ kind: dm|group|channel, id }`)
|
||
- `match.guildId` / `match.teamId` (optional; provider-specific)
|
||
|
||
Deterministic match order:
|
||
1) `match.peer`
|
||
2) `match.guildId`
|
||
3) `match.teamId`
|
||
4) `match.accountId` (exact, no peer/guild/team)
|
||
5) `match.accountId: "*"` (provider-wide, no peer/guild/team)
|
||
6) default agent (`agents.list[].default`, else first list entry, else `"main"`)
|
||
|
||
Within each match tier, the first matching entry in `bindings` wins.
|
||
|
||
#### Per-agent access profiles (multi-agent)
|
||
|
||
Each agent can carry its own sandbox + tool policy. Use this to mix access
|
||
levels in one gateway:
|
||
- **Full access** (personal agent)
|
||
- **Read-only** tools + workspace
|
||
- **No filesystem access** (messaging/session tools only)
|
||
|
||
See [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools) for precedence and
|
||
additional examples.
|
||
|
||
Full access (no sandbox):
|
||
```json5
|
||
{
|
||
agents: {
|
||
list: [
|
||
{
|
||
id: "personal",
|
||
workspace: "~/clawd-personal",
|
||
sandbox: { mode: "off" }
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
Read-only tools + read-only workspace:
|
||
```json5
|
||
{
|
||
agents: {
|
||
list: [
|
||
{
|
||
id: "family",
|
||
workspace: "~/clawd-family",
|
||
sandbox: {
|
||
mode: "all",
|
||
scope: "agent",
|
||
workspaceAccess: "ro"
|
||
},
|
||
tools: {
|
||
allow: ["read", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"],
|
||
deny: ["write", "edit", "bash", "process", "browser"]
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
No filesystem access (messaging/session tools enabled):
|
||
```json5
|
||
{
|
||
agents: {
|
||
list: [
|
||
{
|
||
id: "public",
|
||
workspace: "~/clawd-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", "bash", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"]
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
Example: two WhatsApp accounts → two agents:
|
||
|
||
```json5
|
||
{
|
||
agents: {
|
||
list: [
|
||
{ id: "home", default: true, workspace: "~/clawd-home" },
|
||
{ id: "work", workspace: "~/clawd-work" }
|
||
]
|
||
},
|
||
bindings: [
|
||
{ agentId: "home", match: { provider: "whatsapp", accountId: "personal" } },
|
||
{ agentId: "work", match: { provider: "whatsapp", accountId: "biz" } }
|
||
],
|
||
whatsapp: {
|
||
accounts: {
|
||
personal: {},
|
||
biz: {},
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### `tools.agentToAgent` (optional)
|
||
|
||
Agent-to-agent messaging is opt-in:
|
||
|
||
```json5
|
||
{
|
||
tools: {
|
||
agentToAgent: {
|
||
enabled: false,
|
||
allow: ["home", "work"]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### `messages.queue`
|
||
|
||
Controls how inbound messages behave when an agent run is already active.
|
||
|
||
```json5
|
||
{
|
||
messages: {
|
||
queue: {
|
||
mode: "collect", // steer | followup | collect | steer-backlog (steer+backlog ok) | interrupt (queue=steer legacy)
|
||
debounceMs: 1000,
|
||
cap: 20,
|
||
drop: "summarize", // old | new | summarize
|
||
byProvider: {
|
||
whatsapp: "collect",
|
||
telegram: "collect",
|
||
discord: "collect",
|
||
imessage: "collect",
|
||
webchat: "collect"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### `commands` (chat command handling)
|
||
|
||
Controls how chat commands are enabled across connectors.
|
||
|
||
```json5
|
||
{
|
||
commands: {
|
||
native: false, // register native commands when supported
|
||
text: true, // parse slash commands in chat messages
|
||
restart: false, // allow /restart + gateway restart tool
|
||
useAccessGroups: true // enforce access-group allowlists/policies for commands
|
||
}
|
||
}
|
||
```
|
||
|
||
Notes:
|
||
- Text commands must be sent as a **standalone** message and use the leading `/` (no plain-text aliases).
|
||
- `commands.text: false` disables parsing chat messages for commands.
|
||
- `commands.native: true` registers native commands on supported connectors (Discord/Slack/Telegram). Platforms without native commands still rely on text commands.
|
||
- `commands.native: false` skips native registration; Discord/Telegram clear previously registered commands on startup. Slack commands are managed in the Slack app.
|
||
- `commands.restart: true` enables `/restart` and the gateway tool restart action.
|
||
- `commands.useAccessGroups: false` allows commands to bypass access-group allowlists/policies.
|
||
|
||
### `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.
|
||
|
||
```json5
|
||
{
|
||
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.
|
||
Multi-account support lives under `telegram.accounts` (see the multi-account section above). Env tokens only apply to the default account.
|
||
|
||
```json5
|
||
{
|
||
telegram: {
|
||
enabled: true,
|
||
botToken: "your-bot-token",
|
||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||
allowFrom: ["tg:123456789"], // optional; "open" requires ["*"]
|
||
groups: {
|
||
"*": { requireMention: true },
|
||
"-1001234567890": {
|
||
allowFrom: ["@admin"],
|
||
systemPrompt: "Keep answers brief.",
|
||
topics: {
|
||
"99": {
|
||
requireMention: false,
|
||
skills: ["search"],
|
||
systemPrompt: "Stay on topic."
|
||
}
|
||
}
|
||
}
|
||
},
|
||
historyLimit: 50, // include last N group messages as context (0 disables)
|
||
replyToMode: "first", // off | first | all
|
||
streamMode: "partial", // off | partial | block (draft streaming; separate from block streaming)
|
||
draftChunk: { // optional; only for streamMode=block
|
||
minChars: 200,
|
||
maxChars: 800,
|
||
breakPreference: "paragraph" // paragraph | newline | sentence
|
||
},
|
||
actions: { reactions: true, sendMessage: true }, // tool action gates (false disables)
|
||
mediaMaxMb: 5,
|
||
retry: { // outbound retry policy
|
||
attempts: 3,
|
||
minDelayMs: 400,
|
||
maxDelayMs: 30000,
|
||
jitter: 0.1
|
||
},
|
||
proxy: "socks5://localhost:9050",
|
||
webhookUrl: "https://example.com/telegram-webhook",
|
||
webhookSecret: "secret",
|
||
webhookPath: "/telegram-webhook"
|
||
}
|
||
}
|
||
```
|
||
|
||
Draft streaming notes:
|
||
- Uses Telegram `sendMessageDraft` (draft bubble, not a real message).
|
||
- Requires **private chat topics** (message_thread_id in DMs; bot has topics enabled).
|
||
- `/reasoning stream` streams reasoning into the draft, then sends the final answer.
|
||
Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry).
|
||
|
||
### `discord` (bot transport)
|
||
|
||
Configure the Discord bot by setting the bot token and optional gating:
|
||
Multi-account support lives under `discord.accounts` (see the multi-account section above). Env tokens only apply to the default account.
|
||
|
||
```json5
|
||
{
|
||
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
|
||
dm: {
|
||
enabled: true, // disable all DMs when false
|
||
policy: "pairing", // pairing | allowlist | open | disabled
|
||
allowFrom: ["1234567890", "steipete"], // optional DM allowlist ("open" requires ["*"])
|
||
groupEnabled: false, // enable group DMs
|
||
groupChannels: ["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,
|
||
users: ["987654321098765432"],
|
||
skills: ["docs"],
|
||
systemPrompt: "Short answers only."
|
||
}
|
||
}
|
||
}
|
||
},
|
||
historyLimit: 20, // include last N guild messages as context
|
||
textChunkLimit: 2000, // optional outbound text chunk size (chars)
|
||
maxLinesPerMessage: 17, // soft max lines per message (Discord UI clipping)
|
||
retry: { // outbound retry policy
|
||
attempts: 3,
|
||
minDelayMs: 500,
|
||
maxDelayMs: 30000,
|
||
jitter: 0.1
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
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; bare numeric IDs are ambiguous and rejected.
|
||
Guild slugs are lowercase with spaces replaced by `-`; channel keys use the slugged channel name (no leading `#`). Prefer guild ids as keys to avoid rename ambiguity.
|
||
Reaction notification modes:
|
||
- `off`: no reaction events.
|
||
- `own`: reactions on the bot's own messages (default).
|
||
- `all`: all reactions on all messages.
|
||
- `allowlist`: reactions from `guilds.<id>.users` on all messages (empty list disables).
|
||
Outbound text is chunked by `discord.textChunkLimit` (default 2000). Discord clients can clip very tall messages, so `discord.maxLinesPerMessage` (default 17) splits long multi-line replies even when under 2000 chars.
|
||
Retry policy defaults and behavior are documented in [Retry policy](/concepts/retry).
|
||
|
||
### `slack` (socket mode)
|
||
|
||
Slack runs in Socket Mode and requires both a bot token and app token:
|
||
|
||
```json5
|
||
{
|
||
slack: {
|
||
enabled: true,
|
||
botToken: "xoxb-...",
|
||
appToken: "xapp-...",
|
||
dm: {
|
||
enabled: true,
|
||
policy: "pairing", // pairing | allowlist | open | disabled
|
||
allowFrom: ["U123", "U456", "*"], // optional; "open" requires ["*"]
|
||
groupEnabled: false,
|
||
groupChannels: ["G123"]
|
||
},
|
||
channels: {
|
||
C123: { allow: true, requireMention: true, allowBots: false },
|
||
"#general": {
|
||
allow: true,
|
||
requireMention: true,
|
||
allowBots: false,
|
||
users: ["U123"],
|
||
skills: ["docs"],
|
||
systemPrompt: "Short answers only."
|
||
}
|
||
},
|
||
historyLimit: 50, // include last N channel/group messages as context (0 disables)
|
||
allowBots: false,
|
||
reactionNotifications: "own", // off | own | all | allowlist
|
||
reactionAllowlist: ["U123"],
|
||
replyToMode: "off", // off | first | all
|
||
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
|
||
}
|
||
}
|
||
```
|
||
|
||
Multi-account support lives under `slack.accounts` (see the multi-account section above). Env tokens only apply to the default account.
|
||
|
||
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.
|
||
|
||
Bot-authored messages are ignored by default. Enable with `slack.allowBots` or `slack.channels.<id>.allowBots`.
|
||
|
||
Reaction notification modes:
|
||
- `off`: no reaction events.
|
||
- `own`: reactions on the bot's own messages (default).
|
||
- `all`: all reactions on all messages.
|
||
- `allowlist`: reactions from `slack.reactionAllowlist` on 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 |
|
||
|
||
### `signal` (signal-cli)
|
||
|
||
Signal reactions can emit system events (shared reaction tooling):
|
||
|
||
```json5
|
||
{
|
||
signal: {
|
||
reactionNotifications: "own", // off | own | all | allowlist
|
||
reactionAllowlist: ["+15551234567", "uuid:123e4567-e89b-12d3-a456-426614174000"],
|
||
historyLimit: 50 // include last N group messages as context (0 disables)
|
||
}
|
||
}
|
||
```
|
||
|
||
Reaction notification modes:
|
||
- `off`: no reaction events.
|
||
- `own`: reactions on the bot's own messages (default).
|
||
- `all`: all reactions on all messages.
|
||
- `allowlist`: reactions from `signal.reactionAllowlist` on all messages (empty list disables).
|
||
|
||
### `imessage` (imsg CLI)
|
||
|
||
Clawdbot spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required.
|
||
|
||
```json5
|
||
{
|
||
imessage: {
|
||
enabled: true,
|
||
cliPath: "imsg",
|
||
dbPath: "~/Library/Messages/chat.db",
|
||
dmPolicy: "pairing", // pairing | allowlist | open | disabled
|
||
allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
|
||
historyLimit: 50, // include last N group messages as context (0 disables)
|
||
includeAttachments: false,
|
||
mediaMaxMb: 16,
|
||
service: "auto",
|
||
region: "US"
|
||
}
|
||
}
|
||
```
|
||
|
||
Multi-account support lives under `imessage.accounts` (see the multi-account section above).
|
||
|
||
Notes:
|
||
- Requires Full Disk Access to the Messages DB.
|
||
- The first send will prompt for Messages automation permission.
|
||
- Prefer `chat_id:<id>` targets. Use `imsg chats --limit 20` to list chats.
|
||
- `imessage.cliPath` can point to a wrapper script (e.g. `ssh` to another Mac that runs `imsg rpc`); use SSH keys to avoid password prompts.
|
||
|
||
Example wrapper:
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
exec ssh -T mac-mini "imsg rpc"
|
||
```
|
||
|
||
### `agents.defaults.workspace`
|
||
|
||
Sets the **single global workspace directory** used by the agent for file operations.
|
||
|
||
Default: `~/clawd`.
|
||
|
||
```json5
|
||
{
|
||
agents: { defaults: { workspace: "~/clawd" } }
|
||
}
|
||
```
|
||
|
||
If `agents.defaults.sandbox` is enabled, non-main sessions can override this with their
|
||
own per-scope workspaces under `agents.defaults.sandbox.workspaceRoot`.
|
||
|
||
### `agents.defaults.skipBootstrap`
|
||
|
||
Disables automatic creation of the workspace bootstrap files (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, and `BOOTSTRAP.md`).
|
||
|
||
Use this for pre-seeded deployments where your workspace files come from a repo.
|
||
|
||
```json5
|
||
{
|
||
agents: { defaults: { skipBootstrap: true } }
|
||
}
|
||
```
|
||
|
||
### `agents.defaults.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.
|
||
|
||
```json5
|
||
{
|
||
agents: { defaults: { userTimezone: "America/Chicago" } }
|
||
}
|
||
```
|
||
|
||
### `messages`
|
||
|
||
Controls inbound/outbound prefixes and optional ack reactions.
|
||
See [Messages](/concepts/messages) for queueing, sessions, and streaming context.
|
||
|
||
```json5
|
||
{
|
||
messages: {
|
||
responsePrefix: "🦞", // or "auto"
|
||
ackReaction: "👀",
|
||
ackReactionScope: "group-mentions",
|
||
removeAckAfterReply: false
|
||
}
|
||
}
|
||
```
|
||
|
||
`responsePrefix` is applied to **all outbound replies** (tool summaries, block
|
||
streaming, final replies) across providers unless already present.
|
||
|
||
If `messages.responsePrefix` is unset, no prefix is applied by default.
|
||
Set it to `"auto"` to derive `[{identity.name}]` for the routed agent (when set).
|
||
|
||
WhatsApp inbound prefix is configured via `whatsapp.messagePrefix` (deprecated:
|
||
`messages.messagePrefix`). Default stays **unchanged**: `"[clawdbot]"` when
|
||
`whatsapp.allowFrom` is empty, otherwise `""` (no prefix). When using
|
||
`"[clawdbot]"`, Clawdbot will instead use `[{identity.name}]` when the routed
|
||
agent has `identity.name` set.
|
||
|
||
`ackReaction` sends a best-effort emoji reaction to acknowledge inbound messages
|
||
on providers that support reactions (Slack/Discord/Telegram). Defaults to the
|
||
active agent’s `identity.emoji` when set, otherwise `"👀"`. Set it to `""` to disable.
|
||
|
||
`ackReactionScope` controls when reactions fire:
|
||
- `group-mentions` (default): only when a group/room requires mentions **and** the bot was mentioned
|
||
- `group-all`: all group/room messages
|
||
- `direct`: direct messages only
|
||
- `all`: all messages
|
||
|
||
`removeAckAfterReply` removes the bot’s ack reaction after a reply is sent
|
||
(Slack/Discord/Telegram only). Default: `false`.
|
||
|
||
### `talk`
|
||
|
||
Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to `ELEVENLABS_VOICE_ID` or `SAG_VOICE_ID` when unset.
|
||
`apiKey` falls back to `ELEVENLABS_API_KEY` (or the gateway’s shell profile) when unset.
|
||
`voiceAliases` lets Talk directives use friendly names (e.g. `"voice":"Clawd"`).
|
||
|
||
```json5
|
||
{
|
||
talk: {
|
||
voiceId: "elevenlabs_voice_id",
|
||
voiceAliases: {
|
||
Clawd: "EXAVITQu4vr4xnSDxMaL",
|
||
Roger: "CwhRBWXzGAHq8TQ4Fs17"
|
||
},
|
||
modelId: "eleven_v3",
|
||
outputFormat: "mp3_44100_128",
|
||
apiKey: "elevenlabs_api_key",
|
||
interruptOnSpeech: true
|
||
}
|
||
}
|
||
```
|
||
|
||
### `agents.defaults`
|
||
|
||
Controls the embedded agent runtime (model/thinking/verbose/timeouts).
|
||
`agents.defaults.models` defines the configured model catalog (and acts as the allowlist for `/model`).
|
||
`agents.defaults.model.primary` sets the default model; `agents.defaults.model.fallbacks` are global failovers.
|
||
`agents.defaults.imageModel` is optional and is **only used if the primary model lacks image input**.
|
||
Each `agents.defaults.models` entry can include:
|
||
- `alias` (optional model shortcut, e.g. `/opus`).
|
||
- `params` (optional provider-specific API params passed through to the model request).
|
||
|
||
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.
|
||
|
||
Clawdbot also ships a few built-in alias shorthands. Defaults only apply when the model
|
||
is already present in `agents.defaults.models`:
|
||
|
||
- `opus` -> `anthropic/claude-opus-4-5`
|
||
- `sonnet` -> `anthropic/claude-sonnet-4-5`
|
||
- `gpt` -> `openai/gpt-5.2`
|
||
- `gpt-mini` -> `openai/gpt-5-mini`
|
||
- `gemini` -> `google/gemini-3-pro-preview`
|
||
- `gemini-flash` -> `google/gemini-3-flash-preview`
|
||
|
||
If you configure the same alias name (case-insensitive) yourself, your value wins (defaults never override).
|
||
|
||
#### `agents.defaults.cliBackends` (CLI fallback)
|
||
|
||
Optional CLI backends for text-only fallback runs (no tool calls). These are useful as a
|
||
backup path when API providers fail. Image pass-through is supported when you configure
|
||
an `imageArg` that accepts file paths.
|
||
|
||
Notes:
|
||
- CLI backends are **text-first**; tools are always disabled.
|
||
- Sessions are supported when `sessionArg` is set; session ids are persisted per backend.
|
||
- For `claude-cli`, defaults are wired in. Override the command path if PATH is minimal
|
||
(launchd/systemd).
|
||
|
||
Example:
|
||
|
||
```json5
|
||
{
|
||
agents: {
|
||
defaults: {
|
||
cliBackends: {
|
||
"claude-cli": {
|
||
command: "/opt/homebrew/bin/claude"
|
||
},
|
||
"my-cli": {
|
||
command: "my-cli",
|
||
args: ["--json"],
|
||
output: "json",
|
||
modelArg: "--model",
|
||
sessionArg: "--session",
|
||
sessionMode: "existing",
|
||
systemPromptArg: "--system",
|
||
systemPromptWhen: "first",
|
||
imageArg: "--image",
|
||
imageMode: "repeat"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
```json5
|
||
{
|
||
agents: {
|
||
defaults: {
|
||
models: {
|
||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||
"anthropic/claude-sonnet-4-1": { alias: "Sonnet" },
|
||
"openrouter/deepseek/deepseek-r1:free": {},
|
||
"zai/glm-4.7": {
|
||
alias: "GLM",
|
||
params: {
|
||
thinking: {
|
||
type: "enabled",
|
||
clear_thinking: false
|
||
}
|
||
}
|
||
}
|
||
},
|
||
model: {
|
||
primary: "anthropic/claude-opus-4-5",
|
||
fallbacks: [
|
||
"openrouter/deepseek/deepseek-r1:free",
|
||
"openrouter/meta-llama/llama-3.3-70b-instruct:free"
|
||
]
|
||
},
|
||
imageModel: {
|
||
primary: "openrouter/qwen/qwen-2.5-vl-72b-instruct:free",
|
||
fallbacks: [
|
||
"openrouter/google/gemini-2.0-flash-vision:free"
|
||
]
|
||
},
|
||
thinkingDefault: "low",
|
||
verboseDefault: "off",
|
||
elevatedDefault: "on",
|
||
timeoutSeconds: 600,
|
||
mediaMaxMb: 5,
|
||
heartbeat: {
|
||
every: "30m",
|
||
target: "last"
|
||
},
|
||
maxConcurrent: 3,
|
||
subagents: {
|
||
maxConcurrent: 1,
|
||
archiveAfterMinutes: 60
|
||
},
|
||
bash: {
|
||
backgroundMs: 10000,
|
||
timeoutSec: 1800,
|
||
cleanupMs: 1800000
|
||
},
|
||
contextTokens: 200000
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### `agents.defaults.contextPruning` (tool-result pruning)
|
||
|
||
`agents.defaults.contextPruning` prunes **old tool results** from the in-memory context right before a request is sent to the LLM.
|
||
It does **not** modify the session history on disk (`*.jsonl` remains complete).
|
||
|
||
This is intended to reduce token usage for chatty agents that accumulate large tool outputs over time.
|
||
|
||
High level:
|
||
- Never touches user/assistant messages.
|
||
- Protects the last `keepLastAssistants` assistant messages (no tool results after that point are pruned).
|
||
- Protects the bootstrap prefix (nothing before the first user message is pruned).
|
||
- Modes:
|
||
- `adaptive`: soft-trims oversized tool results (keep head/tail) when the estimated context ratio crosses `softTrimRatio`.
|
||
Then hard-clears the oldest eligible tool results when the estimated context ratio crosses `hardClearRatio` **and**
|
||
there’s enough prunable tool-result bulk (`minPrunableToolChars`).
|
||
- `aggressive`: always replaces eligible tool results before the cutoff with the `hardClear.placeholder` (no ratio checks).
|
||
|
||
Soft vs hard pruning (what changes in the context sent to the LLM):
|
||
- **Soft-trim**: only for *oversized* tool results. Keeps the beginning + end and inserts `...` in the middle.
|
||
- Before: `toolResult("…very long output…")`
|
||
- After: `toolResult("HEAD…\n...\n…TAIL\n\n[Tool result trimmed: …]")`
|
||
- **Hard-clear**: replaces the entire tool result with the placeholder.
|
||
- Before: `toolResult("…very long output…")`
|
||
- After: `toolResult("[Old tool result content cleared]")`
|
||
|
||
Notes / current limitations:
|
||
- Tool results containing **image blocks are skipped** (never trimmed/cleared) right now.
|
||
- The estimated “context ratio” is based on **characters** (approximate), not exact tokens.
|
||
- If the session doesn’t contain at least `keepLastAssistants` assistant messages yet, pruning is skipped.
|
||
- In `aggressive` mode, `hardClear.enabled` is ignored (eligible tool results are always replaced with `hardClear.placeholder`).
|
||
|
||
Default (adaptive):
|
||
```json5
|
||
{
|
||
agents: { defaults: { contextPruning: { mode: "adaptive" } } }
|
||
}
|
||
```
|
||
|
||
To disable:
|
||
```json5
|
||
{
|
||
agents: { defaults: { contextPruning: { mode: "off" } } }
|
||
}
|
||
```
|
||
|
||
Defaults (when `mode` is `"adaptive"` or `"aggressive"`):
|
||
- `keepLastAssistants`: `3`
|
||
- `softTrimRatio`: `0.3` (adaptive only)
|
||
- `hardClearRatio`: `0.5` (adaptive only)
|
||
- `minPrunableToolChars`: `50000` (adaptive only)
|
||
- `softTrim`: `{ maxChars: 4000, headChars: 1500, tailChars: 1500 }` (adaptive only)
|
||
- `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }`
|
||
|
||
Example (aggressive, minimal):
|
||
```json5
|
||
{
|
||
agents: { defaults: { contextPruning: { mode: "aggressive" } } }
|
||
}
|
||
```
|
||
|
||
Example (adaptive tuned):
|
||
```json5
|
||
{
|
||
agents: {
|
||
defaults: {
|
||
contextPruning: {
|
||
mode: "adaptive",
|
||
keepLastAssistants: 3,
|
||
softTrimRatio: 0.3,
|
||
hardClearRatio: 0.5,
|
||
minPrunableToolChars: 50000,
|
||
softTrim: { maxChars: 4000, headChars: 1500, tailChars: 1500 },
|
||
hardClear: { enabled: true, placeholder: "[Old tool result content cleared]" },
|
||
// Optional: restrict pruning to specific tools (deny wins; supports "*" wildcards)
|
||
tools: { deny: ["browser", "canvas"] },
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
See [/concepts/session-pruning](/concepts/session-pruning) for behavior details.
|
||
|
||
Block streaming:
|
||
- `agents.defaults.blockStreamingDefault`: `"on"`/`"off"` (default off).
|
||
- Provider overrides: `*.blockStreaming` (and per-account variants) to force block streaming on/off.
|
||
Non-Telegram providers require an explicit `*.blockStreaming: true` to enable block replies.
|
||
- `agents.defaults.blockStreamingBreak`: `"text_end"` or `"message_end"` (default: text_end).
|
||
- `agents.defaults.blockStreamingChunk`: soft chunking for streamed blocks. Defaults to
|
||
800–1200 chars, prefers paragraph breaks (`\n\n`), then newlines, then sentences.
|
||
Example:
|
||
```json5
|
||
{
|
||
agents: { defaults: { blockStreamingChunk: { minChars: 800, maxChars: 1200 } } }
|
||
}
|
||
```
|
||
- `agents.defaults.blockStreamingCoalesce`: merge streamed blocks before sending.
|
||
Defaults to `{ idleMs: 1000 }` and inherits `minChars` from `blockStreamingChunk`
|
||
with `maxChars` capped to the provider text limit. Signal/Slack/Discord default
|
||
to `minChars: 1500` unless overridden.
|
||
Provider overrides: `whatsapp.blockStreamingCoalesce`, `telegram.blockStreamingCoalesce`,
|
||
`discord.blockStreamingCoalesce`, `slack.blockStreamingCoalesce`, `signal.blockStreamingCoalesce`,
|
||
`imessage.blockStreamingCoalesce`, `msteams.blockStreamingCoalesce` (and per-account variants).
|
||
- `agents.defaults.humanDelay`: randomized pause between **block replies** after the first.
|
||
Modes: `off` (default), `natural` (800–2500ms), `custom` (use `minMs`/`maxMs`).
|
||
Per-agent override: `agents.list[].humanDelay`.
|
||
Example:
|
||
```json5
|
||
{
|
||
agents: { defaults: { humanDelay: { mode: "natural" } } }
|
||
}
|
||
```
|
||
See [/concepts/streaming](/concepts/streaming) for behavior + chunking details.
|
||
|
||
Typing indicators:
|
||
- `agents.defaults.typingMode`: `"never" | "instant" | "thinking" | "message"`. Defaults to
|
||
`instant` for direct chats / mentions and `message` for unmentioned group chats.
|
||
- `session.typingMode`: per-session override for the mode.
|
||
- `agents.defaults.typingIntervalSeconds`: how often the typing signal is refreshed (default: 6s).
|
||
- `session.typingIntervalSeconds`: per-session override for the refresh interval.
|
||
See [/concepts/typing-indicators](/concepts/typing-indicators) for behavior details.
|
||
|
||
`agents.defaults.model.primary` should be set as `provider/model` (e.g. `anthropic/claude-opus-4-5`).
|
||
Aliases come from `agents.defaults.models.*.alias` (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.
|
||
|
||
`agents.defaults.heartbeat` configures periodic heartbeat runs:
|
||
- `every`: duration string (`ms`, `s`, `m`, `h`); default unit minutes. Default:
|
||
`30m`. Set `0m` to disable.
|
||
- `model`: optional override model for heartbeat runs (`provider/model`).
|
||
- `includeReasoning`: when `true`, heartbeats will also deliver the separate `Reasoning:` message when available (same shape as `/reasoning on`). Default: `false`.
|
||
- `target`: optional delivery provider (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `none`). Default: `last`.
|
||
- `to`: optional recipient override (provider-specific id, e.g. E.164 for WhatsApp, chat id for Telegram).
|
||
- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`). Overrides are sent verbatim; include a `Read HEARTBEAT.md if exists` line if you still want the file read.
|
||
- `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30).
|
||
|
||
Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful
|
||
of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`.
|
||
|
||
`tools.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)
|
||
|
||
`agents.defaults.subagents` configures sub-agent defaults:
|
||
- `maxConcurrent`: max concurrent sub-agent runs (default 1)
|
||
- `archiveAfterMinutes`: auto-archive sub-agent sessions after N minutes (default 60; set `0` to disable)
|
||
- Per-subagent tool policy: `tools.subagents.tools.allow` / `tools.subagents.tools.deny` (deny wins)
|
||
|
||
`tools.allow` / `tools.deny` configure a global tool allow/deny policy (deny wins).
|
||
This is applied even when the Docker sandbox is **off**.
|
||
|
||
Example (disable browser/canvas everywhere):
|
||
```json5
|
||
{
|
||
tools: { deny: ["browser", "canvas"] }
|
||
}
|
||
```
|
||
|
||
`tools.elevated` controls elevated (host) bash access:
|
||
- `enabled`: allow elevated mode (default true)
|
||
- `allowFrom`: per-provider allowlists (empty = disabled)
|
||
- `whatsapp`: E.164 numbers
|
||
- `telegram`: chat ids or usernames
|
||
- `discord`: user ids or usernames (falls back to `discord.dm.allowFrom` if omitted)
|
||
- `signal`: E.164 numbers
|
||
- `imessage`: handles/chat ids
|
||
- `webchat`: session ids or usernames
|
||
|
||
Example:
|
||
```json5
|
||
{
|
||
tools: {
|
||
elevated: {
|
||
enabled: true,
|
||
allowFrom: {
|
||
whatsapp: ["+15555550123"],
|
||
discord: ["steipete", "1234567890123"]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Per-agent override (further restrict):
|
||
```json5
|
||
{
|
||
agents: {
|
||
list: [
|
||
{
|
||
id: "family",
|
||
tools: {
|
||
elevated: { enabled: false }
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
Notes:
|
||
- `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can only further restrict (both must allow).
|
||
- `/elevated on|off` stores state per session key; inline directives apply to a single message.
|
||
- Elevated `bash` runs on the host and bypasses sandboxing.
|
||
- Tool policy still applies; if `bash` is denied, elevated cannot be used.
|
||
|
||
`agents.defaults.maxConcurrent` sets the maximum number of embedded agent runs that can
|
||
execute in parallel across sessions. Each session is still serialized (one run
|
||
per session key at a time). Default: 1.
|
||
|
||
### `agents.defaults.sandbox`
|
||
|
||
Optional **Docker sandboxing** for the embedded agent. Intended for non-main
|
||
sessions so they cannot access your host system.
|
||
|
||
Details: [Sandboxing](/gateway/sandboxing)
|
||
|
||
Defaults (if enabled):
|
||
- scope: `"agent"` (one container + workspace per agent)
|
||
- Debian bookworm-slim based image
|
||
- agent workspace access: `workspaceAccess: "none"` (default)
|
||
- `"none"`: use a per-scope sandbox workspace under `~/.clawdbot/sandboxes`
|
||
- `"ro"`: keep the sandbox workspace at `/workspace`, and mount the agent workspace read-only at `/agent` (disables `write`/`edit`)
|
||
- `"rw"`: mount the agent workspace read/write at `/workspace`
|
||
- auto-prune: idle > 24h OR age > 7d
|
||
- tool policy: allow only `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` (deny wins)
|
||
- configure via `tools.sandbox.tools`, override per-agent via `agents.list[].tools.sandbox.tools`
|
||
- optional sandboxed browser (Chromium + CDP, noVNC observer)
|
||
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`
|
||
|
||
Warning: `scope: "shared"` means a shared container and shared workspace. No
|
||
cross-session isolation. Use `scope: "session"` for per-session isolation.
|
||
|
||
Legacy: `perSession` is still supported (`true` → `scope: "session"`,
|
||
`false` → `scope: "shared"`).
|
||
|
||
```json5
|
||
{
|
||
agents: {
|
||
defaults: {
|
||
sandbox: {
|
||
mode: "non-main", // off | non-main | all
|
||
scope: "agent", // session | agent | shared (agent is default)
|
||
workspaceAccess: "none", // none | ro | rw
|
||
workspaceRoot: "~/.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",
|
||
// Per-agent override (multi-agent): agents.list[].sandbox.docker.*
|
||
pidsLimit: 256,
|
||
memory: "1g",
|
||
memorySwap: "2g",
|
||
cpus: 1,
|
||
ulimits: {
|
||
nofile: { soft: 1024, hard: 2048 },
|
||
nproc: 256
|
||
},
|
||
seccompProfile: "/path/to/seccomp.json",
|
||
apparmorProfile: "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,
|
||
allowHostControl: false,
|
||
allowedControlUrls: ["http://10.0.0.42:18791"],
|
||
allowedControlHosts: ["browser.lab.local", "10.0.0.42"],
|
||
allowedControlPorts: [18791],
|
||
autoStart: true,
|
||
autoStartTimeoutMs: 12000
|
||
},
|
||
prune: {
|
||
idleHours: 24, // 0 disables idle pruning
|
||
maxAgeDays: 7 // 0 disables max-age pruning
|
||
}
|
||
}
|
||
}
|
||
},
|
||
tools: {
|
||
sandbox: {
|
||
tools: {
|
||
allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"],
|
||
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Build the default sandbox image once with:
|
||
```bash
|
||
scripts/sandbox-setup.sh
|
||
```
|
||
|
||
Note: sandbox containers default to `network: "none"`; set `agents.defaults.sandbox.docker.network`
|
||
to `"bridge"` (or your custom network) if the agent needs outbound access.
|
||
|
||
Note: inbound attachments are staged into the active workspace at `media/inbound/*`. With `workspaceAccess: "rw"`, that means files are written into the agent workspace.
|
||
|
||
Build the optional browser image with:
|
||
```bash
|
||
scripts/sandbox-browser-setup.sh
|
||
```
|
||
|
||
When `agents.defaults.sandbox.browser.enabled=true`, the browser tool uses a sandboxed
|
||
Chromium instance (CDP). If noVNC is enabled (default when headless=false),
|
||
the noVNC URL is injected into the system prompt so the agent can reference it.
|
||
This does not require `browser.enabled` in the main config; the sandbox control
|
||
URL is injected per session.
|
||
|
||
`agents.defaults.sandbox.browser.allowHostControl` (default: false) allows
|
||
sandboxed sessions to explicitly target the **host** browser control server
|
||
via the browser tool (`target: "host"`). Leave this off if you want strict
|
||
sandbox isolation.
|
||
|
||
Allowlists for remote control:
|
||
- `allowedControlUrls`: exact control URLs permitted for `target: "custom"`.
|
||
- `allowedControlHosts`: hostnames permitted (hostname only, no port).
|
||
- `allowedControlPorts`: ports permitted (defaults: http=80, https=443).
|
||
Defaults: all allowlists are unset (no restriction). `allowHostControl` defaults to false.
|
||
|
||
### `models` (custom providers + base URLs)
|
||
|
||
Clawdbot uses the **pi-coding-agent** model catalog. You can add custom providers
|
||
(LiteLLM, local OpenAI-compatible servers, Anthropic proxies, etc.) by writing
|
||
`~/.clawdbot/agents/<agentId>/agent/models.json` or by defining the same schema inside your
|
||
Clawdbot config under `models.providers`.
|
||
Provider-by-provider overview + examples: [/concepts/model-providers](/concepts/model-providers).
|
||
|
||
When `models.providers` is present, Clawdbot writes/merges a `models.json` into
|
||
`~/.clawdbot/agents/<agentId>/agent/` on startup:
|
||
- default behavior: **merge** (keeps existing providers, overrides on name)
|
||
- set `models.mode: "replace"` to overwrite the file contents
|
||
|
||
Select the model via `agents.defaults.model.primary` (provider/model).
|
||
|
||
```json5
|
||
{
|
||
agents: {
|
||
defaults: {
|
||
model: { primary: "custom-proxy/llama-3.1-8b" },
|
||
models: {
|
||
"custom-proxy/llama-3.1-8b": {}
|
||
}
|
||
}
|
||
},
|
||
models: {
|
||
mode: "merge",
|
||
providers: {
|
||
"custom-proxy": {
|
||
baseUrl: "http://localhost:4000/v1",
|
||
apiKey: "LITELLM_KEY",
|
||
api: "openai-completions",
|
||
models: [
|
||
{
|
||
id: "llama-3.1-8b",
|
||
name: "Llama 3.1 8B",
|
||
reasoning: false,
|
||
input: ["text"],
|
||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||
contextWindow: 128000,
|
||
maxTokens: 32000
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### OpenCode Zen (multi-model proxy)
|
||
|
||
OpenCode Zen is a multi-model gateway with per-model endpoints. Clawdbot uses
|
||
the built-in `opencode` provider from pi-ai; set `OPENCODE_API_KEY` (or
|
||
`OPENCODE_ZEN_API_KEY`) from https://opencode.ai/auth.
|
||
|
||
Notes:
|
||
- Model refs use `opencode/<modelId>` (example: `opencode/claude-opus-4-5`).
|
||
- If you enable an allowlist via `agents.defaults.models`, add each model you plan to use.
|
||
- Shortcut: `clawdbot onboard --auth-choice opencode-zen`.
|
||
|
||
```json5
|
||
{
|
||
agents: {
|
||
defaults: {
|
||
model: { primary: "opencode/claude-opus-4-5" },
|
||
models: { "opencode/claude-opus-4-5": { alias: "Opus" } }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Z.AI (GLM-4.7) — provider alias support
|
||
|
||
Z.AI models are available via the built-in `zai` provider. Set `ZAI_API_KEY`
|
||
in your environment and reference the model by provider/model.
|
||
|
||
Shortcut: `clawdbot onboard --auth-choice zai-api-key`.
|
||
|
||
```json5
|
||
{
|
||
agents: {
|
||
defaults: {
|
||
model: { primary: "zai/glm-4.7" },
|
||
models: { "zai/glm-4.7": {} }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Notes:
|
||
- `z.ai/*` and `z-ai/*` are accepted aliases and normalize to `zai/*`.
|
||
- If `ZAI_API_KEY` is missing, requests to `zai/*` will fail with an auth error at runtime.
|
||
- Example error: `No API key found for provider "zai".`
|
||
- Z.AI’s general API endpoint is `https://api.z.ai/api/paas/v4`. GLM coding
|
||
requests use the dedicated Coding endpoint `https://api.z.ai/api/coding/paas/v4`.
|
||
The built-in `zai` provider uses the Coding endpoint. If you need the general
|
||
endpoint, define a custom provider in `models.providers` with the base URL
|
||
override (see the custom providers section above).
|
||
- Use a fake placeholder in docs/configs; never commit real API keys.
|
||
|
||
### 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**.
|
||
|
||
```json5
|
||
{
|
||
agents: {
|
||
defaults: {
|
||
model: { primary: "lmstudio/minimax-m2.1-gs32" },
|
||
models: {
|
||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||
"lmstudio/minimax-m2.1-gs32": { alias: "Minimax" }
|
||
}
|
||
}
|
||
},
|
||
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`/`maxTokens` if your LM Studio context length differs.
|
||
|
||
### MiniMax API (platform.minimax.io)
|
||
|
||
Use MiniMax's Anthropic-compatible API directly without LM Studio:
|
||
|
||
```json5
|
||
{
|
||
agent: {
|
||
model: { primary: "minimax/MiniMax-M2.1" },
|
||
models: {
|
||
"anthropic/claude-opus-4-5": { alias: "Opus" },
|
||
"minimax/MiniMax-M2.1": { alias: "Minimax" }
|
||
}
|
||
},
|
||
models: {
|
||
mode: "merge",
|
||
providers: {
|
||
minimax: {
|
||
baseUrl: "https://api.minimax.io/anthropic",
|
||
apiKey: "${MINIMAX_API_KEY}",
|
||
api: "anthropic-messages",
|
||
models: [
|
||
{
|
||
id: "MiniMax-M2.1",
|
||
name: "MiniMax M2.1",
|
||
reasoning: false,
|
||
input: ["text"],
|
||
// Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs.
|
||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
||
contextWindow: 200000,
|
||
maxTokens: 8192
|
||
},
|
||
{
|
||
id: "MiniMax-M2.1-lightning",
|
||
name: "MiniMax M2.1 Lightning",
|
||
reasoning: false,
|
||
input: ["text"],
|
||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
||
contextWindow: 200000,
|
||
maxTokens: 8192
|
||
},
|
||
{
|
||
id: "MiniMax-M2",
|
||
name: "MiniMax M2",
|
||
reasoning: true,
|
||
input: ["text"],
|
||
cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
|
||
contextWindow: 200000,
|
||
maxTokens: 8192
|
||
}
|
||
]
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Notes:
|
||
- Set `MINIMAX_API_KEY` environment variable or use `clawdbot onboard --auth-choice minimax-api`
|
||
- Available models: `MiniMax-M2.1` (default), `MiniMax-M2.1-lightning` (~100 tps), `MiniMax-M2` (reasoning)
|
||
- Pricing is a placeholder; MiniMax doesn't publish public rates. Override in `models.json` for accurate cost tracking.
|
||
|
||
Notes:
|
||
- Supported APIs: `openai-completions`, `openai-responses`, `anthropic-messages`,
|
||
`google-generative-ai`
|
||
- Use `authHeader: true` + `headers` for custom auth needs.
|
||
- Override the agent config root with `CLAWDBOT_AGENT_DIR` (or `PI_CODING_AGENT_DIR`)
|
||
if you want `models.json` stored elsewhere (default: `~/.clawdbot/agents/main/agent`).
|
||
|
||
### `session`
|
||
|
||
Controls session scoping, idle expiry, reset triggers, and where the session store is written.
|
||
|
||
```json5
|
||
{
|
||
session: {
|
||
scope: "per-sender",
|
||
idleMinutes: 60,
|
||
resetTriggers: ["/new", "/reset"],
|
||
// Default is already per-agent under ~/.clawdbot/agents/<agentId>/sessions/sessions.json
|
||
// You can override with {agentId} templating:
|
||
store: "~/.clawdbot/agents/{agentId}/sessions/sessions.json",
|
||
// Direct chats collapse to agent:<agentId>:<mainKey> (default: "main").
|
||
mainKey: "main",
|
||
agentToAgent: {
|
||
// Max ping-pong reply turns between requester/target (0–5).
|
||
maxPingPongTurns: 5
|
||
},
|
||
sendPolicy: {
|
||
rules: [
|
||
{ action: "deny", match: { provider: "discord", chatType: "group" } }
|
||
],
|
||
default: "allow"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Fields:
|
||
- `mainKey`: direct-chat bucket key (default: `"main"`). Useful when you want to “rename” the primary DM thread without changing `agentId`.
|
||
- Sandbox note: `agents.defaults.sandbox.mode: "non-main"` uses this key to detect the main session. Any session key that does not match `mainKey` (groups/channels) is sandboxed.
|
||
- `agentToAgent.maxPingPongTurns`: max reply-back turns between requester/target (0–5, default 5).
|
||
- `sendPolicy.default`: `allow` or `deny` fallback when no rule matches.
|
||
- `sendPolicy.rules[]`: match by `provider`, `chatType` (`direct|group|room`), or `keyPrefix` (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`: set `false` to disable a skill even if it’s bundled/installed.
|
||
- `env`: environment variables injected for the agent run (only if not already set).
|
||
- `apiKey`: optional convenience for skills that declare a primary env var (e.g. `nano-banana-pro` → `GEMINI_API_KEY`).
|
||
|
||
Example:
|
||
|
||
```json5
|
||
{
|
||
skills: {
|
||
allowBundled: ["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 uses `18792`)
|
||
- 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`).
|
||
|
||
```json5
|
||
{
|
||
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.
|
||
|
||
```json5
|
||
{
|
||
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)
|
||
|
||
```json5
|
||
{
|
||
gateway: {
|
||
mode: "local", // or "remote"
|
||
port: 18789, // WS + HTTP multiplex
|
||
bind: "loopback",
|
||
// controlUi: { enabled: true, basePath: "/clawdbot" }
|
||
// auth: { mode: "token", token: "your-token" } // token gates WS + Control UI access
|
||
// tailscale: { mode: "off" | "serve" | "funnel" }
|
||
}
|
||
}
|
||
```
|
||
|
||
Control UI base path:
|
||
- `gateway.controlUi.basePath` sets the URL prefix where the Control UI is served.
|
||
- Examples: `"/ui"`, `"/clawdbot"`, `"/apps/clawdbot"`.
|
||
- Default: root (`/`) (unchanged).
|
||
|
||
Related docs:
|
||
- [Control UI](/web/control-ui)
|
||
- [Web overview](/web)
|
||
- [Tailscale](/gateway/tailscale)
|
||
- [Remote access](/gateway/remote)
|
||
|
||
Notes:
|
||
- `clawdbot gateway` refuses to start unless `gateway.mode` is set to `local` (or you pass the override flag).
|
||
- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
|
||
- OpenAI Chat Completions endpoint: **disabled by default**; enable with `gateway.http.endpoints.chatCompletions.enabled: true`.
|
||
- Precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`.
|
||
- Non-loopback binds (`lan`/`tailnet`/`auto`) require auth. Use `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`).
|
||
- The onboarding wizard generates a gateway token by default (even on loopback).
|
||
- `gateway.remote.token` is **only** for remote CLI calls; it does not enable local gateway auth. `gateway.token` is ignored.
|
||
|
||
Auth and Tailscale:
|
||
- `gateway.auth.mode` sets the handshake requirements (`token` or `password`).
|
||
- `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine).
|
||
- When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers).
|
||
- `gateway.auth.password` can be set here, or via `CLAWDBOT_GATEWAY_PASSWORD` (recommended).
|
||
- `gateway.auth.allowTailscale` controls 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.resetOnExit` resets Serve/Funnel config on shutdown.
|
||
|
||
Remote client defaults (CLI):
|
||
- `gateway.remote.url` sets the default Gateway WebSocket URL for CLI calls when `gateway.mode = "remote"`.
|
||
- `gateway.remote.token` supplies the token for remote calls (leave unset for no auth).
|
||
- `gateway.remote.password` supplies the password for remote calls (leave unset for no auth).
|
||
|
||
macOS app behavior:
|
||
- Clawdbot.app watches `~/.clawdbot/clawdbot.json` and switches modes live when `gateway.mode` or `gateway.remote.url` changes.
|
||
- If `gateway.mode` is unset but `gateway.remote.url` is set, the macOS app treats it as remote mode.
|
||
- When you change connection mode in the macOS app, it writes `gateway.mode` (and `gateway.remote.url` in remote mode) back to the config file.
|
||
|
||
```json5
|
||
{
|
||
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.
|
||
|
||
```json5
|
||
{
|
||
gateway: {
|
||
reload: {
|
||
mode: "hybrid",
|
||
debounceMs: 300
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Hot reload matrix (files + impact)
|
||
|
||
Files watched:
|
||
- `~/.clawdbot/clawdbot.json` (or `CLAWDBOT_CONFIG_PATH`)
|
||
|
||
Hot-applied (no full gateway restart):
|
||
- `hooks` (webhook auth/path/mappings) + `hooks.gmail` (Gmail watcher restarted)
|
||
- `browser` (browser control server restart)
|
||
- `cron` (cron service restart + concurrency update)
|
||
- `agents.defaults.heartbeat` (heartbeat runner restart)
|
||
- `web` (WhatsApp web 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)
|
||
- `bridge`
|
||
- `discovery`
|
||
- `canvasHost`
|
||
- 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)
|
||
- `agents.defaults.workspace` (memories)
|
||
- `gateway.port` (unique per instance)
|
||
|
||
Convenience flags (CLI):
|
||
- `clawdbot --dev …` → uses `~/.clawdbot-dev` + shifts ports from base `19001`
|
||
- `clawdbot --profile <name> …` → uses `~/.clawdbot-<name>` (port via config/env/flags)
|
||
|
||
See [Gateway runbook](/gateway) for the derived port mapping (gateway/bridge/browser/canvas).
|
||
|
||
Example:
|
||
```bash
|
||
CLAWDBOT_CONFIG_PATH=~/.clawdbot/a.json \
|
||
CLAWDBOT_STATE_DIR=~/.clawdbot-a \
|
||
clawdbot gateway --port 19001
|
||
```
|
||
|
||
### `hooks` (Gateway webhooks)
|
||
|
||
Enable a simple HTTP webhook endpoint on the Gateway HTTP server.
|
||
|
||
Defaults:
|
||
- enabled: `false`
|
||
- path: `/hooks`
|
||
- maxBodyBytes: `262144` (256 KB)
|
||
|
||
```json5
|
||
{
|
||
hooks: {
|
||
enabled: true,
|
||
token: "shared-secret",
|
||
path: "/hooks",
|
||
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}}",
|
||
deliver: true,
|
||
provider: "last",
|
||
model: "openai/gpt-5.2-mini",
|
||
},
|
||
],
|
||
}
|
||
}
|
||
```
|
||
|
||
Requests must include the hook token:
|
||
- `Authorization: Bearer <token>` **or**
|
||
- `x-clawdbot-token: <token>` **or**
|
||
- `?token=<token>`
|
||
|
||
Endpoints:
|
||
- `POST /hooks/wake` → `{ text, mode?: "now"|"next-heartbeat" }`
|
||
- `POST /hooks/agent` → `{ message, name?, sessionKey?, wakeMode?, deliver?, provider?, to?, model?, thinking?, timeoutSeconds? }`
|
||
- `POST /hooks/<name>` → resolved via `hooks.mappings`
|
||
|
||
`/hooks/agent` always posts a summary into the main session (and can optionally trigger an immediate heartbeat via `wakeMode: "now"`).
|
||
|
||
Mapping notes:
|
||
- `match.path` matches the sub-path after `/hooks` (e.g. `/hooks/gmail` → `gmail`).
|
||
- `match.source` matches a payload field (e.g. `{ source: "gmail" }`) so you can use a generic `/hooks/ingest` path.
|
||
- Templates like `{{messages[0].subject}}` read from the payload.
|
||
- `transform` can point to a JS/TS module that returns a hook action.
|
||
- `deliver: true` sends the final reply to a provider; `provider` defaults to `last` (falls back to WhatsApp).
|
||
- If there is no prior delivery route, set `provider` + `to` explicitly (required for Telegram/Discord/Slack/Signal/iMessage).
|
||
- `model` overrides the LLM for this hook run (`provider/model` or alias; must be allowed if `agents.defaults.models` is set).
|
||
|
||
Gmail helper config (used by `clawdbot hooks gmail setup` / `run`):
|
||
|
||
```json5
|
||
{
|
||
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" },
|
||
|
||
// Optional: use a cheaper model for Gmail hook processing
|
||
// Falls back to agents.defaults.model.fallbacks, then primary, on auth/rate-limit/timeout
|
||
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
|
||
// Optional: default thinking level for Gmail hooks
|
||
thinking: "off",
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Model override for Gmail hooks:
|
||
- `hooks.gmail.model` specifies a model to use for Gmail hook processing (defaults to session primary).
|
||
- Accepts `provider/model` refs or aliases from `agents.defaults.models`.
|
||
- Falls back to `agents.defaults.model.fallbacks`, then `agents.defaults.model.primary`, on auth/rate-limit/timeouts.
|
||
- If `agents.defaults.models` is set, include the hooks model in the allowlist.
|
||
- At startup, warns if the configured model is not in the model catalog or allowlist.
|
||
- `hooks.gmail.thinking` sets the default thinking level for Gmail hooks and is overridden by per-hook `thinking`.
|
||
|
||
Gateway auto-start:
|
||
- If `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts
|
||
`gog gmail watch serve` on boot and auto-renews the watch.
|
||
- Set `CLAWDBOT_SKIP_GMAIL_WATCHER=1` to disable the auto-start (for manual runs).
|
||
- Avoid running a separate `gog gmail watch serve` alongside the Gateway; it will
|
||
fail with `listen tcp 127.0.0.1:8788: bind: address already in use`.
|
||
|
||
Note: when `tailscale.mode` is on, Clawdbot defaults `serve.path` to `/` so
|
||
Tailscale can proxy `/gmail-pubsub` correctly (it strips the set-path prefix).
|
||
If you need the backend to receive the prefixed path, set
|
||
`hooks.gmail.tailscale.target` to a full URL (and align `serve.path`).
|
||
|
||
### `canvasHost` (LAN/tailnet Canvas file server + live reload)
|
||
|
||
The Gateway serves a directory of HTML/CSS/JS over HTTP so iOS/Android nodes can simply `canvas.navigate` to it.
|
||
|
||
Default root: `~/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.html` when the directory is empty (so you see something immediately)
|
||
- also serves A2UI at `/__clawdbot__/a2ui/` and is advertised to nodes as `canvasHostUrl`
|
||
(always used by nodes for Canvas/A2UI)
|
||
|
||
Disable live reload (and file watching) if the directory is large or you hit `EMFILE`:
|
||
- config: `canvasHost: { liveReload: false }`
|
||
|
||
```json5
|
||
{
|
||
canvasHost: {
|
||
root: "~/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 to `0.0.0.0`)
|
||
|
||
Bind modes:
|
||
- `lan`: `0.0.0.0` (reachable on any interface, including LAN/Wi‑Fi and Tailscale)
|
||
- `tailnet`: bind only to the machine’s Tailscale IP (recommended for Vienna ⇄ London)
|
||
- `loopback`: `127.0.0.1` (local only)
|
||
- `auto`: prefer tailnet IP if present, else `lan`
|
||
|
||
```json5
|
||
{
|
||
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.internal` via that server
|
||
|
||
One-time setup helper (gateway host):
|
||
|
||
```bash
|
||
clawdbot dns setup --apply
|
||
```
|
||
|
||
```json5
|
||
{
|
||
discovery: { wideArea: { enabled: true } }
|
||
}
|
||
```
|
||
|
||
## Template variables
|
||
|
||
Template placeholders are expanded in `tools.audio.transcription.args` (and any future templated argument fields).
|
||
|
||
| Variable | Description |
|
||
|----------|-------------|
|
||
| `{{Body}}` | Full inbound message body |
|
||
| `{{RawBody}}` | Raw inbound message body (no history/sender wrappers; best for command parsing) |
|
||
| `{{BodyStripped}}` | Body with group mentions stripped (best default for agents) |
|
||
| `{{From}}` | Sender identifier (E.164 for WhatsApp; may differ per provider) |
|
||
| `{{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) |
|
||
| `{{Provider}}` | Provider hint (whatsapp|telegram|discord|imessage|webchat|…) |
|
||
|
||
## Cron (Gateway scheduler)
|
||
|
||
Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron jobs](/automation/cron-jobs) for the feature overview and CLI examples.
|
||
|
||
```json5
|
||
{
|
||
cron: {
|
||
enabled: true,
|
||
maxConcurrentRuns: 2
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
*Next: [Agent Runtime](/concepts/agent)* 🦞
|