Files
clawdbot/docs/configuration.md
2026-01-06 00:56:58 +00:00

39 KiB
Raw Blame History

summary, read_when
summary read_when
All configuration options for ~/.clawdbot/clawdbot.json with examples
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 mention behavior (whatsapp.groups, telegram.groups, discord.guilds, routing.groupChat)
  • customize message prefixes (messages)
  • set the agent's workspace (agent.workspace)
  • tune the embedded agent (agent) and session behavior (session)
  • set the agent's identity (identity)

Schema + UI hints

The Gateway exposes a JSON Schema representation of the config via config.schema for UI editors. The Control UI renders a form from this schema, with a Raw JSON editor as an escape hatch.

Hints (labels, grouping, sensitive fields) ship alongside the schema so clients can render better forms without hard-coding config knowledge.

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

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 auth profiles (OAuth + API keys) in:

  • ~/.clawdbot/agent/auth-profiles.json

Legacy OAuth imports:

  • ~/.clawdbot/credentials/oauth.json (or $CLAWDBOT_STATE_DIR/credentials/oauth.json)

The embedded Pi agent maintains a runtime cache at:

  • ~/.clawdbot/agent/auth.json (managed automatically; dont edit manually)

Overrides:

  • OAuth dir (legacy import only): CLAWDBOT_OAUTH_DIR
  • Agent dir: CLAWDBOT_AGENT_DIR (preferred), PI_CODING_AGENT_DIR (legacy)

On first use, Clawdbot imports oauth.json entries into auth-profiles.json.

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:default": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
      "anthropic:work": { provider: "anthropic", mode: "api_key" }
    },
    order: {
      anthropic: ["anthropic:default", "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 havent set them explicitly):

  • messages.responsePrefix from identity.emoji
  • 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, update).

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

logging

  • Default log file: /tmp/clawdbot/clawdbot-YYYY-MM-DD.log
  • If you want a stable path, set logging.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.allowFrom

Allowlist of E.164 phone numbers that may trigger WhatsApp auto-replies (DMs only). If empty, the default allowlist is your own WhatsApp number (self-chat mode).

{
  whatsapp: {
    allowFrom: ["+15555550123", "+447700900123"],
    textChunkLimit: 4000 // optional outbound chunk size (chars)
  }
}

routing.groupChat

Group messages default to require mention (either metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, and iMessage group chats.

Mention types:

  • Metadata mentions: Native platform @-mentions (e.g., WhatsApp tap-to-mention). Ignored in WhatsApp self-chat mode (see whatsapp.allowFrom).
  • Text patterns: Regex patterns defined in mentionPatterns. Always checked regardless of self-chat mode.
  • Mention gating is enforced only when mention detection is possible (native mentions or at least one mentionPattern).
{
  routing: {
    groupChat: {
      mentionPatterns: ["@clawd", "clawdbot", "clawd"],
      historyLimit: 50
    }
  }
}

Mention gating defaults live per provider (whatsapp.groups, telegram.groups, imessage.groups, discord.guilds).

To respond only to specific text triggers (ignoring native @-mentions):

{
  whatsapp: {
    // Include your own number to enable self-chat mode (ignore native @-mentions).
    allowFrom: ["+15555550123"],
    groups: { "*": { requireMention: true } }
  },
  routing: {
    groupChat: {
      // Only these text patterns will trigger responses
      mentionPatterns: ["reisponde", "@clawd"]
    }
  }
}

routing.queue

Controls how inbound messages behave when an agent run is already active.

{
  routing: {
    queue: {
      mode: "collect", // steer | followup | collect | steer-backlog (steer+backlog ok) | interrupt (queue=steer legacy)
      debounceMs: 1000,
      cap: 20,
      drop: "summarize", // old | new | summarize
      bySurface: {
        whatsapp: "collect",
        telegram: "collect",
        discord: "collect",
        imessage: "collect",
        webchat: "collect"
      }
    }
  }
}

web (WhatsApp web provider)

WhatsApp runs through the gateways web provider. It starts automatically when a linked session exists. Set web.enabled: false to keep it off by default.

{
  web: {
    enabled: true,
    heartbeatSeconds: 60,
    reconnect: {
      initialMs: 2000,
      maxMs: 120000,
      factor: 1.4,
      jitter: 0.2,
      maxAttempts: 0
    }
  }
}

telegram (bot transport)

Clawdbot starts Telegram only when a telegram config section exists. The bot token is resolved from TELEGRAM_BOT_TOKEN or telegram.botToken. Set telegram.enabled: false to disable automatic startup.

{
  telegram: {
    enabled: true,
    botToken: "your-bot-token",
    requireMention: true,
    allowFrom: ["123456789"],
    mediaMaxMb: 5,
    proxy: "socks5://localhost:9050",
    webhookUrl: "https://example.com/telegram-webhook",
    webhookSecret: "secret",
    webhookPath: "/telegram-webhook"
  }
}

discord (bot transport)

Configure the Discord bot by setting the bot token and optional gating:

{
  discord: {
    enabled: true,
    token: "your-bot-token",
    mediaMaxMb: 8,                          // clamp inbound media size
    actions: {                              // tool action gates (false disables)
      reactions: true,
      stickers: true,
      polls: true,
      permissions: true,
      messages: true,
      threads: true,
      pins: true,
      search: true,
      memberInfo: true,
      roleInfo: true,
      roles: false,
      channelInfo: true,
      voiceStatus: true,
      events: true,
      moderation: false
    },
    replyToMode: "off",                     // off | first | all
    slashCommand: {                         // user-installed app slash commands
      enabled: true,
      name: "clawd",
      sessionPrefix: "discord:slash",
      ephemeral: true
    },
    dm: {
      enabled: true,                        // disable all DMs when false
      allowFrom: ["1234567890", "steipete"], // optional DM allowlist (ids or names)
      groupEnabled: false,                 // enable group DMs
      groupChannels: ["clawd-dm"]          // optional group DM allowlist
    },
    guilds: {
      "123456789012345678": {               // guild id (preferred) or slug
        slug: "friends-of-clawd",
        requireMention: false,              // per-guild default
        reactionNotifications: "own",       // off | own | all | allowlist
        users: ["987654321098765432"],      // optional per-guild user allowlist
        channels: {
          general: { allow: true },
          help: { allow: true, requireMention: true }
        }
      }
    },
    historyLimit: 20                        // include last N guild messages as context
  }
}

Clawdbot starts Discord only when a discord config section exists. The token is resolved from DISCORD_BOT_TOKEN or discord.token (unless discord.enabled is false). Use user:<id> (DM) or channel:<id> (guild channel) when specifying delivery targets for cron/CLI commands. Guild slugs are lowercase with spaces replaced by -; channel keys use the slugged channel name (no leading #). Prefer guild ids as keys to avoid rename ambiguity. Reaction notification modes:

  • off: no reaction events.
  • own: reactions on the bot's own messages (default).
  • all: all reactions on all messages.
  • allowlist: reactions from guilds.<id>.users on all messages (empty list disables).

slack (socket mode)

Slack runs in Socket Mode and requires both a bot token and app token:

{
  slack: {
    enabled: true,
    botToken: "xoxb-...",
    appToken: "xapp-...",
    dm: {
      enabled: true,
      allowFrom: ["U123", "U456", "*"],
      groupEnabled: false,
      groupChannels: ["G123"]
    },
    channels: {
      C123: { allow: true, requireMention: true },
      "#general": { allow: true, requireMention: false }
    },
    reactionNotifications: "own", // off | own | all | allowlist
    reactionAllowlist: ["U123"],
    actions: {
      reactions: true,
      messages: true,
      pins: true,
      memberInfo: true,
      emojiList: true
    },
    slashCommand: {
      enabled: true,
      name: "clawd",
      sessionPrefix: "slack:slash",
      ephemeral: true
    },
    textChunkLimit: 4000,
    mediaMaxMb: 20
  }
}

Clawdbot starts Slack when the provider is enabled and both tokens are set (via config or SLACK_BOT_TOKEN + SLACK_APP_TOKEN). Use user:<id> (DM) or channel:<id> when specifying delivery targets for cron/CLI commands.

Reaction notification modes:

  • off: no reaction events.
  • own: reactions on the bot's own messages (default).
  • all: all reactions on all messages.
  • allowlist: reactions 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

imessage (imsg CLI)

Clawdbot spawns imsg rpc (JSON-RPC over stdio). No daemon or port required.

{
  imessage: {
    enabled: true,
    cliPath: "imsg",
    dbPath: "~/Library/Messages/chat.db",
    allowFrom: ["+15555550123", "user@example.com", "chat_id:123"],
    includeAttachments: false,
    mediaMaxMb: 16,
    service: "auto",
    region: "US"
  }
}

Notes:

  • Requires Full Disk Access to the Messages DB.
  • The first send will prompt for Messages automation permission.
  • Prefer chat_id:<id> targets. Use imsg chats --limit 20 to list chats.

agent.workspace

Sets the single global workspace directory used by the agent for file operations.

Default: ~/clawd.

{
  agent: { workspace: "~/clawd" }
}

If agent.sandbox is enabled, non-main sessions can override this with their own per-session workspaces under agent.sandbox.workspaceRoot.

agent.userTimezone

Sets the users timezone for system prompt context (not for timestamps in message envelopes). If unset, Clawdbot uses the host timezone at runtime.

{
  agent: { userTimezone: "America/Chicago" }
}

messages

Controls inbound/outbound prefixes.

{
  messages: {
    messagePrefix: "[clawdbot]",
    responsePrefix: "🦞"
  }
}

responsePrefix is applied to all outbound replies (tool summaries, block streaming, final replies) across providers unless already present.

talk

Defaults for Talk mode (macOS/iOS/Android). Voice IDs fall back to ELEVENLABS_VOICE_ID or SAG_VOICE_ID when unset. apiKey falls back to ELEVENLABS_API_KEY (or the gateways 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.

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": {}
    },
    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,
    bash: {
      backgroundMs: 10000,
      timeoutSec: 1800,
      cleanupMs: 1800000
    },
    contextTokens: 200000
  }
}

Block streaming:

  • agent.blockStreamingDefault: "on"/"off" (default on).
  • agent.blockStreamingBreak: "text_end" or "message_end" (default: text_end).
  • agent.blockStreamingChunk: soft chunking for streamed blocks. Defaults to 8001200 chars, prefers paragraph breaks (\n\n), then newlines, then sentences. Example:
    {
      agent: {
        blockStreamingChunk: { minChars: 800, maxChars: 1200 }
      }
    }
    

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. Omit or set 0m to disable.
  • model: optional override model for heartbeat runs (provider/model).
  • target: optional delivery channel (last, whatsapp, telegram, discord, imessage, none). Default: last.
  • to: optional recipient override (E.164 for WhatsApp, chat id for Telegram).
  • prompt: optional override for the heartbeat body (default: HEARTBEAT).
  • ackMaxChars: max chars allowed after HEARTBEAT_OK before delivery (default: 30).

agent.bash configures background bash defaults:

  • backgroundMs: time before auto-background (ms, default 10000)
  • timeoutSec: auto-kill after this runtime (seconds, default 1800)
  • cleanupMs: how long to keep finished sessions in memory (ms, default 1800000)

agent.tools configures a global tool allow/deny policy (deny wins). This is applied even when the Docker sandbox is off.

Example (disable browser/canvas everywhere):

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

agent.elevated controls elevated (host) bash access:

  • enabled: allow elevated mode (default true)
  • allowFrom: per-surface allowlists (empty = disabled)
    • whatsapp: E.164 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"]
      }
    }
  }
}

agent.maxConcurrent sets the maximum number of embedded agent runs that can execute in parallel across sessions. Each session is still serialized (one run per session key at a time). Default: 1.

agent.sandbox

Optional per-session Docker sandboxing for the embedded agent. Intended for non-main sessions so they cannot access your host system.

Defaults (if enabled):

  • one container per session
  • Debian bookworm-slim based image
  • workspace per session under ~/.clawdbot/sandboxes
  • auto-prune: idle > 24h OR age > 7d
  • tools: allow only bash, process, read, write, edit (deny wins)
  • optional sandboxed browser (Chromium + CDP, noVNC observer)
  • hardening knobs: network, user, pidsLimit, memory, cpus, ulimits, seccompProfile, apparmorProfile
{
  agent: {
    sandbox: {
      mode: "non-main", // off | non-main | all
      perSession: true,
      workspaceRoot: "~/.clawdbot/sandboxes",
      docker: {
        image: "clawdbot-sandbox:bookworm-slim",
        containerPrefix: "clawdbot-sbx-",
        workdir: "/workspace",
        readOnlyRoot: true,
        tmpfs: ["/tmp", "/var/tmp", "/run"],
        network: "none",
        user: "1000:1000",
        capDrop: ["ALL"],
        env: { LANG: "C.UTF-8" },
        setupCommand: "apt-get update && apt-get install -y git curl jq",
        pidsLimit: 256,
        memory: "1g",
        memorySwap: "2g",
        cpus: 1,
        ulimits: {
          nofile: { soft: 1024, hard: 2048 },
          nproc: 256
        },
        seccompProfile: "/path/to/seccomp.json",
        apparmorProfile: "clawdbot-sandbox",
        dns: ["1.1.1.1", "8.8.8.8"],
        extraHosts: ["internal.service:10.0.0.5"]
      },
      browser: {
        enabled: false,
        image: "clawdbot-sandbox-browser:bookworm-slim",
        containerPrefix: "clawdbot-sbx-browser-",
        cdpPort: 9222,
        vncPort: 5900,
        noVncPort: 6080,
        headless: false,
        enableNoVnc: true
      },
      tools: {
        allow: ["bash", "process", "read", "write", "edit"],
        deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
      },
      prune: {
        idleHours: 24,  // 0 disables idle pruning
        maxAgeDays: 7   // 0 disables max-age pruning
      }
    }
  }
}

Build the default sandbox image once with:

scripts/sandbox-setup.sh

Note: sandbox containers default to network: "none"; set agent.sandbox.docker.network to "bridge" (or your custom network) if the agent needs outbound access.

Build the optional browser image with:

scripts/sandbox-browser-setup.sh

When agent.sandbox.browser.enabled=true, the browser tool uses a sandboxed Chromium instance (CDP). If noVNC is enabled (default when headless=false), the noVNC URL is injected into the system prompt so the agent can reference it. This does not require browser.enabled in the main config; the sandbox control URL is injected per session.

models (custom providers + base URLs)

Clawdbot uses the pi-coding-agent model catalog. You can add custom providers (LiteLLM, local OpenAI-compatible servers, Anthropic proxies, etc.) by writing ~/.clawdbot/agent/models.json or by defining the same schema inside your Clawdbot config under models.providers.

When models.providers is present, Clawdbot writes/merges a models.json into ~/.clawdbot/agent/ on startup:

  • default behavior: merge (keeps existing providers, overrides on name)
  • set models.mode: "replace" to overwrite the file contents

Select the model via agent.model.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
          }
        ]
      }
    }
  }
}

Best current local setup (what were 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.

session

Controls session scoping, idle expiry, reset triggers, and where the session store is written.

{
  session: {
    scope: "per-sender",
    idleMinutes: 60,
    resetTriggers: ["/new", "/reset"],
    store: "~/.clawdbot/sessions/sessions.json",
    // mainKey is ignored; primary key is fixed to "main"
    agentToAgent: {
      // Max ping-pong reply turns between requester/target (05).
      maxPingPongTurns: 5
    },
    sendPolicy: {
      rules: [
        { action: "deny", match: { surface: "discord", chatType: "group" } }
      ],
      default: "allow"
    }
  }
}

Fields:

  • agentToAgent.maxPingPongTurns: max reply-back turns between requester/target (05, default 5).
  • sendPolicy.default: allow or deny fallback when no rule matches.
  • sendPolicy.rules[]: match by surface (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 its 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).

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.

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 surface on the Gateway HTTP server.

Defaults:

  • enabled: false
  • path: /hooks
  • maxBodyBytes: 262144 (256 KB)
{
  hooks: {
    enabled: true,
    token: "shared-secret",
    path: "/hooks",
    presets: ["gmail"],
    transformsDir: "~/.clawdbot/hooks",
    mappings: [
      {
        match: { path: "gmail" },
        action: "agent",
        wakeMode: "now",
        name: "Gmail",
        sessionKey: "hook:gmail:{{messages[0].id}}",
        messageTemplate:
          "From: {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}",
      },
    ],
  }
}

Requests must include the hook token:

  • Authorization: Bearer <token> 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?, channel?, to?, 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.

Gmail helper config (used by clawdbot hooks gmail setup / run):

{
  hooks: {
    gmail: {
      account: "clawdbot@gmail.com",
      topic: "projects/<project-id>/topics/gog-gmail-watch",
      subscription: "gog-gmail-watch-push",
      pushToken: "shared-push-token",
      hookUrl: "http://127.0.0.1:18789/hooks/gmail",
      includeBody: true,
      maxBytes: 20000,
      renewEveryMinutes: 720,
      serve: { bind: "127.0.0.1", port: 8788, path: "/" },
      tailscale: { mode: "funnel", path: "/gmail-pubsub" },
    }
  }
}

Gateway auto-start:

  • If hooks.enabled=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/WiFi and Tailscale)
  • tailnet: bind only to the machines 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 DNSSD)

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

Variable Description
{{Body}} Full inbound message body
{{BodyStripped}} Body with group mentions stripped (best default for agents)
{{From}} Sender identifier (E.164 for WhatsApp; may differ per surface)
{{To}} Destination identifier
{{MessageSid}} Provider message id (when available)
{{SessionId}} Current session UUID
{{IsNewSession}} "true" when a new session was created
{{MediaUrl}} Inbound media pseudo-URL (if present)
{{MediaPath}} Local media path (if downloaded)
{{MediaType}} Media type (image/audio/document/…)
{{Transcript}} Audio transcript (when enabled)
{{ChatType}} "direct" or "group"
{{GroupSubject}} Group subject (best effort)
{{GroupMembers}} Group members preview (best effort)
{{SenderName}} Sender display name (best effort)
{{SenderE164}} Sender phone number (best effort)
{{Surface}} Surface hint (whatsapp

Cron (Gateway scheduler)

Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See Cron + wakeups for the full RFC and CLI examples.

{
  cron: {
    enabled: true,
    maxConcurrentRuns: 2
  }
}

Next: Agent Runtime 🦞