Configuration
All Zubo configuration lives in a single file: ~/.zubo/config.json. You can edit this file directly with any text editor, or use the zubo config CLI commands for quick changes without opening a file. Changes to the configuration file are picked up on the next restart, while CLI changes via zubo config set take effect immediately if Zubo is running.
Quick Config via CLI
The zubo config command provides a convenient way to read and write configuration values from the terminal:
# Set values
zubo config set activeProvider ollama
zubo config set model llama3.3
zubo config set heartbeatMinutes 60
zubo config set auth.enabled true
zubo config set rateLimit.chatPerMinute 30
zubo config set channels.webchat.port 8080
zubo config set providers.anthropic.model claude-sonnet-4-5-20250929
# Get values
zubo config get # Show entire configuration (pretty-printed)
zubo config get activeProvider # Show a single value
zubo config get channels.telegram # Show a nested object
zubo config get rateLimit # Show all rate limit settings
Notes on the CLI:
- Supports dotted keys for nested values.
zubo config set channels.webchat.port 8080writes to{"channels": {"webchat": {"port": 8080}}}. - Types are auto-detected:
trueandfalsebecome booleans, strings of digits become numbers, everything else stays a string. - To explicitly set a string that looks like a number, wrap it in quotes:
zubo config set someField '"12345"'. - The
setcommand writes the file atomically, so there is no risk of corruption from concurrent writes.
LLM Providers
Zubo supports a multi-provider architecture. You configure one or more LLM providers under the providers object, set one as activeProvider, and optionally define a failover chain. If the active provider fails (network error, rate limit, authentication error), Zubo automatically tries each failover provider in order until one succeeds.
Supported Providers
| Provider Key | Default Base URL | Notes |
|---|---|---|
anthropic |
(uses Anthropic SDK directly) | Claude models. Supports up to 200k token context. Native streaming. Recommended primary provider. |
openai |
https://api.openai.com/v1 |
GPT-4o, GPT-4-turbo, and other OpenAI models. Streaming supported. |
ollama |
http://localhost:11434/v1 |
Run models locally. No API key required. Great as a failover for offline operation. |
groq |
https://api.groq.com/openai/v1 |
Extremely fast inference. Supports Llama, Mixtral, and other open models. |
together |
https://api.together.xyz/v1 |
Wide selection of open-source models. Competitive pricing. |
openrouter |
https://openrouter.ai/api/v1 |
Multi-model gateway. Access Claude, GPT-4, Llama, and more through a single API key. |
lmstudio |
http://localhost:1234/v1 |
Local model GUI. Download and run models with a graphical interface. No API key required. |
google |
https://generativelanguage.googleapis.com/v1beta |
Google Gemini models. Supports Gemini Pro, Gemini Flash, and other Google AI models. |
deepseek |
https://api.deepseek.com/v1 |
DeepSeek models. OpenAI-compatible API with strong reasoning capabilities at competitive pricing. |
xai |
https://api.x.ai/v1 |
xAI Grok models. OpenAI-compatible API with real-time knowledge. |
fireworks |
https://api.fireworks.ai/inference/v1 |
Fireworks AI. Fast inference for open-source models with optimized serving. |
Provider Configuration Fields
Each provider object under providers accepts the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
apiKey |
string | Yes* | API key for the provider. Not required for local providers (Ollama, LM Studio). |
baseUrl |
string | No | Override the default base URL. Useful for proxies or self-hosted endpoints. |
model |
string | No | Model name to use. Defaults vary by provider (e.g., claude-sonnet-4-5-20250929 for Anthropic). |
streaming |
boolean | No | Enable or disable streaming responses. Defaults to true. |
contextWindow |
number | No | Maximum context window size in tokens. Zubo uses this for context compaction decisions. Defaults to the provider's known maximum. |
Multi-Provider Example with Failover
{
"providers": {
"anthropic": {
"apiKey": "sk-ant-api03-...",
"model": "claude-sonnet-4-5-20250929",
"streaming": true,
"contextWindow": 200000
},
"openai": {
"apiKey": "sk-proj-...",
"model": "gpt-4o",
"streaming": true
},
"ollama": {
"baseUrl": "http://localhost:11434/v1",
"model": "llama3.3",
"streaming": true
}
},
"activeProvider": "anthropic",
"failover": ["openai", "ollama"]
}
In this configuration, Zubo uses Claude as the primary model. If Anthropic returns an error (rate limit, network issue, etc.), it automatically tries OpenAI next, and finally Ollama as a local fallback. The failover is transparent to the user — they receive a response regardless of which provider handled it.
Smart Routing
Smart routing automatically selects the best provider for each query based on complexity. Simple questions (greetings, quick lookups, short follow-ups) are routed to fast, inexpensive models, while complex queries (multi-step reasoning, code generation, long-form writing) go to your most capable provider. This can significantly reduce costs without noticeable quality loss.
| Field | Type | Default | Description |
|---|---|---|---|
smartRouting.enabled |
boolean | false |
Enable automatic query routing between providers. |
smartRouting.fastProvider |
string | — | Provider key to use for simple queries (e.g., "groq", "deepseek"). |
smartRouting.complexProvider |
string | — | Provider key to use for complex queries. Defaults to activeProvider if not set. |
"smartRouting": {
"enabled": true,
"fastProvider": "groq",
"complexProvider": "anthropic"
}
When smart routing is enabled, Zubo classifies each incoming message and routes it accordingly. You can always override the routing for a specific message by prefixing it with the provider name (e.g., @anthropic explain quantum computing).
Channel Configuration
Channels define how users interact with Zubo. Each channel is independently configured under the channels object. All enabled channels share the same memory, personality, tools, and conversation history — a fact learned via Telegram is immediately available in Discord or the web dashboard.
Web Chat
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable the built-in web chat dashboard. |
port |
number | 3000 |
HTTP port for the web dashboard and API. |
"webchat": {
"enabled": true,
"port": 3000
}
Telegram
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable the Telegram channel. |
botToken |
string | — | Bot token from @BotFather. |
allowedUsers |
number[] | [] |
Array of numeric Telegram user IDs allowed to interact. Empty array means no restriction. |
"telegram": {
"enabled": true,
"botToken": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"allowedUsers": [12345678, 87654321]
}
Discord
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable the Discord channel. |
botToken |
string | — | Bot token from the Discord Developer Portal. |
allowedUsers |
string[] | [] |
Array of Discord user ID strings allowed to interact. Empty array means no restriction. |
"discord": {
"enabled": true,
"botToken": "MTk4NjIyNDgzNDcxOTI1...",
"allowedUsers": ["198622483471925248"]
}
Slack
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable the Slack channel. |
botToken |
string | — | Bot User OAuth Token (starts with xoxb-). |
appToken |
string | — | App-Level Token (starts with xapp-). Required for Socket Mode. |
allowedUsers |
string[] | [] |
Array of Slack member ID strings allowed to interact. Empty array means no restriction. |
"slack": {
"enabled": true,
"botToken": "xoxb-1234567890-abcdefghij",
"appToken": "xapp-1-A0123456789-1234567890123-abcdef",
"allowedUsers": ["U01ABCDEF"]
}
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable the WhatsApp channel. |
authDir |
string | ~/.zubo/whatsapp-auth |
Directory to store WhatsApp Web authentication data. On first run, a QR code is displayed for pairing. |
allowedNumbers |
string[] | [] |
Array of phone numbers (with country code, e.g., "14155551234") allowed to interact. Empty array means no restriction. |
"whatsapp": {
"enabled": true,
"authDir": "/Users/you/.zubo/whatsapp-auth",
"allowedNumbers": ["14155551234", "447700900000"]
}
Signal
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable the Signal channel. |
phoneNumber |
string | — | The phone number registered with Signal for the bot (with country code, e.g., "+14155551234"). |
signalCliPath |
string | signal-cli |
Path to the signal-cli binary. Defaults to looking on the system PATH. |
allowedNumbers |
string[] | [] |
Array of phone numbers allowed to interact. Empty array means no restriction. |
"signal": {
"enabled": true,
"phoneNumber": "+14155551234",
"signalCliPath": "/usr/local/bin/signal-cli",
"allowedNumbers": ["+447700900000"]
}
Voice Configuration
Zubo supports speech-to-text (STT) for transcribing voice messages received on any channel, and text-to-speech (TTS) for generating spoken audio responses. Voice is configured under the voice object.
Speech-to-Text (STT)
| Field | Type | Default | Description |
|---|---|---|---|
provider |
string | "whisper" |
STT provider. Currently supported: whisper (OpenAI Whisper API). |
apiKey |
string | — | API key for the STT provider. For Whisper, this is your OpenAI API key. |
model |
string | "whisper-1" |
Model to use for transcription. |
Text-to-Speech (TTS)
| Field | Type | Default | Description |
|---|---|---|---|
provider |
string | "openai" |
TTS provider. Supported: openai, elevenlabs. |
apiKey |
string | — | API key for the TTS provider. |
voice |
string | "nova" |
Voice to use for speech generation. OpenAI voices: alloy, echo, fable, onyx, nova, shimmer. ElevenLabs voices depend on your account. |
"voice": {
"stt": {
"provider": "whisper",
"apiKey": "sk-proj-...",
"model": "whisper-1"
},
"tts": {
"provider": "openai",
"apiKey": "sk-proj-...",
"voice": "nova"
}
}
Agent Settings
These top-level settings control the core agent behavior.
| Field | Type | Default | Description |
|---|---|---|---|
maxTurns |
number | 50 |
Maximum number of tool-call rounds per conversation turn. When the agent reaches this limit, it stops calling tools and produces a text response with whatever information it has gathered. This prevents runaway tool loops. Reasonable values range from 10 (tightly constrained) to 100 (complex multi-step tasks). |
heartbeatMinutes |
number | 30 |
Interval in minutes (1–1440) between heartbeat cycles. During each heartbeat, Zubo wakes up, checks for pending cron jobs, processes scheduled tasks, evaluates proactive memory triggers, and optionally sends notifications. Set to a low value (1–5) for near-real-time scheduling, or a high value (60–1440) to conserve resources. |
{
"maxTurns": 50,
"heartbeatMinutes": 30
}
Rate Limiting
Zubo includes built-in per-IP rate limiting to protect against abuse. Rate limits use a sliding window algorithm — each IP address gets an independent counter that resets on a rolling 60-second basis.
| Field | Type | Default | Description |
|---|---|---|---|
rateLimit.chatPerMinute |
number | 60 |
Maximum chat messages per minute per IP address. Applies to the /api/chat endpoint and WebSocket messages. When exceeded, the client receives a 429 Too Many Requests response. |
rateLimit.uploadPerMinute |
number | 10 |
Maximum file uploads per minute per IP address. Applies to the /api/upload endpoint. Uploads are more resource-intensive (document parsing, chunking, embedding), so the default is lower. |
"rateLimit": {
"chatPerMinute": 60,
"uploadPerMinute": 10
}
For personal use on localhost, the defaults are generous. If you are exposing Zubo on a public network or sharing it with others, consider lowering these values (e.g., chatPerMinute: 30, uploadPerMinute: 5).
Budget Controls
Zubo tracks token usage and estimated costs for every LLM request. You can set spending limits to prevent surprise bills and view per-model cost breakdowns in the dashboard.
| Field | Type | Default | Description |
|---|---|---|---|
budget.dailyLimit |
number | 0 |
Maximum daily spend in USD. Set to 0 for no limit. When the limit is reached, Zubo pauses LLM requests and notifies you. |
budget.monthlyLimit |
number | 0 |
Maximum monthly spend in USD. Set to 0 for no limit. |
budget.warningThreshold |
number | 0.8 |
Percentage (0–1) of the budget at which Zubo sends a warning notification. Default is 80%. |
"budget": {
"dailyLimit": 5,
"monthlyLimit": 50,
"warningThreshold": 0.8
}
Cost tracking is always active even without spending limits. View your usage breakdown in the dashboard under Analytics → Costs, or ask Zubo directly: “How much have I spent today?”
Authentication
When auth.enabled is true, all HTTP API endpoints and WebSocket connections require a valid API key. This is strongly recommended if you are exposing Zubo's port beyond localhost.
| Field | Type | Default | Description |
|---|---|---|---|
auth.enabled |
boolean | false |
Enable API key authentication for all HTTP and WebSocket endpoints. |
Managing API Keys
API keys can be created and managed via the CLI or the web dashboard:
# Create a new API key
zubo auth create --name "my-app"
# Output: API key created: zb_k1_a1b2c3d4e5f6...
# List all API keys
zubo auth list
# Revoke an API key
zubo auth revoke zb_k1_a1b2c3d4e5f6...
Using API Keys
Include the API key in the Authorization header as a Bearer token:
curl -X POST http://localhost:3000/api/chat \
-H "Authorization: Bearer zb_k1_a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{"message": "Hello, Zubo!"}'
For WebSocket connections, pass the key as a query parameter:
ws://localhost:3000/ws?token=zb_k1_a1b2c3d4e5f6...
API keys are stored as hashed values in the SQLite database. The plain-text key is only shown once at creation time and cannot be retrieved later. If you lose a key, revoke it and create a new one.
Sandbox
The sandbox controls how user-installed skills (from ~/.zubo/workspace/skills/) are executed. Built-in tools always run in the main process, but user skills run in isolated subprocesses for safety.
| Field | Type | Default | Description |
|---|---|---|---|
sandbox.enabled |
boolean | true |
Enable subprocess sandboxing for user-installed skills. When enabled, each skill invocation runs in a separate Bun subprocess with restricted access. Disabling this is not recommended but may be necessary for skills that require direct main-process access. |
sandbox.timeoutMs |
number | 30000 |
Maximum execution time in milliseconds for a single skill invocation. If a skill exceeds this timeout, its subprocess is terminated and an error is returned to the agent. Set higher for skills that perform long-running operations (e.g., large file processing, slow API calls). |
"sandbox": {
"enabled": true,
"timeoutMs": 30000
}
Full Example
Here is a complete ~/.zubo/config.json showing all configuration sections together. This represents a fully configured instance with Anthropic as the primary provider, Groq for smart routing, Ollama as a local failover, three channels enabled, voice support, budget controls, and production-ready security settings:
{
"providers": {
"anthropic": {
"apiKey": "sk-ant-api03-...",
"model": "claude-sonnet-4-5-20250929",
"streaming": true,
"contextWindow": 200000
},
"groq": {
"apiKey": "gsk_...",
"model": "llama-3.3-70b-versatile",
"streaming": true
},
"ollama": {
"baseUrl": "http://localhost:11434/v1",
"model": "llama3.3",
"streaming": true
}
},
"activeProvider": "anthropic",
"failover": ["groq", "ollama"],
"smartRouting": {
"enabled": true,
"fastProvider": "groq",
"complexProvider": "anthropic"
},
"channels": {
"webchat": {
"enabled": true,
"port": 3000
},
"telegram": {
"enabled": true,
"botToken": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"allowedUsers": [12345678]
},
"discord": {
"enabled": true,
"botToken": "MTk4NjIyNDgzNDcxOTI1...",
"allowedUsers": []
},
"slack": {
"enabled": false,
"botToken": "",
"appToken": "",
"allowedUsers": []
},
"whatsapp": {
"enabled": false,
"authDir": "",
"allowedNumbers": []
},
"signal": {
"enabled": false,
"phoneNumber": "",
"signalCliPath": "signal-cli",
"allowedNumbers": []
}
},
"voice": {
"stt": {
"provider": "whisper",
"apiKey": "sk-proj-...",
"model": "whisper-1"
},
"tts": {
"provider": "openai",
"apiKey": "sk-proj-...",
"voice": "nova"
}
},
"maxTurns": 50,
"heartbeatMinutes": 30,
"rateLimit": {
"chatPerMinute": 60,
"uploadPerMinute": 10
},
"budget": {
"dailyLimit": 5,
"monthlyLimit": 50,
"warningThreshold": 0.8
},
"auth": {
"enabled": false
},
"sandbox": {
"enabled": true,
"timeoutMs": 30000
}
}
Tip: You do not need to include every field. Zubo applies sensible defaults for any omitted values. A minimal config only needs activeProvider and the corresponding provider entry with an API key.
Environment
By default, Zubo stores all data and configuration under ~/.zubo. You can override this by setting the ZUBO_HOME environment variable to an absolute path:
# Use a custom home directory
export ZUBO_HOME=/opt/zubo-data
zubo start
# Or inline for a single invocation
ZUBO_HOME=/opt/zubo-data zubo start --daemon
When ZUBO_HOME is set, all paths — config file, database, logs, sessions, models, and workspace — are resolved relative to that directory instead of ~/.zubo. This is useful for running multiple isolated Zubo instances on the same machine, or for placing the data directory on a specific volume or mount point.
Other environment variables recognized by Zubo:
| Variable | Description |
|---|---|
ZUBO_HOME |
Override the default ~/.zubo data directory. |
ZUBO_LOG_LEVEL |
Set the log level: debug, info, warn, error. Defaults to info. |
ZUBO_PORT |
Override the web dashboard port (takes precedence over config file). |
NODE_ENV |
When set to production, Zubo disables verbose logging and enables additional security hardening. |