Skip to main content
Source: https://docs.datzi.ai/gateway/configuration-reference Complete field-by-field reference for ~/.datzi/datzi.json

Configuration Reference

Every field available in ~/.datzi/datzi.json. For a task-oriented overview, see Configuration. Config format is JSON5 (comments + trailing commas allowed). All fields are optional โ€” Datzi uses safe defaults when omitted.

Channels

Each channel starts automatically when its config section exists (unless enabled: false).

DM and group access

All channels support DM policies and group policies:
DM policyBehavior
pairing (default)Unknown senders get a one-time pairing code; owner must approve
allowlistOnly senders in allowFrom (or paired allow store)
openAllow all inbound DMs (requires allowFrom: ["*"])
disabledIgnore all inbound DMs
Group policyBehavior
allowlist (default)Only groups matching the configured allowlist
openBypass group allowlists (mention-gating still applies)
disabledBlock all group/room messages
channels.defaults.groupPolicy sets the default when a providerโ€™s groupPolicy is unset. Pairing codes expire after 1 hour. Pending DM pairing requests are capped at 3 per channel. Slack/Discord have a special fallback: if their provider section is missing entirely, runtime group policy can resolve to open (with a startup warning).

Channel model overrides

Use channels.modelByChannel to pin specific channel IDs to a model. Values accept provider/model or configured model aliases. The channel mapping applies when a session does not already have a model override (for example, set via /model).
{
  channels: {
    modelByChannel: {
      discord: {
        '123456789012345678': 'ollama/qwen3-coder:32b'
      },
      slack: {
        C1234567890: 'ollama/deepseek-r1:32b'
      },
      telegram: {
        '-1001234567890': 'ollama/qwen3-coder:14b',
        '-1001234567890:topic:99': 'ollama/qwen3-coder:14b'
      }
    }
  }
}

WhatsApp

WhatsApp runs through the gatewayโ€™s web channel (Baileys Web). It starts automatically when a linked session exists.
{
  channels: {
    whatsapp: {
      dmPolicy: 'pairing',
      // pairing | allowlist | open | disabled
      allowFrom: ['+15555550123', '+447700900123'],
      textChunkLimit: 4000,
      chunkMode: 'length',
      // length | newline
      mediaMaxMb: 50,
      sendReadReceipts: true,
      // blue ticks (false in self-chat mode)
      groups: {
        '*': {
          requireMention: true
        }
      },
      groupPolicy: 'allowlist',
      groupAllowFrom: ['+15551234567']
    }
  },
  web: {
    enabled: true,
    heartbeatSeconds: 60,
    reconnect: {
      initialMs: 2000,
      maxMs: 120000,
      factor: 1.4,
      jitter: 0.2,
      maxAttempts: 0
    }
  }
}
{
  channels: {
    whatsapp: {
      accounts: {
        default: {},
        personal: {},
        biz: {
          // authDir: "~/.datzi/credentials/whatsapp/biz",
        },
      },
    },
  },
}
  • Outbound commands default to account default if present; otherwise the first configured account id (sorted).
  • Legacy single-account Baileys auth dir is migrated by datzi doctor into whatsapp/default.
  • Per-account overrides: channels.whatsapp.accounts.<id>.sendReadReceipts, channels.whatsapp.accounts.<id>.dmPolicy, channels.whatsapp.accounts.<id>.allowFrom.

Telegram

{
  channels: {
    telegram: {
      enabled: true,
      botToken: 'your-bot-token',
      dmPolicy: 'pairing',
      allowFrom: ['tg:123456789'],
      groups: {
        '*': {
          requireMention: true
        },
        '-1001234567890': {
          allowFrom: ['@admin'],
          systemPrompt: 'Keep answers brief.',
          topics: {
            '99': {
              requireMention: false,
              skills: ['search'],
              systemPrompt: 'Stay on topic.'
            }
          }
        }
      },
      customCommands: [
        {
          command: 'backup',
          description: 'Git backup'
        },
        {
          command: 'generate',
          description: 'Create an image'
        }
      ],
      historyLimit: 50,
      replyToMode: 'first',
      // off | first | all
      linkPreview: true,
      streaming: 'partial',
      // off | partial | block | progress (default: off)
      actions: {
        reactions: true,
        sendMessage: true
      },
      reactionNotifications: 'own',
      // off | own | all
      mediaMaxMb: 5,
      retry: {
        attempts: 3,
        minDelayMs: 400,
        maxDelayMs: 30000,
        jitter: 0.1
      },
      network: {
        autoSelectFamily: false
      },
      proxy: 'socks5://localhost:9050',
      webhookUrl: 'https://example.com/telegram-webhook',
      webhookSecret: 'secret',
      webhookPath: '/telegram-webhook'
    }
  }
}
  • Bot token: channels.telegram.botToken or channels.telegram.tokenFile, with TELEGRAM_BOT_TOKEN as fallback for the default account.
  • configWrites: false blocks Telegram-initiated config writes (supergroup ID migrations, /config set|unset).
  • Telegram stream previews use sendMessage + editMessageText (works in direct and group chats).
  • Retry policy: see Retry policy.

Discord

{
  channels: {
    discord: {
      enabled: true,
      token: 'your-bot-token',
      mediaMaxMb: 8,
      allowBots: false,
      actions: {
        reactions: true,
        stickers: true,
        polls: true,
        permissions: true,
        messages: true,
        threads: true,
        pins: true,
        search: true,
        memberInfo: true,
        roleInfo: true,
        roles: false,
        channelInfo: true,
        voiceStatus: true,
        events: true,
        moderation: false
      },
      replyToMode: 'off',
      // off | first | all
      dmPolicy: 'pairing',
      allowFrom: ['1234567890', 'steipete'],
      dm: {
        enabled: true,
        groupEnabled: false,
        groupChannels: ['datzi-dm']
      },
      guilds: {
        '123456789012345678': {
          slug: 'friends-of-datzi',
          requireMention: false,
          reactionNotifications: 'own',
          users: ['987654321098765432'],
          channels: {
            general: {
              allow: true
            },
            help: {
              allow: true,
              requireMention: true,
              users: ['987654321098765432'],
              skills: ['docs'],
              systemPrompt: 'Short answers only.'
            }
          }
        }
      },
      historyLimit: 20,
      textChunkLimit: 2000,
      chunkMode: 'length',
      // length | newline
      streaming: 'off',
      // off | partial | block | progress (progress maps to partial on Discord)
      maxLinesPerMessage: 17,
      ui: {
        components: {
          accentColor: '#5865F2'
        }
      },
      threadBindings: {
        enabled: true,
        ttlHours: 24,
        spawnSubagentSessions: false
        // opt-in for sessions_spawn({ thread: true })
      },
      voice: {
        enabled: true,
        autoJoin: [
          {
            guildId: '123456789012345678',
            channelId: '234567890123456789'
          }
        ],
        tts: {
          provider: 'openai',
          openai: {
            voice: 'alloy'
          }
        }
      },
      retry: {
        attempts: 3,
        minDelayMs: 500,
        maxDelayMs: 30000,
        jitter: 0.1
      }
    }
  }
}
  • Token: channels.discord.token, with DISCORD_BOT_TOKEN as fallback for the default account.
  • Use user:<id> (DM) or channel:<id> (guild channel) for delivery targets; bare numeric IDs are rejected.
  • Guild slugs are lowercase with spaces replaced by -; channel keys use the slugged name (no #). Prefer guild IDs.
  • Bot-authored messages are ignored by default. allowBots: true enables them (own messages still filtered).
  • maxLinesPerMessage (default 17) splits tall messages even when under 2000 chars.
  • channels.discord.threadBindings controls Discord thread-bound routing:
    • enabled: Discord override for thread-bound session features (/focus, /unfocus, /agents, /session ttl, and bound delivery/routing)
    • ttlHours: Discord override for auto-unfocus TTL (0 disables)
    • spawnSubagentSessions: opt-in switch for sessions_spawn({ thread: true }) auto thread creation/binding
  • channels.discord.ui.components.accentColor sets the accent color for Discord components v2 containers.
  • channels.discord.voice enables Discord voice channel conversations and optional auto-join + TTS overrides.
  • channels.discord.streaming is the canonical stream mode key. Legacy streamMode and boolean streaming values are auto-migrated.
Reaction notification modes: off (none), own (botโ€™s messages, default), all (all messages), allowlist (from guilds.<id>.users on all messages).

Google Chat

{
  channels: {
    googlechat: {
      enabled: true,
      serviceAccountFile: '/path/to/service-account.json',
      audienceType: 'app-url',
      // app-url | project-number
      audience: 'https://gateway.example.com/googlechat',
      webhookPath: '/googlechat',
      botUser: 'users/1234567890',
      dm: {
        enabled: true,
        policy: 'pairing',
        allowFrom: ['users/1234567890']
      },
      groupPolicy: 'allowlist',
      groups: {
        'spaces/AAAA': {
          allow: true,
          requireMention: true
        }
      },
      actions: {
        reactions: true
      },
      typingIndicator: 'message',
      mediaMaxMb: 20
    }
  }
}
  • Service account JSON: inline (serviceAccount) or file-based (serviceAccountFile).
  • Env fallbacks: GOOGLE_CHAT_SERVICE_ACCOUNT or GOOGLE_CHAT_SERVICE_ACCOUNT_FILE.
  • Use spaces/<spaceId> or users/<userId|email> for delivery targets.

Slack

{
  channels: {
    slack: {
      enabled: true,
      botToken: 'xoxb-...',
      appToken: 'xapp-...',
      dmPolicy: 'pairing',
      allowFrom: ['U123', 'U456', '*'],
      dm: {
        enabled: true,
        groupEnabled: false,
        groupChannels: ['G123']
      },
      channels: {
        C123: {
          allow: true,
          requireMention: true,
          allowBots: false
        },
        '#general': {
          allow: true,
          requireMention: true,
          allowBots: false,
          users: ['U123'],
          skills: ['docs'],
          systemPrompt: 'Short answers only.'
        }
      },
      historyLimit: 50,
      allowBots: false,
      reactionNotifications: 'own',
      reactionAllowlist: ['U123'],
      replyToMode: 'off',
      // off | first | all
      thread: {
        historyScope: 'thread',
        // thread | channel
        inheritParent: false
      },
      actions: {
        reactions: true,
        messages: true,
        pins: true,
        memberInfo: true,
        emojiList: true
      },
      slashCommand: {
        enabled: true,
        name: 'datzi',
        sessionPrefix: 'slack:slash',
        ephemeral: true
      },
      textChunkLimit: 4000,
      chunkMode: 'length',
      streaming: 'partial',
      // off | partial | block | progress (preview mode)
      nativeStreaming: true,
      // use Slack native streaming API when streaming=partial
      mediaMaxMb: 20
    }
  }
}
  • Socket mode requires both botToken and appToken (SLACK_BOT_TOKEN + SLACK_APP_TOKEN for default account env fallback).
  • HTTP mode requires botToken plus signingSecret (at root or per-account).
  • configWrites: false blocks Slack-initiated config writes.
  • channels.slack.streaming is the canonical stream mode key. Legacy streamMode and boolean streaming values are auto-migrated.
  • Use user:<id> (DM) or channel:<id> for delivery targets.
Reaction notification modes: off, own (default), all, allowlist (from reactionAllowlist). Thread session isolation: thread.historyScope is per-thread (default) or shared across channel. thread.inheritParent copies parent channel transcript to new threads.
Action groupDefaultNotes
reactionsenabledReact + list reactions
messagesenabledRead/send/edit/delete
pinsenabledPin/unpin/list
memberInfoenabledMember info
emojiListenabledCustom emoji list

Mattermost

Mattermost ships as a plugin: datzi plugins install @datzi/mattermost.
{
  channels: {
    mattermost: {
      enabled: true,
      botToken: 'mm-token',
      baseUrl: 'https://chat.example.com',
      dmPolicy: 'pairing',
      chatmode: 'oncall',
      // oncall | onmessage | onchar
      oncharPrefixes: ['>', '!'],
      textChunkLimit: 4000,
      chunkMode: 'length'
    }
  }
}
Chat modes: oncall (respond on @-mention, default), onmessage (every message), onchar (messages starting with trigger prefix).

Signal

{
  channels: {
    signal: {
      reactionNotifications: 'own',
      // off | own | all | allowlist
      reactionAllowlist: [
        '+15551234567',
        'uuid:123e4567-e89b-12d3-a456-426614174000'
      ],
      historyLimit: 50
    }
  }
}
Reaction notification modes: off, own (default), all, allowlist (from reactionAllowlist).

iMessage

Datzi spawns imsg rpc (JSON-RPC over stdio). No daemon or port required.
{
  channels: {
    imessage: {
      enabled: true,
      cliPath: 'imsg',
      dbPath: '~/Library/Messages/chat.db',
      remoteHost: 'user@gateway-host',
      dmPolicy: 'pairing',
      allowFrom: ['+15555550123', 'user@example.com', 'chat_id:123'],
      historyLimit: 50,
      includeAttachments: false,
      attachmentRoots: ['/Users/*/Library/Messages/Attachments'],
      remoteAttachmentRoots: ['/Users/*/Library/Messages/Attachments'],
      mediaMaxMb: 16,
      service: 'auto',
      region: 'US'
    }
  }
}
  • Requires Full Disk Access to the Messages DB.
  • Prefer chat_id:<id> targets. Use imsg chats --limit 20 to list chats.
  • cliPath can point to an SSH wrapper; set remoteHost (host or user@host) for SCP attachment fetching.
  • attachmentRoots and remoteAttachmentRoots restrict inbound attachment paths (default: /Users/*/Library/Messages/Attachments).
  • SCP uses strict host-key checking, so ensure the relay host key already exists in ~/.ssh/known_hosts.
bash #!/usr/bin/env bash exec ssh -T gateway-host imsg "$@"

Multi-account (all channels)

Run multiple accounts per channel (each with its own accountId):
{
  channels: {
    telegram: {
      accounts: {
        default: {
          name: 'Primary bot',
          botToken: '123456:ABC...'
        },
        alerts: {
          name: 'Alerts bot',
          botToken: '987654:XYZ...'
        }
      }
    }
  }
}
  • default is used when accountId is omitted (CLI + routing).
  • Env tokens only apply to the default account.
  • Base channel settings apply to all accounts unless overridden per account.
  • Use bindings[].match.accountId to route each account to a different agent.

Group chat mention gating

Group messages default to require mention (metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, Google Chat, and iMessage group chats. Mention types:
  • Metadata mentions: Native platform @-mentions. Ignored in WhatsApp self-chat mode.
  • Text patterns: Regex patterns in agents.list[].groupChat.mentionPatterns. Always checked.
  • Mention gating is enforced only when detection is possible (native mentions or at least one pattern).
{
  messages: {
    groupChat: {
      historyLimit: 50
    }
  },
  agents: {
    list: [
      {
        id: 'main',
        groupChat: {
          mentionPatterns: ['@datzi', 'datzi']
        }
      }
    ]
  }
}
messages.groupChat.historyLimit sets the global default. Channels can override withchannels.<channel>.historyLimit ( or per-account). Set 0 to disable.

DM history limits

{
  channels: {
    telegram: {
      dmHistoryLimit: 30,
      dms: {
        '123456789': {
          historyLimit: 50
        }
      }
    }
  }
}
Resolution: per-DM override โ†’ provider default โ†’ no limit (all retained). Supported: telegram, whatsapp, discord, slack, signal, imessage, msteams.

Self-chat mode

Include your own number in allowFrom to enable self-chat mode (ignores native @-mentions, only responds to text patterns):
{
  channels: {
    whatsapp: {
      allowFrom: ['+15555550123'],
      groups: {
        '*': {
          requireMention: true
        }
      }
    }
  },
  agents: {
    list: [
      {
        id: 'main',
        groupChat: {
          mentionPatterns: ['reisponde', '@datzi']
        }
      }
    ]
  }
}

Commands (chat command handling)

{
  commands: {
    native: 'auto',
    // register native commands when supported
    text: true,
    // parse /commands in chat messages
    bash: false,
    // allow ! (alias: /bash)
    bashForegroundMs: 2000,
    config: false,
    // allow /config
    debug: false,
    // allow /debug
    restart: false,
    // allow /restart + gateway restart tool
    allowFrom: {
      '*': ['user1'],
      discord: ['user:123']
    },
    useAccessGroups: true
  }
}
  • Text commands must be standalone messages with leading /.
  • native: "auto" turns on native commands for Discord/Telegram, leaves Slack off.
  • Override per channel: channels.discord.commands.native (bool or "auto"). false clears previously registered commands.
  • channels.telegram.customCommands adds extra Telegram bot menu entries.
  • bash: true enables ! <cmd> for host shell. Requires tools.elevated.enabled and sender in tools.elevated.allowFrom.<channel>.
  • config: true enables /config (reads/writes datzi.json).
  • channels.<provider>.configWrites gates config mutations per channel (default: true).
  • allowFrom is per-provider. When set, it is the only authorization source (channel allowlists/pairing and useAccessGroups are ignored).
  • useAccessGroups: false allows commands to bypass access-group policies when allowFrom is not set.

Agent defaults

agents.defaults.workspace

Default: ~/.datzi/workspace.
{
  agents: {
    defaults: {
      workspace: '~/.datzi/workspace'
    }
  }
}

agents.defaults.repoRoot

Optional repository root shown in the system promptโ€™s Runtime line. If unset, Datzi auto-detects by walking upward from the workspace.
{
  agents: {
    defaults: {
      repoRoot: '~/Projects/datzi'
    }
  }
}

agents.defaults.skipBootstrap

Disables automatic creation of workspace bootstrap files (AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md, HEARTBEAT.md, BOOTSTRAP.md).
{
  agents: {
    defaults: {
      skipBootstrap: true
    }
  }
}

agents.defaults.bootstrapMaxChars

Max characters per workspace bootstrap file before truncation. Default: 20000.
{
  agents: {
    defaults: {
      bootstrapMaxChars: 20000
    }
  }
}

agents.defaults.bootstrapTotalMaxChars

Max total characters injected across all workspace bootstrap files. Default: 150000.
{
  agents: {
    defaults: {
      bootstrapTotalMaxChars: 150000
    }
  }
}

agents.defaults.imageMaxDimensionPx

Max pixel size for the longest image side in transcript/tool image blocks before provider calls. Default: 1200. Lower values usually reduce vision-token usage and request payload size for screenshot-heavy runs. Higher values preserve more visual detail.
{
  agents: {
    defaults: {
      imageMaxDimensionPx: 1200
    }
  }
}

agents.defaults.userTimezone

Timezone for system prompt context (not message timestamps). Falls back to host timezone.
{
  agents: {
    defaults: {
      userTimezone: 'America/Chicago'
    }
  }
}

agents.defaults.timeFormat

Time format in system prompt. Default: auto (OS preference).
{
  agents: {
    defaults: {
      timeFormat: 'auto'
    }
  }
  // auto | 12 | 24
}

agents.defaults.model

{
  agents: {
    defaults: {
      models: {
        'ollama/qwen3-coder:32b': {
          alias: 'opus'
        },
        'minimax/MiniMax-M2.1': {
          alias: 'minimax'
        }
      },
      model: {
        primary: 'ollama/qwen3-coder:32b',
        fallbacks: ['minimax/MiniMax-M2.1']
      },
      imageModel: {
        primary: 'ollama/llava:latest',
        fallbacks: ['ollama/qwen3-coder:14b']
      },
      thinkingDefault: 'low',
      verboseDefault: 'off',
      elevatedDefault: 'on',
      timeoutSeconds: 600,
      mediaMaxMb: 5,
      contextTokens: 200000,
      maxConcurrent: 3
    }
  }
}
  • model.primary: format provider/model (e.g. ollama/qwen3-coder:32b). If you omit the provider, Datzi assumes anthropic (deprecated).
  • models: the configured model catalog and allowlist for /model. Each entry can include alias (shortcut) and params (provider-specific: temperature, maxTokens).
  • imageModel: only used if the primary model lacks image input.
  • maxConcurrent: max parallel agent runs across sessions (each session still serialized). Default: 1.
Built-in alias shorthands (only apply when the model is in agents.defaults.models):
AliasModel
ollamaollama/qwen3-coder:32b
deepseekollama/deepseek-r1:32b
coder14bollama/qwen3-coder:14b
Your configured aliases always win over defaults. Z.AI GLM-4.x models automatically enable thinking mode unless you set --thinking off or define agents.defaults.models["zai/<model>"].params.thinking yourself. Z.AI models enable tool_stream by default for tool call streaming. Set agents.defaults.models["zai/<model>"].params.tool_stream to false to disable it.

agents.defaults.cliBackends

Optional CLI backends for text-only fallback runs (no tool calls). Useful as a backup when API providers fail.
{
  agents: {
    defaults: {
      cliBackends: {
        'claude-cli': {
          command: '/opt/homebrew/bin/claude'
        },
        'my-cli': {
          command: 'my-cli',
          args: ['--json'],
          output: 'json',
          modelArg: '--model',
          sessionArg: '--session',
          sessionMode: 'existing',
          systemPromptArg: '--system',
          systemPromptWhen: 'first',
          imageArg: '--image',
          imageMode: 'repeat'
        }
      }
    }
  }
}
  • CLI backends are text-first; tools are always disabled.
  • Sessions supported when sessionArg is set.
  • Image pass-through supported when imageArg accepts file paths.

agents.defaults.heartbeat

Periodic heartbeat runs.
{
  agents: {
    defaults: {
      heartbeat: {
        every: '30m',
        // 0m disables
        model: 'ollama/qwen3-coder:14b',
        includeReasoning: false,
        session: 'main',
        to: '+15555550123',
        target: 'last',
        // last | whatsapp | telegram | discord | ... | none
        prompt: 'Read HEARTBEAT.md if it exists...',
        ackMaxChars: 300,
        suppressToolErrorWarnings: false
      }
    }
  }
}
  • every: duration string (ms/s/m/h). Default: 30m.
  • suppressToolErrorWarnings: when true, suppresses tool error warning payloads during heartbeat runs.
  • Per-agent: set agents.list[].heartbeat. When any agent defines heartbeat, only those agents run heartbeats.
  • Heartbeats run full agent turns โ€” shorter intervals burn more tokens.

agents.defaults.compaction

{
  agents: {
    defaults: {
      compaction: {
        mode: 'safeguard',
        // default | safeguard
        reserveTokensFloor: 24000,
        memoryFlush: {
          enabled: true,
          softThresholdTokens: 6000,
          systemPrompt: 'Session nearing compaction. Store durable memories now.',
          prompt: 'Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.'
        }
      }
    }
  }
}
  • mode: default or safeguard (chunked summarization for long histories). See Compaction.
  • memoryFlush: silent agentic turn before auto-compaction to store durable memories. Skipped when workspace is read-only.

agents.defaults.contextPruning

Prunes old tool results from in-memory context before sending to the LLM. Does not modify session history on disk.
{
  agents: {
    defaults: {
      contextPruning: {
        mode: 'cache-ttl',
        // off | cache-ttl
        ttl: '1h',
        // duration (ms/s/m/h), default unit: minutes
        keepLastAssistants: 3,
        softTrimRatio: 0.3,
        hardClearRatio: 0.5,
        minPrunableToolChars: 50000,
        softTrim: {
          maxChars: 4000,
          headChars: 1500,
          tailChars: 1500
        },
        hardClear: {
          enabled: true,
          placeholder: '[Old tool result content cleared]'
        },
        tools: {
          deny: ['browser', 'canvas']
        }
      }
    }
  }
}
  • mode: "cache-ttl" enables pruning passes.
  • ttl controls how often pruning can run again (after the last cache touch).
  • Pruning soft-trims oversized tool results first, then hard-clears older tool results if needed.
Soft-trim keeps beginning + end and inserts ... in the middle.Hard-clear replaces the entire tool result with the placeholder.Notes:
  • Image blocks are never trimmed/cleared.
  • Ratios are character-based (approximate), not exact token counts.
  • If fewer than keepLastAssistants assistant messages exist, pruning is skipped.
See Session Pruning for behavior details.

Block streaming

{
  agents: {
    defaults: {
      blockStreamingDefault: 'off',
      // on | off
      blockStreamingBreak: 'text_end',
      // text_end | message_end
      blockStreamingChunk: {
        minChars: 800,
        maxChars: 1200
      },
      blockStreamingCoalesce: {
        idleMs: 1000
      },
      humanDelay: {
        mode: 'natural'
      }
      // off | natural | custom (use minMs/maxMs)
    }
  }
}
  • Non-Telegram channels require explicit *.blockStreaming: true to enable block replies.
  • Channel overrides: channels.<channel>.blockStreamingCoalesce (and per-account variants). Signal/Slack/Discord/Google Chat default minChars: 1500.
  • humanDelay: randomized pause between block replies. natural = 800โ€“2500ms. Per-agent override: agents.list[].humanDelay.
See Streaming for behavior + chunking details.

Typing indicators

{
  agents: {
    defaults: {
      typingMode: 'instant',
      // never | instant | thinking | message
      typingIntervalSeconds: 6
    }
  }
}
  • Defaults: instant for direct chats/mentions, message for unmentioned group chats.
  • Per-session overrides: session.typingMode, session.typingIntervalSeconds.
See Typing Indicators.

agents.defaults.sandbox

Optional Docker sandboxing for the embedded agent. See Sandboxing for the full guide.
{
  agents: {
    defaults: {
      sandbox: {
        mode: 'non-main',
        // off | non-main | all
        scope: 'agent',
        // session | agent | shared
        workspaceAccess: 'none',
        // none | ro | rw
        workspaceRoot: '~/.datzi/sandboxes',
        docker: {
          image: 'datzi-sandbox:bookworm-slim',
          containerPrefix: 'datzi-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: 'datzi-sandbox',
          dns: ['1.1.1.1', '8.8.8.8'],
          extraHosts: ['internal.service:10.0.0.5'],
          binds: ['/home/user/source:/source:rw']
        },
        browser: {
          enabled: false,
          image: 'datzi-sandbox-browser:bookworm-slim',
          network: 'datzi-sandbox-browser',
          cdpPort: 9222,
          cdpSourceRange: '172.21.0.1/32',
          vncPort: 5900,
          noVncPort: 6080,
          headless: false,
          enableNoVnc: true,
          allowHostControl: false,
          autoStart: true,
          autoStartTimeoutMs: 12000
        },
        prune: {
          idleHours: 24,
          maxAgeDays: 7
        }
      }
    }
  },
  tools: {
    sandbox: {
      tools: {
        allow: [
          'exec',
          'process',
          'read',
          'write',
          'edit',
          'apply_patch',
          'sessions_list',
          'sessions_history',
          'sessions_send',
          'sessions_spawn',
          'session_status'
        ],
        deny: ['browser', 'canvas', 'nodes', 'cron', 'discord', 'gateway']
      }
    }
  }
}
Workspace access:
  • none: per-scope sandbox workspace under ~/.datzi/sandboxes
  • ro: sandbox workspace at /workspace, agent workspace mounted read-only at /agent
  • rw: agent workspace mounted read/write at /workspace
Scope:
  • session: per-session container + workspace
  • agent: one container + workspace per agent (default)
  • shared: shared container and workspace (no cross-session isolation)
setupCommand runs once after container creation (via sh -lc). Needs network egress, writable root, root user.Containers default to network: "none" โ€” set to "bridge" if the agent needs outbound access.Inbound attachments are staged into media/inbound/* in the active workspace.docker.binds mounts additional host directories; global and per-agent binds are merged.Sandboxed browser (sandbox.browser.enabled): Chromium + CDP in a container. noVNC URL injected into system prompt. Does not require browser.enabled in main config. noVNC observer access uses VNC auth by default and Datzi emits a short-lived token URL (instead of exposing the password in the shared URL).
  • allowHostControl: false (default) blocks sandboxed sessions from targeting the host browser.
  • network defaults to datzi-sandbox-browser (dedicated bridge network). Set to bridge only when you explicitly want global bridge connectivity.
  • cdpSourceRange optionally restricts CDP ingress at the container edge to a CIDR range (for example 172.21.0.1/32).
  • sandbox.browser.binds mounts additional host directories into the sandbox browser container only. When set ( including []), it replaces docker.binds for the browser container.
Build images:
scripts/sandbox-setup.sh           # main sandbox image
scripts/sandbox-browser-setup.sh   # optional browser image

agents.list (per-agent overrides)

{
  agents: {
    list: [
      {
        id: 'main',
        default: true,
        name: 'Main Agent',
        workspace: '~/.datzi/workspace',
        agentDir: '~/.datzi/agents/main/agent',
        model: 'ollama/qwen3-coder:32b',
        // or { primary, fallbacks }
        identity: {
          name: 'Samantha',
          theme: 'helpful sloth',
          emoji: '๐Ÿฆฅ',
          avatar: 'avatars/samantha.png'
        },
        groupChat: {
          mentionPatterns: ['@datzi']
        },
        sandbox: {
          mode: 'off'
        },
        subagents: {
          allowAgents: ['*']
        },
        tools: {
          profile: 'coding',
          allow: ['browser'],
          deny: ['canvas'],
          elevated: {
            enabled: true
          }
        }
      }
    ]
  }
}
  • id: stable agent id (required).
  • default: when multiple are set, first wins (warning logged). If none set, first list entry is default.
  • model: string form overrides primary only; object form { primary, fallbacks } overrides both ([] disables global fallbacks). Cron jobs that only override primary still inherit default fallbacks unless you set fallbacks: [].
  • identity.avatar: workspace-relative path, http(s) URL, or data: URI.
  • identity derives defaults: ackReaction from emoji, mentionPatterns from name/emoji.
  • subagents.allowAgents: allowlist of agent ids for sessions_spawn (["*"] = any; default: same agent only).

Multi-agent routing

Run multiple isolated agents inside one Gateway. See Multi-Agent.
{
  agents: {
    list: [
      {
        id: 'home',
        default: true,
        workspace: '~/.datzi/workspace-home'
      },
      {
        id: 'work',
        workspace: '~/.datzi/workspace-work'
      }
    ]
  },
  bindings: [
    {
      agentId: 'home',
      match: {
        channel: 'whatsapp',
        accountId: 'personal'
      }
    },
    {
      agentId: 'work',
      match: {
        channel: 'whatsapp',
        accountId: 'biz'
      }
    }
  ]
}

Binding match fields

  • match.channel (required)
  • match.accountId (optional; * = any account; omitted = default account)
  • match.peer (optional; { kind: direct|group|channel, id })
  • match.guildId / match.teamId (optional; channel-specific)
Deterministic match order:
  1. match.peer
  2. match.guildId
  3. match.teamId
  4. match.accountId (exact, no peer/guild/team)
  5. match.accountId: "*" (channel-wide)
  6. Default agent
Within each tier, the first matching bindings entry wins.

Per-agent access profiles

{
  agents: {
    list: [
      {
        id: "personal",
        workspace: "~/.datzi/workspace-personal",
        sandbox: { mode: "off" },
      },
    ],
  },
}
{
  agents: {
    list: [
      {
        id: "family",
        workspace: "~/.datzi/workspace-family",
        sandbox: { mode: "all", scope: "agent", workspaceAccess: "ro" },
        tools: {
          allow: [
            "read",
            "sessions_list",
            "sessions_history",
            "sessions_send",
            "sessions_spawn",
            "session_status",
          ],
          deny: ["write", "edit", "apply_patch", "exec", "process", "browser"],
        },
      },
    ],
  },
}
{
  agents: {
    list: [
      {
        id: "public",
        workspace: "~/.datzi/workspace-public",
        sandbox: { mode: "all", scope: "agent", workspaceAccess: "none" },
        tools: {
          allow: [
            "sessions_list",
            "sessions_history",
            "sessions_send",
            "sessions_spawn",
            "session_status",
            "whatsapp",
            "telegram",
            "slack",
            "discord",
            "gateway",
          ],
          deny: [
            "read",
            "write",
            "edit",
            "apply_patch",
            "exec",
            "process",
            "browser",
            "canvas",
            "nodes",
            "cron",
            "gateway",
            "image",
          ],
        },
      },
    ],
  },
}
See Multi-Agent Sandbox & Tools for precedence details.

Session

{
  session: {
    scope: 'per-sender',
    dmScope: 'main',
    // main | per-peer | per-channel-peer | per-account-channel-peer
    identityLinks: {
      alice: ['telegram:123456789', 'discord:987654321012345678']
    },
    reset: {
      mode: 'daily',
      // daily | idle
      atHour: 4,
      idleMinutes: 60
    },
    resetByType: {
      thread: {
        mode: 'daily',
        atHour: 4
      },
      direct: {
        mode: 'idle',
        idleMinutes: 240
      },
      group: {
        mode: 'idle',
        idleMinutes: 120
      }
    },
    resetTriggers: ['/new', '/reset'],
    store: '~/.datzi/agents/{agentId}/sessions/sessions.json',
    maintenance: {
      mode: 'warn',
      // warn | enforce
      pruneAfter: '30d',
      maxEntries: 500,
      rotateBytes: '10mb'
    },
    threadBindings: {
      enabled: true,
      ttlHours: 24
      // default auto-unfocus TTL for thread-bound sessions (0 disables)
    },
    mainKey: 'main',
    // legacy (runtime always uses "main")
    agentToAgent: {
      maxPingPongTurns: 5
    },
    sendPolicy: {
      rules: [
        {
          action: 'deny',
          match: {
            channel: 'discord',
            chatType: 'group'
          }
        }
      ],
      default: 'allow'
    }
  }
}
  • dmScope: how DMs are grouped. * main: all DMs share the main session. * per-peer: isolate by sender id across channels. * per-channel-peer: isolate per channel + sender (recommended for multi-user inboxes). * per-account-channel-peer: isolate per account + channel + sender (recommended for multi-account). * identityLinks: map canonical ids to provider-prefixed peers for cross-channel session sharing. * reset: primary reset policy. daily resets at atHour local time; idle resets after idleMinutes. When both configured, whichever expires first wins. * resetByType: per-type overrides (direct, group, thread). Legacy dm accepted as alias for direct. * mainKey: legacy field. Runtime now always uses "main" for the main direct-chat bucket. * sendPolicy: match by channel, chatType (direct|group|channel, with legacy dm alias), keyPrefix, or rawKeyPrefix. First deny wins. * maintenance: warn warns the active session on eviction; enforce applies pruning and rotation. * threadBindings: global defaults for thread-bound session features. * enabled: master default switch (providers can override; Discord uses channels.discord.threadBindings.enabled) * ttlHours: default auto-unfocus TTL in hours (0 disables; providers can override)

Messages

{
  messages: {
    responsePrefix: '๐Ÿฆž',
    // or "auto"
    ackReaction: '๐Ÿ‘€',
    ackReactionScope: 'group-mentions',
    // group-mentions | group-all | direct | all
    removeAckAfterReply: false,
    queue: {
      mode: 'collect',
      // steer | followup | collect | steer-backlog | steer+backlog | queue | interrupt
      debounceMs: 1000,
      cap: 20,
      drop: 'summarize',
      // old | new | summarize
      byChannel: {
        whatsapp: 'collect',
        telegram: 'collect'
      }
    },
    inbound: {
      debounceMs: 2000,
      // 0 disables
      byChannel: {
        whatsapp: 5000,
        slack: 1500
      }
    }
  }
}

Response prefix

Per-channel/account overrides: channels.<channel>.responsePrefix, channels.<channel>.accounts.<id>.responsePrefix. Resolution (most specific wins): account โ†’ channel โ†’ global. "" disables and stops cascade. "auto" derives [{identity.name}]. Template variables:
VariableDescriptionExample
{model}Short model nameqwen3-coder:32b
{modelFull}Full model identifierollama/qwen3-coder:32b
{provider}Provider nameanthropic
{thinkingLevel}Current thinking levelhigh, low, off
{identity.name}Agent identity name(same as "auto")
Variables are case-insensitive. {think} is an alias for {thinkingLevel}.

Ack reaction

  • Defaults to active agentโ€™s identity.emoji, otherwise "๐Ÿ‘€". Set "" to disable.
  • Per-channel overrides: channels.<channel>.ackReaction, channels.<channel>.accounts.<id>.ackReaction.
  • Resolution order: account โ†’ channel โ†’ messages.ackReaction โ†’ identity fallback.
  • Scope: group-mentions (default), group-all, direct, all.
  • removeAckAfterReply: removes ack after reply (Slack/Discord/Telegram/Google Chat only).

Inbound debounce

Batches rapid text-only messages from the same sender into a single agent turn. Media/attachments flush immediately. Control commands bypass debouncing.

TTS (text-to-speech)

{
  messages: {
    tts: {
      auto: 'always',
      // off | always | inbound | tagged
      mode: 'final',
      // final | all
      provider: 'elevenlabs',
      summaryModel: 'ollama/qwen3-coder:14b',
      modelOverrides: {
        enabled: true
      },
      maxTextLength: 4000,
      timeoutMs: 30000,
      prefsPath: '~/.datzi/settings/tts.json',
      elevenlabs: {
        apiKey: 'elevenlabs_api_key',
        baseUrl: 'https://api.elevenlabs.io',
        voiceId: 'voice_id',
        modelId: 'eleven_multilingual_v2',
        seed: 42,
        applyTextNormalization: 'auto',
        languageCode: 'en',
        voiceSettings: {
          stability: 0.5,
          similarityBoost: 0.75,
          style: 0.0,
          useSpeakerBoost: true,
          speed: 1.0
        }
      },
      openai: {
        apiKey: 'openai_api_key',
        model: 'gpt-4o-mini-tts',
        voice: 'alloy'
      }
    }
  }
}
  • auto controls auto-TTS. /tts off|always|inbound|tagged overrides per session.
  • summaryModel overrides agents.defaults.model.primary for auto-summary.
  • modelOverrides is enabled by default; modelOverrides.allowProvider defaults to false (opt-in).
  • API keys fall back to ELEVENLABS_API_KEY/XI_API_KEY and OPENAI_API_KEY.

Talk

Defaults for Talk mode (macOS/iOS/Android).
{
  talk: {
    voiceId: 'elevenlabs_voice_id',
    voiceAliases: {
      Clawd: 'EXAVITQu4vr4xnSDxMaL',
      Roger: 'CwhRBWXzGAHq8TQ4Fs17'
    },
    modelId: 'eleven_v3',
    outputFormat: 'mp3_44100_128',
    apiKey: 'elevenlabs_api_key',
    interruptOnSpeech: true
  }
}
  • Voice IDs fall back to ELEVENLABS_VOICE_ID or SAG_VOICE_ID.
  • apiKey falls back to ELEVENLABS_API_KEY.
  • voiceAliases lets Talk directives use friendly names.

Tools

Tool profiles

tools.profile sets a base allowlist before tools.allow/tools.deny:
ProfileIncludes
minimalsession_status only
codinggroup:fs, group:runtime, group:sessions, group:memory, image
messaginggroup:messaging, sessions_list, sessions_history, sessions_send, session_status
fullNo restriction (same as unset)

Tool groups

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

tools.allow / tools.deny

Global tool allow/deny policy (deny wins). Case-insensitive, supports * wildcards. Applied even when Docker sandbox is off.
{
  tools: {
    deny: ['browser', 'canvas']
  }
}

tools.byProvider

Further restrict tools for specific providers or models. Order: base profile โ†’ provider profile โ†’ allow/deny.
{
  tools: {
    profile: 'coding',
    byProvider: {
      'google-antigravity': {
        profile: 'minimal'
      },
      'ollama/deepseek-r1:32b': {
        allow: ['group:fs', 'sessions_list']
      }
    }
  }
}

tools.elevated

Controls elevated (host) exec access:
{
  tools: {
    elevated: {
      enabled: true,
      allowFrom: {
        whatsapp: ['+15555550123'],
        discord: ['steipete', '1234567890123']
      }
    }
  }
}
  • Per-agent override (agents.list[].tools.elevated) can only further restrict.
  • /elevated on|off|ask|full stores state per session; inline directives apply to single message.
  • Elevated exec runs on the host, bypasses sandboxing.

tools.exec

{
  tools: {
    exec: {
      backgroundMs: 10000,
      timeoutSec: 1800,
      cleanupMs: 1800000,
      notifyOnExit: true,
      notifyOnExitEmptySuccess: false,
      applyPatch: {
        enabled: false,
        allowModels: ['ollama/deepseek-r1:32b']
      }
    }
  }
}

tools.loopDetection

Tool-loop safety checks are disabled by default. Set enabled: true to activate detection. Settings can be defined globally in tools.loopDetection and overridden per-agent at agents.list[].tools.loopDetection.
{
  tools: {
    loopDetection: {
      enabled: true,
      historySize: 30,
      warningThreshold: 10,
      criticalThreshold: 20,
      globalCircuitBreakerThreshold: 30,
      detectors: {
        genericRepeat: true,
        knownPollNoProgress: true,
        pingPong: true
      }
    }
  }
}
  • historySize: max tool-call history retained for loop analysis.
  • warningThreshold: repeating no-progress pattern threshold for warnings.
  • criticalThreshold: higher repeating threshold for blocking critical loops.
  • globalCircuitBreakerThreshold: hard stop threshold for any no-progress run.
  • detectors.genericRepeat: warn on repeated same-tool/same-args calls.
  • detectors.knownPollNoProgress: warn/block on known poll tools (process.poll, command_status, etc.).
  • detectors.pingPong: warn/block on alternating no-progress pair patterns.
  • If warningThreshold >= criticalThreshold or criticalThreshold >= globalCircuitBreakerThreshold, validation fails.

tools.web

{
  tools: {
    web: {
      search: {
        enabled: true,
        apiKey: 'brave_api_key',
        // or BRAVE_API_KEY env
        maxResults: 5,
        timeoutSeconds: 30,
        cacheTtlMinutes: 15
      },
      fetch: {
        enabled: true,
        maxChars: 50000,
        maxCharsCap: 50000,
        timeoutSeconds: 30,
        cacheTtlMinutes: 15,
        userAgent: 'custom-ua'
      }
    }
  }
}

tools.media

Configures inbound media understanding (image/audio/video):
{
  tools: {
    media: {
      concurrency: 2,
      audio: {
        enabled: true,
        maxBytes: 20971520,
        scope: {
          default: 'deny',
          rules: [
            {
              action: 'allow',
              match: {
                chatType: 'direct'
              }
            }
          ]
        },
        models: [
          {
            provider: 'openai',
            model: 'gpt-4o-mini-transcribe'
          },
          {
            type: 'cli',
            command: 'whisper',
            args: ['--model', 'base', '{{MediaPath}}']
          }
        ]
      },
      video: {
        enabled: true,
        maxBytes: 52428800,
        models: [
          {
            provider: 'google',
            model: 'gemini-3-flash-preview'
          }
        ]
      }
    }
  }
}
Provider entry (type: "provider" or omitted):
  • provider: API provider id (openai, anthropic, google/gemini, groq, etc.)
  • model: model id override
  • profile / preferredProfile: auth profile selection
CLI entry (type: "cli"):
  • command: executable to run
  • args: templated args (supports {{MediaPath}}, {{Prompt}}, {{MaxChars}}, etc.)
Common fields:
  • capabilities: optional list (image, audio, video). Defaults: openai/anthropic/minimax โ†’ image,google โ†’ image+audio+video, groq โ†’ audio.
  • prompt, maxChars, maxBytes, timeoutSeconds, language: per-entry overrides.
  • Failures fall back to the next entry.
Provider auth follows standard order: auth profiles โ†’ env vars โ†’ models.providers.*.apiKey.

tools.agentToAgent

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

tools.sessions

Controls which sessions can be targeted by the session tools (sessions_list, sessions_history, sessions_send). Default: tree (current session + sessions spawned by it, such as subagents).
{
  tools: {
    sessions: {
      // "self" | "tree" | "agent" | "all"
      visibility: 'tree'
    }
  }
}
Notes:
  • self: only the current session key.
  • tree: current session + sessions spawned by the current session (subagents).
  • agent: any session belonging to the current agent id (can include other users if you run per-sender sessions under the same agent id).
  • all: any session. Cross-agent targeting still requires tools.agentToAgent.
  • Sandbox clamp: when the current session is sandboxed and agents.defaults.sandbox.sessionToolsVisibility="spawned", visibility is forced to tree even if tools.sessions.visibility="all".

tools.subagents

{
  agents: {
    defaults: {
      subagents: {
        model: 'minimax/MiniMax-M2.1',
        maxConcurrent: 1,
        archiveAfterMinutes: 60
      }
    }
  }
}
  • model: default model for spawned sub-agents. If omitted, sub-agents inherit the callerโ€™s model.
  • Per-subagent tool policy: tools.subagents.tools.allow / tools.subagents.tools.deny.

Custom providers and base URLs

Datzi uses the pi-coding-agent model catalog. Add custom providers via models.providers in config or ~/.datzi/agents/<agentId>/agent/models.json.
{
  models: {
    mode: 'merge',
    // merge (default) | replace
    providers: {
      'custom-proxy': {
        baseUrl: 'http://localhost:4000/v1',
        apiKey: 'LITELLM_KEY',
        api: 'openai-completions',
        // openai-completions | openai-responses | anthropic-messages | google-generative-ai
        models: [
          {
            id: 'llama-3.1-8b',
            name: 'Llama 3.1 8B',
            reasoning: false,
            input: ['text'],
            cost: {
              input: 0,
              output: 0,
              cacheRead: 0,
              cacheWrite: 0
            },
            contextWindow: 128000,
            maxTokens: 32000
          }
        ]
      }
    }
  }
}
  • Use authHeader: true + headers for custom auth needs.
  • Override agent config root with DATZI_AGENT_DIR (or PI_CODING_AGENT_DIR).

Provider examples

{
  env: { CEREBRAS_API_KEY: "sk-..." },
  agents: {
    defaults: {
      model: {
        primary: "cerebras/zai-glm-4.7",
        fallbacks: ["cerebras/zai-glm-4.6"],
      },
      models: {
        "cerebras/zai-glm-4.7": { alias: "GLM 4.7 (Cerebras)" },
        "cerebras/zai-glm-4.6": { alias: "GLM 4.6 (Cerebras)" },
      },
    },
  },
  models: {
    mode: "merge",
    providers: {
      cerebras: {
        baseUrl: "https://api.cerebras.ai/v1",
        apiKey: "${CEREBRAS_API_KEY}",
        api: "openai-completions",
        models: [
          { id: "zai-glm-4.7", name: "GLM 4.7 (Cerebras)" },
          { id: "zai-glm-4.6", name: "GLM 4.6 (Cerebras)" },
        ],
      },
    },
  },
}
Use cerebras/zai-glm-4.7 for Cerebras; zai/glm-4.7 for Z.AI direct.
{
  agents: {
    defaults: {
      model: { primary: "ollama/qwen3-coder:32b" },
      models: { "ollama/qwen3-coder:32b": { alias: "Qwen3" } },
    },
  },
}
Set OPENCODE_API_KEY (or OPENCODE_ZEN_API_KEY). Shortcut: datzi onboard --auth-choice opencode-zen.
{
  agents: {
    defaults: {
      model: { primary: "zai/glm-4.7" },
      models: { "zai/glm-4.7": {} },
    },
  },
}
Set ZAI_API_KEY. z.ai/* and z-ai/* are accepted aliases. Shortcut: datzi onboard --auth-choice zai-api-key.
  • General endpoint: https://api.z.ai/api/paas/v4
  • Coding endpoint (default): https://api.z.ai/api/coding/paas/v4
  • For the general endpoint, define a custom provider with the base URL override.
{
  env: { MOONSHOT_API_KEY: "sk-..." },
  agents: {
    defaults: {
      model: { primary: "moonshot/kimi-k2.5" },
      models: { "moonshot/kimi-k2.5": { alias: "Kimi K2.5" } },
    },
  },
  models: {
    mode: "merge",
    providers: {
      moonshot: {
        baseUrl: "https://api.moonshot.ai/v1",
        apiKey: "${MOONSHOT_API_KEY}",
        api: "openai-completions",
        models: [
          {
            id: "kimi-k2.5",
            name: "Kimi K2.5",
            reasoning: false,
            input: ["text"],
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
            contextWindow: 256000,
            maxTokens: 8192,
          },
        ],
      },
    },
  },
}
For the China endpoint: baseUrl: "https://api.moonshot.cn/v1" or datzi onboard --auth-choice moonshot-api-key-cn.
{
  env: { KIMI_API_KEY: "sk-..." },
  agents: {
    defaults: {
      model: { primary: "kimi-coding/k2p5" },
      models: { "kimi-coding/k2p5": { alias: "Kimi K2.5" } },
    },
  },
}
Anthropic-compatible, built-in provider. Shortcut: datzi onboard --auth-choice kimi-code-api-key.
{
  env: { SYNTHETIC_API_KEY: "sk-..." },
  agents: {
    defaults: {
      model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.1" },
      models: { "synthetic/hf:MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax M2.1" } },
    },
  },
  models: {
    mode: "merge",
    providers: {
      synthetic: {
        baseUrl: "https://api.synthetic.new/anthropic",
        apiKey: "${SYNTHETIC_API_KEY}",
        api: "anthropic-messages",
        models: [
          {
            id: "hf:MiniMaxAI/MiniMax-M2.1",
            name: "MiniMax M2.1",
            reasoning: false,
            input: ["text"],
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
            contextWindow: 192000,
            maxTokens: 65536,
          },
        ],
      },
    },
  },
}
Base URL should omit /v1 (Anthropic client appends it). Shortcut: datzi onboard --auth-choice synthetic-api-key.
{
  agents: {
    defaults: {
      model: { primary: "minimax/MiniMax-M2.1" },
      models: {
        "minimax/MiniMax-M2.1": { alias: "Minimax" },
      },
    },
  },
  models: {
    mode: "merge",
    providers: {
      minimax: {
        baseUrl: "https://api.minimax.io/anthropic",
        apiKey: "${MINIMAX_API_KEY}",
        api: "anthropic-messages",
        models: [
          {
            id: "MiniMax-M2.1",
            name: "MiniMax M2.1",
            reasoning: false,
            input: ["text"],
            cost: { input: 15, output: 60, cacheRead: 2, cacheWrite: 10 },
            contextWindow: 200000,
            maxTokens: 8192,
          },
        ],
      },
    },
  },
}
Set MINIMAX_API_KEY. Shortcut: datzi onboard --auth-choice minimax-api.
See Local Models. TL;DR: run MiniMax M2.1 via LM Studio Responses API on serious hardware; keep hosted models merged for fallback.

Skills

{
  skills: {
    allowBundled: ['gemini', 'peekaboo'],
    load: {
      extraDirs: ['~/Projects/agent-scripts/skills']
    },
    install: {
      preferBrew: true,
      nodeManager: 'npm'
      // npm | pnpm | yarn
    },
    entries: {
      'nano-banana-pro': {
        apiKey: 'GEMINI_KEY_HERE',
        env: {}
      },
      peekaboo: {
        enabled: true
      },
      sag: {
        enabled: false
      }
    }
  }
}
  • allowBundled: optional allowlist for bundled skills only (managed/workspace skills unaffected).
  • entries.<skillKey>.enabled: false disables a skill even if bundled/installed.
  • entries.<skillKey>.apiKey: convenience for skills declaring a primary env var.

Plugins

{
  plugins: {
    enabled: true,
    allow: ['voice-call'],
    deny: [],
    load: {
      paths: ['~/Projects/oss/voice-call-extension']
    },
    entries: {
      'voice-call': {
        enabled: true,
        config: {
          provider: 'twilio'
        }
      }
    }
  }
}
  • Loaded from ~/.datzi/extensions, <workspace>/.datzi/extensions, plus plugins.load.paths.
  • Config changes require a gateway restart.
  • allow: optional allowlist (only listed plugins load). deny wins.
See Plugins.

Browser

{
  browser: {
    enabled: true,
    evaluateEnabled: true,
    defaultProfile: 'chrome',
    profiles: {
      datzi: {
        cdpPort: 18800,
        color: '#FF4500'
      },
      work: {
        cdpPort: 18801,
        color: '#0066CC'
      },
      remote: {
        cdpUrl: 'http://10.0.0.42:9222',
        color: '#00AA00'
      }
    },
    color: '#FF4500'
    // headless: false,
    // noSandbox: false,
    // executablePath: "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
    // attachOnly: false,
  }
}
  • evaluateEnabled: false disables act:evaluate and wait --fn.
  • Remote profiles are attach-only (start/stop/reset disabled).
  • Auto-detect order: default browser if Chromium-based โ†’ Chrome โ†’ Brave โ†’ Edge โ†’ Chromium โ†’ Chrome Canary.
  • Control service: loopback only (port derived from gateway.port, default 18791).

UI

{
  ui: {
    seamColor: '#FF4500',
    assistant: {
      name: 'Datzi',
      avatar: 'CB'
      // emoji, short text, image URL, or data URI
    }
  }
}
  • seamColor: accent color for native app UI chrome (Talk Mode bubble tint, etc.).
  • assistant: Control UI identity override. Falls back to active agent identity.

Gateway

{
  gateway: {
    mode: 'local',
    // local | remote
    port: 18789,
    bind: 'loopback',
    auth: {
      mode: 'token',
      // none | token | password | trusted-proxy
      token: 'your-token',
      // password: "your-password", // or DATZI_GATEWAY_PASSWORD
      // trustedProxy: { userHeader: "x-forwarded-user" }, // for mode=trusted-proxy; see /gateway/trusted-proxy-auth
      allowTailscale: true,
      rateLimit: {
        maxAttempts: 10,
        windowMs: 60000,
        lockoutMs: 300000,
        exemptLoopback: true
      }
    },
    tailscale: {
      mode: 'off',
      // off | serve | funnel
      resetOnExit: false
    },
    controlUi: {
      enabled: true,
      basePath: '/datzi'
      // root: "dist/control-ui",
      // allowInsecureAuth: false,
      // dangerouslyDisableDeviceAuth: false,
    },
    remote: {
      url: 'ws://gateway.tailnet:18789',
      transport: 'ssh',
      // ssh | direct
      token: 'your-token'
      // password: "your-password",
    },
    trustedProxies: ['10.0.0.1'],
    // Optional. Default false.
    allowRealIpFallback: false,
    tools: {
      // Additional /tools/invoke HTTP denies
      deny: ['browser'],
      // Remove tools from the default HTTP deny list
      allow: ['gateway']
    }
  }
}
  • mode: local (run gateway) or remote (connect to remote gateway). Gateway refuses to start unless local.
  • port: single multiplexed port for WS + HTTP. Precedence: --port > DATZI_GATEWAY_PORT > gateway.port > 18789.
  • bind: auto, loopback (default), lan (0.0.0.0), tailnet (Tailscale IP only), or custom.
  • Auth: required by default. Non-loopback binds require a shared token/password. Onboarding wizard generates a token by default.
  • auth.mode: "none": explicit no-auth mode. Use only for trusted local loopback setups; this is intentionally not offered by onboarding prompts.
  • auth.mode: "trusted-proxy": delegate auth to an identity-aware reverse proxy and trust identity headers from gateway.trustedProxies (see Trusted Proxy Auth).
  • auth.allowTailscale: when true, Tailscale Serve identity headers can satisfy Control UI/WebSocket auth (verified via tailscale whois); HTTP API endpoints still require token/password auth. This tokenless flow assumes the gateway host is trusted. Defaults to true when tailscale.mode = "serve".
  • auth.rateLimit: optional failed-auth limiter. Applies per client IP and per auth scope (shared-secret and device-token are tracked independently). Blocked attempts return 429 + Retry-After.
    • auth.rateLimit.exemptLoopback defaults to true; set false when you intentionally want localhost traffic rate-limited too (for test setups or strict proxy deployments).
  • tailscale.mode: serve (tailnet only, loopback bind) or funnel (public, requires auth).
  • remote.transport: ssh (default) or direct (ws/wss). For direct, remote.url must be ws:// or wss://.
  • gateway.remote.token is for remote CLI calls only; does not enable local gateway auth.
  • trustedProxies: reverse proxy IPs that terminate TLS. Only list proxies you control.
  • allowRealIpFallback: when true, the gateway accepts X-Real-IP if X-Forwarded-For is missing. Default false for fail-closed behavior.
  • gateway.tools.deny: extra tool names blocked for HTTP POST /tools/invoke (extends default deny list).
  • gateway.tools.allow: remove tool names from the default HTTP deny list.

OpenAI-compatible endpoints

  • Chat Completions: disabled by default. Enable with gateway.http.endpoints.chatCompletions.enabled: true.
  • Responses API: gateway.http.endpoints.responses.enabled.
  • Responses URL-input hardening:
    • gateway.http.endpoints.responses.maxUrlParts
    • gateway.http.endpoints.responses.files.urlAllowlist
    • gateway.http.endpoints.responses.images.urlAllowlist

Multi-instance isolation

Run multiple gateways on one host with unique ports and state dirs:
DATZI_CONFIG_PATH=~/.datzi/a.json \
DATZI_STATE_DIR=~/.datzi-a \
datzi gateway --port 19001
Convenience flags: --dev (uses ~/.datzi-dev + port 19001), --profile <name> (uses ~/.datzi-<name>). See Multiple Gateways.

Hooks

{
  hooks: {
    enabled: true,
    token: 'shared-secret',
    path: '/hooks',
    maxBodyBytes: 262144,
    defaultSessionKey: 'hook:ingress',
    allowRequestSessionKey: false,
    allowedSessionKeyPrefixes: ['hook:'],
    allowedAgentIds: ['hooks', 'main'],
    presets: ['gmail'],
    transformsDir: '~/.datzi/hooks/transforms',
    mappings: [
      {
        match: {
          path: 'gmail'
        },
        action: 'agent',
        agentId: 'hooks',
        wakeMode: 'now',
        name: 'Gmail',
        sessionKey: 'hook:gmail:{{messages[0].id}}',
        messageTemplate: 'From: {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}',
        deliver: true,
        channel: 'last',
        model: 'ollama/qwen3-coder:14b'
      }
    ]
  }
}
Auth: Authorization: Bearer <token> or x-datzi-token: <token>. Endpoints:
  • POST /hooks/wake โ†’ { text, mode?: "now"|"next-heartbeat" }
  • POST /hooks/agent โ†’ { message, name?, agentId?, sessionKey?, wakeMode?, deliver?, channel?, to?, model?, thinking?, timeoutSeconds? }
    • sessionKey from request payload is accepted only when hooks.allowRequestSessionKey=true (default: false).
  • POST /hooks/<name> โ†’ resolved via hooks.mappings
  • match.path matches sub-path after /hooks (e.g. /hooks/gmail โ†’ gmail).
  • match.source matches a payload field for generic paths.
  • Templates like {{messages[0].subject}} read from the payload.
  • transform can point to a JS/TS module returning a hook action.
    • transform.module must be a relative path and stays within hooks.transformsDir (absolute paths and traversal are rejected).
  • agentId routes to a specific agent; unknown IDs fall back to default.
  • allowedAgentIds: restricts explicit routing (* or omitted = allow all, [] = deny all).
  • defaultSessionKey: optional fixed session key for hook agent runs without explicit sessionKey.
  • allowRequestSessionKey: allow /hooks/agent callers to set sessionKey (default: false).
  • allowedSessionKeyPrefixes: optional prefix allowlist for explicit sessionKey values (request + mapping), e.g. ["hook:"].
  • deliver: true sends final reply to a channel; channel defaults to last.
  • model overrides LLM for this hook run (must be allowed if model catalog is set).

Gmail integration

{
  hooks: {
    gmail: {
      account: 'datzi@gmail.com',
      topic: 'projects/<project-id>/topics/gog-gmail-watch',
      subscription: 'gog-gmail-watch-push',
      pushToken: 'shared-push-token',
      hookUrl: 'http://127.0.0.1:18789/hooks/gmail',
      includeBody: true,
      maxBytes: 20000,
      renewEveryMinutes: 720,
      serve: {
        bind: '127.0.0.1',
        port: 8788,
        path: '/'
      },
      tailscale: {
        mode: 'funnel',
        path: '/gmail-pubsub'
      },
      model: 'openrouter/meta-llama/llama-3.3-70b-instruct:free',
      thinking: 'off'
    }
  }
}
  • Gateway auto-starts gog gmail watch serve on boot when configured. Set DATZI_SKIP_GMAIL_WATCHER=1 to disable.
  • Donโ€™t run a separate gog gmail watch serve alongside the Gateway.

Canvas host

{
  canvasHost: {
    root: '~/.datzi/workspace/canvas',
    liveReload: true
    // enabled: false, // or DATZI_SKIP_CANVAS_HOST=1
  }
}
  • Serves agent-editable HTML/CSS/JS and A2UI over HTTP under the Gateway port:
    • http://<gateway-host>:<gateway.port>/__datzi__/canvas/
    • http://<gateway-host>:<gateway.port>/__datzi__/a2ui/
  • Local-only: keep gateway.bind: "loopback" (default).
  • Non-loopback binds: canvas routes require Gateway auth (token/password/trusted-proxy), same as other Gateway HTTP surfaces.
  • Node WebViews typically donโ€™t send auth headers; after a node is paired and connected, the Gateway advertises node-scoped capability URLs for canvas/A2UI access.
  • Capability URLs are bound to the active node WS session and expire quickly. IP-based fallback is not used.
  • Injects live-reload client into served HTML.
  • Auto-creates starter index.html when empty.
  • Also serves A2UI at /__datzi__/a2ui/.
  • Changes require a gateway restart.
  • Disable live reload for large directories or EMFILE errors.

Discovery

mDNS (Bonjour)

{
  discovery: {
    mdns: {
      mode: 'minimal'
      // minimal | full | off
    }
  }
}
  • minimal (default): omit cliPath + sshPort from TXT records.
  • full: include cliPath + sshPort.
  • Hostname defaults to datzi. Override with DATZI_MDNS_HOSTNAME.

Wide-area (DNS-SD)

{
  discovery: {
    wideArea: {
      enabled: true
    }
  }
}
Writes a unicast DNS-SD zone under ~/.datzi/dns/. For cross-network discovery, pair with a DNS server (CoreDNS recommended) + Tailscale split DNS. Setup: datzi dns setup --apply.

Environment

env (inline env vars)

{
  env: {
    vars: {
      GROQ_API_KEY: 'gsk-...'
    },
    shellEnv: {
      enabled: true,
      timeoutMs: 15000
    }
  }
}
  • Inline env vars are only applied if the process env is missing the key.
  • .env files: CWD .env + ~/.datzi/.env (neither overrides existing vars).
  • shellEnv: imports missing expected keys from your login shell profile.
  • See Environment for full precedence.

Env var substitution

Reference env vars in any config string with ${VAR_NAME}:
{
  gateway: {
    auth: {
      token: '${DATZI_GATEWAY_TOKEN}'
    }
  }
}
  • Only uppercase names matched: [A-Z_][A-Z0-9_]*.
  • Missing/empty vars throw an error at config load.
  • Escape with $${VAR} for a literal ${VAR}.
  • Works with $include.

Auth storage

{
  auth: {
    profiles: {
      'anthropic:me@example.com': {
        provider: 'anthropic',
        mode: 'oauth',
        email: 'me@example.com'
      },
      'anthropic:work': {
        provider: 'anthropic',
        mode: 'api_key'
      }
    },
    order: {
      anthropic: ['anthropic:me@example.com', 'anthropic:work']
    }
  }
}
  • Per-agent auth profiles stored at <agentDir>/auth-profiles.json.
  • Legacy OAuth imports from ~/.datzi/credentials/oauth.json.
  • See OAuth.

Logging

{
  logging: {
    level: 'info',
    file: '/tmp/datzi/datzi.log',
    consoleLevel: 'info',
    consoleStyle: 'pretty',
    // pretty | compact | json
    redactSensitive: 'tools',
    // off | tools
    redactPatterns: ['\\bTOKEN\\b\\s*[=:]\\s*(["\']?)([^\\s"\']+)\\1']
  }
}
  • Default log file: /tmp/datzi/datzi-YYYY-MM-DD.log.
  • Set logging.file for a stable path.
  • consoleLevel bumps to debug when --verbose.

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'
  }
}

Identity

{
  agents: {
    list: [
      {
        id: 'main',
        identity: {
          name: 'Samantha',
          theme: 'helpful sloth',
          emoji: '๐Ÿฆฅ',
          avatar: 'avatars/samantha.png'
        }
      }
    ]
  }
}
Written by the macOS onboarding assistant. Derives defaults:
  • messages.ackReaction from identity.emoji (falls back to ๐Ÿ‘€)
  • mentionPatterns from identity.name/identity.emoji
  • avatar accepts: workspace-relative path, http(s) URL, or data: URI

Bridge (legacy, removed)

Current builds no longer include the TCP bridge. Nodes connect over the Gateway WebSocket. bridge.* keys are no longer part of the config schema (validation fails until removed; datzi doctor --fix can strip unknown keys).
{
  "bridge": {
    "enabled": true,
    "port": 18790,
    "bind": "tailnet",
    "tls": {
      "enabled": true,
      "autoGenerate": true
    }
  }
}

Cron

{
  cron: {
    enabled: true,
    maxConcurrentRuns: 2,
    webhook: 'https://example.invalid/legacy',
    // deprecated fallback for stored notify:true jobs
    webhookToken: 'replace-with-dedicated-token',
    // optional bearer token for outbound webhook auth
    sessionRetention: '24h'
    // duration string or false
  }
}
  • sessionRetention: how long to keep completed cron sessions before pruning. Default: 24h.
  • webhookToken: bearer token used for cron webhook POST delivery (delivery.mode = "webhook"), if omitted no auth header is sent.
  • webhook: deprecated legacy fallback webhook URL (http/https) used only for stored jobs that still have notify: true.
See Cron Jobs.

Media model template variables

Template placeholders expanded in tools.media.*.models[].args:
VariableDescription
{{Body}}Full inbound message body
{{RawBody}}Raw body (no history/sender wrappers)
{{BodyStripped}}Body with group mentions stripped
{{From}}Sender identifier
{{To}}Destination identifier
{{MessageSid}}Channel message id
{{SessionId}}Current session UUID
{{IsNewSession}}"true" when new session created
{{MediaUrl}}Inbound media pseudo-URL
{{MediaPath}}Local media path
{{MediaType}}Media type (image/audio/document/โ€ฆ)
{{Transcript}}Audio transcript
{{Prompt}}Resolved media prompt for CLI entries
{{MaxChars}}Resolved max output chars for CLI entries
{{ChatType}}"direct" or "group"
{{GroupSubject}}Group subject (best effort)
{{GroupMembers}}Group members preview (best effort)
{{SenderName}}Sender display name (best effort)
{{SenderE164}}Sender phone number (best effort)
{{Provider}}Provider hint (whatsapp, telegram, discord, etc.)

Config includes ($include)

Split config into multiple files:
// ~/.datzi/datzi.json
{
  gateway: {
    port: 18789
  },
  agents: {
    $include: './agents.json5'
  },
  broadcast: {
    $include: ['./clients/mueller.json5', './clients/schmidt.json5']
  }
}
Merge behavior:
  • Single file: replaces the containing object.
  • Array of files: deep-merged in order (later overrides earlier).
  • Sibling keys: merged after includes (override included values).
  • Nested includes: up to 10 levels deep.
  • Paths: resolved relative to the including file, but must stay inside the top-level config directory (dirname of the main config file). Absolute/../ forms are allowed only when they still resolve inside that boundary.
  • Errors: clear messages for missing files, parse errors, and circular includes.

Related: Configuration ยท Configuration Examples ยท Doctor

Discovery and Transports