Skip to main content

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, 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)
New to configuration? Check out the 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):
clawdbot gateway call config.apply --params '{
  "raw": "{\\n  agent: { workspace: \\"~/clawd\\" }\\n}\\n",
  "sessionKey": "agent:main:whatsapp:dm:+15555550123",
  "restartDelayMs": 1000
}'
{
  agent: { workspace: "~/clawd" },
  whatsapp: { allowFrom: ["+15555550123"] }
}
Build the default image once with:
scripts/sandbox-setup.sh
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:
  • .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):
{
  env: {
    OPENROUTER_API_KEY: "sk-or-...",
    vars: {
      GROQ_API_KEY: "gsk-..."
    }
  }
}
See /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.
{
  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 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.
{
  auth: {
    profiles: {
      "anthropic:[email protected]": { provider: "anthropic", mode: "oauth", email: "[email protected]" },
      "anthropic:work": { provider: "anthropic", mode: "api_key" }
    },
    order: {
      anthropic: ["anthropic:[email protected]", "anthropic:work"]
    }
  }
}

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.ackReaction from identity.emoji (falls back to 👀)
  • routing.groupChat.mentionPatterns from identity.name (so “@Samantha” works in groups across Telegram/Slack/Discord/iMessage/WhatsApp)
{
  identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" }
}

wizard

Metadata written by CLI wizards (onboard, configure, doctor).
{
  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)
{
  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. Pairing approvals:
  • clawdbot pairing list --provider whatsapp
  • clawdbot pairing approve --provider 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.
{
  whatsapp: {
    dmPolicy: "pairing", // pairing | allowlist | open | disabled
    allowFrom: ["+15555550123", "+447700900123"],
    textChunkLimit: 4000 // optional outbound chunk size (chars)
  }
}

whatsapp.accounts (multi-account)

Run multiple WhatsApp accounts in one gateway:
{
  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):
{
  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 routing.bindings[].match.accountId to route each account to a different agent.

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.
  • Mention gating is enforced only when mention detection is possible (native mentions or at least one mentionPattern).
  • Per-agent override: routing.agents.<agentId>.mentionPatterns (useful when multiple agents share a group).
{
  routing: {
    groupChat: {
      mentionPatterns: ["@clawd", "clawdbot", "clawd"],
      historyLimit: 50
    }
  }
}
Per-agent override (takes precedence when set, even []):
{
  routing: {
    agents: {
      work: { mentionPatterns: ["@workbot", "\\+15555550123"] },
      personal: { 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):
{
  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"]
    }
  }
}

Group policy (per provider)

Use *.groupPolicy to control whether group/room messages are accepted at all:
{
  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 (routing.agents + routing.bindings)

Run multiple isolated agents (separate workspace, agentDir, sessions) inside one Gateway. Inbound messages are routed to an agent via bindings.
  • routing.defaultAgentId: fallback when no binding matches (default: main).
  • routing.agents.<agentId>: per-agent overrides.
    • name: display name for the agent.
    • workspace: default ~/clawd-<agentId> (for main, falls back to legacy agent.workspace).
    • agentDir: default ~/.clawdbot/agents/<agentId>/agent.
    • model: per-agent default model (provider/model), overrides agent.model for that agent.
    • sandbox: per-agent sandbox config (overrides agent.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")
      • tools: per-agent sandbox tool policy (deny wins; overrides agent.sandbox.tools)
    • 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 (overrides agent.tools; applied before sandbox tool policy).
      • allow: array of allowed tool names
      • deny: array of denied tool names (deny wins)
  • routing.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. routing.defaultAgentId
Within each match tier, the first matching entry in routing.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 for precedence and additional examples. Full access (no sandbox):
{
  routing: {
    agents: {
      personal: {
        workspace: "~/clawd-personal",
        sandbox: { mode: "off" }
      }
    }
  }
}
Read-only tools + read-only workspace:
{
  routing: {
    agents: {
      family: {
        workspace: "~/clawd-family",
        sandbox: {
          mode: "all",
          scope: "agent",
          workspaceAccess: "ro"
        },
        tools: {
          allow: ["read", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn"],
          deny: ["write", "edit", "bash", "process", "browser"]
        }
      }
    }
  }
}
No filesystem access (messaging/session tools enabled):
{
  routing: {
    agents: {
      public: {
        workspace: "~/clawd-public",
        sandbox: {
          mode: "all",
          scope: "agent",
          workspaceAccess: "none"
        },
        tools: {
          allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "whatsapp", "telegram", "slack", "discord", "gateway"],
          deny: ["read", "write", "edit", "bash", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"]
        }
      }
    }
  }
}
Example: two WhatsApp accounts → two agents:
{
  routing: {
    defaultAgentId: "home",
    agents: {
      home: { workspace: "~/clawd-home" },
      work: { workspace: "~/clawd-work" },
    },
    bindings: [
      { agentId: "home", match: { provider: "whatsapp", accountId: "personal" } },
      { agentId: "work", match: { provider: "whatsapp", accountId: "biz" } },
    ],
  },
  whatsapp: {
    accounts: {
      personal: {},
      biz: {},
    }
  }
}

routing.agentToAgent (optional)

Agent-to-agent messaging is opt-in:
{
  routing: {
    agentToAgent: {
      enabled: false,
      allow: ["home", "work"]
    }
  }
}

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
      byProvider: {
        whatsapp: "collect",
        telegram: "collect",
        discord: "collect",
        imessage: "collect",
        webchat: "collect"
      }
    }
  }
}

commands (chat command handling)

Controls how chat commands are enabled across connectors.
{
  commands: {
    native: false,          // register native commands when supported
    text: true,             // parse slash commands in chat messages
    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.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.
{
  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.
{
  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."
          }
        }
      }
    },
    replyToMode: "first",                 // off | first | all
    streamMode: "partial",               // off | partial | block (draft streaming)
    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.

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

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,
      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."
      }
    },
    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 groupDefaultNotes
reactionsenabledReact + list reactions
messagesenabledRead/send/edit/delete
pinsenabledPin/unpin/list
memberInfoenabledMember info
emojiListenabledCustom 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",
    dmPolicy: "pairing", // pairing | allowlist | open | disabled
    allowFrom: ["+15555550123", "[email protected]", "chat_id:123"],
    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:
#!/usr/bin/env bash
exec ssh -T mac-mini "imsg rpc"

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-scope workspaces under agent.sandbox.workspaceRoot.

agent.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.
{
  agent: { skipBootstrap: true }
}

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 and optional ack reactions.
{
  messages: {
    messagePrefix: "[clawdbot]",
    responsePrefix: "🦞",
    ackReaction: "👀",
    ackReactionScope: "group-mentions"
  }
}
responsePrefix is applied to all outbound replies (tool summaries, block streaming, final replies) across providers unless already present. ackReaction sends a best-effort emoji reaction to acknowledge inbound messages on providers that support reactions (Slack/Discord/Telegram). Defaults to the configured 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

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). agent.models defines the configured model catalog (and acts as the allowlist for /model). agent.model.primary sets the default model; agent.model.fallbacks are global failovers. agent.imageModel is optional and is only used if the primary model lacks image input. Each agent.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 agent.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 agent.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).
{
  agent: {
    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
  }
}

agent.contextPruning (tool-result pruning)

agent.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):
{
  agent: {
    contextPruning: {
      mode: "adaptive"
    }
  }
}
To disable:
{
  agent: {
    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):
{
  agent: {
    contextPruning: {
      mode: "aggressive"
    }
  }
}
Example (adaptive tuned):
{
  agent: {
    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 for behavior details. 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 }
      }
    }
    
See /concepts/streaming for behavior + chunking details. Typing indicators:
  • agent.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.
  • agent.typingIntervalSeconds: how often the typing signal is refreshed (default: 6s).
  • session.typingIntervalSeconds: per-session override for the refresh interval. See /concepts/typing-indicators for behavior details.
agent.model.primary should be set as provider/model (e.g. anthropic/claude-opus-4-5). Aliases come from agent.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. agent.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).
  • 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. 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.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)
  • tools.allow / tools.deny: per-subagent tool allow/deny policy (deny wins)
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-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:
{
  agent: {
    elevated: {
      enabled: true,
      allowFrom: {
        whatsapp: ["+15555550123"],
        discord: ["steipete", "1234567890123"]
      }
    }
  }
}
Notes:
  • agent.elevated is global (not per-agent). Availability is based on sender allowlists.
  • /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.
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 Docker sandboxing for the embedded agent. Intended for non-main sessions so they cannot access your host system. Details: 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
  • tools: allow only bash, process, read, write, edit, sessions_list, sessions_history, sessions_send, sessions_spawn (deny wins)
  • 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 (truescope: "session", falsescope: "shared").
{
  agent: {
    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): routing.agents.<agentId>.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
      },
      tools: {
        allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn"],
        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. 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:
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/agents/<agentId>/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/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 agent.model.primary (provider/model).
{
  agent: {
    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
          }
        ]
      }
    }
  }
}

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.
{
  agent: {
    model: "zai/glm-4.7",
    allowedModels: ["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.
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: { 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.
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.
{
  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.
  • 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-proGEMINI_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 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).
{
  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.basePath sets the URL prefix where the Control UI is served.
  • Examples: "/ui", "/clawdbot", "/apps/clawdbot".
  • Default: root (/) (unchanged).
Related docs: 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).
  • 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).
  • 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.
{
  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 (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)
  • 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)
  • 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)
  • agent.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 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 endpoint 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}}",
        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/gmailgmail).
  • 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 agent.models is set).
Gmail helper config (used by clawdbot hooks gmail setup / run):
{
  hooks: {
    gmail: {
      account: "[email protected]",
      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=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).

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 }
{
  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
{
  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):
clawdbot dns setup --apply
{
  discovery: { wideArea: { enabled: true } }
}

Template variables

Template placeholders are expanded in routing.transcribeAudio.command (and any future templated command fields).
VariableDescription
{{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 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 (whatsapptelegramdiscordimessagewebchat…)

Cron (Gateway scheduler)

Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See Cron jobs for the feature overview and CLI examples.
{
  cron: {
    enabled: true,
    maxConcurrentRuns: 2
  }
}

Next: Agent Runtime 🦞