CamelAGI Configuration System
This document covers the complete configuration system: file format, schema, environment variable overrides, runtime mechanics, hot-reload, and provider-specific examples.
Config File Location and Format
- Path:
~/.camelagi/config.yaml - Format: YAML, parsed with the
yamlnpm package (parse/stringify) - Source file:
src/core/config.ts
The config directory (~/.camelagi/) and file are created automatically on first run or when saveConfig() is called. If the file does not exist at load time, all values fall back to schema defaults.
Complete Zod Schema Reference
Top-Level Fields
| Field | Type | Default | Description |
|---|---|---|---|
provider |
"anthropic" | "openai" |
"anthropic" |
LLM provider identifier. |
model |
string |
"claude-sonnet-4-20250514" |
Model name passed to the provider. |
apiKey |
string |
(none) | API key for the configured provider. Optional in schema; typically set via env var. |
baseUrl |
string |
(none) | Custom base URL for the provider API. Used for OpenRouter, Ollama, or any OpenAI-compatible endpoint. |
systemPrompt |
string |
"You are CamelAGI, a helpful AI assistant. You have access to tools for running shell commands, reading/writing files, and fetching URLs. Use them when needed to help the user." |
Base system prompt. Bootstrap files are appended at runtime. |
thinking |
"off" | "low" | "medium" | "high" |
"off" |
Extended thinking / chain-of-thought budget. |
effort |
"low" | "medium" | "high" | "max" |
"high" |
Inference effort level. |
maxBudgetUsd |
number |
(none) | Optional spending cap in USD. |
maxTurns |
number |
25 |
Maximum agent loop iterations per run. |
timeoutSeconds |
number |
300 |
Per-run timeout (5 minutes). |
boot |
boolean |
true |
Whether to run BOOT.md on server start. |
serve – Gateway Server
| Field | Type | Default | Description |
|---|---|---|---|
serve.port |
number |
18789 |
HTTP/WS listen port. |
serve.host |
string |
"127.0.0.1" |
Listen address. |
serve.token |
string |
(none) | Bearer token for API authentication. |
serve.rateLimit.windowMs |
number |
60000 |
Rate limit sliding window in milliseconds. |
serve.rateLimit.max |
number |
60 |
Max requests per window. |
telegram – Telegram Channel
| Field | Type | Default | Description |
|---|---|---|---|
telegram.botToken |
string |
(none) | Telegram bot token for the legacy top-level bot. |
telegram.allowedUsers |
number[] |
[] |
Telegram user IDs allowed to interact. Empty = no restriction. |
telegram.groups.mentionOnly |
boolean |
true |
In groups, only respond when mentioned. |
telegram.chats |
Record<string, ChatOverride> |
{} |
Per-chat overrides (see below). |
ChatOverride fields (all optional):
name, model, systemPrompt, maxTurns, thinking ("off"|"low"|"medium"|"high"), effort ("low"|"medium"|"high"|"max").
compaction – Context Compaction
| Field | Type | Default | Description |
|---|---|---|---|
compaction.enabled |
boolean |
true |
Enable automatic context compaction. |
compaction.maxTokens |
number |
80000 |
Token threshold (compaction triggers at 80% of this). |
compaction.keepTurns |
number |
6 |
Number of recent turns preserved after compaction. |
tools – Tool Policy
| Field | Type | Default | Description |
|---|---|---|---|
tools.allow |
string[] |
[] |
Allowlist of tool names. Empty = allow all. |
tools.deny |
string[] |
[] |
Denylist of tool names. Checked after allowlist. |
skills – Skill Loader
| Field | Type | Default | Description |
|---|---|---|---|
skills.enabled |
boolean |
true |
Enable loading skills from ~/.camelagi/skills/. |
skills.deny |
string[] |
[] |
Skill names to exclude. |
hooks – Lifecycle Hooks
| Field | Type | Default | Description |
|---|---|---|---|
hooks.enabled |
boolean |
false |
Enable lifecycle hooks from ~/.camelagi/hooks/. |
approvals – Tool Approval System
| Field | Type | Default | Description |
|---|---|---|---|
approvals.mode |
"off" | "smart" | "always" |
"off" |
Approval mode. smart = only for destructive ops. |
approvals.allowlist |
string[] |
[] |
Tools that never require approval. |
approvals.timeoutSeconds |
number |
120 |
How long to wait for an approval response. |
approvals.fallback |
"deny" | "allow" |
"deny" |
Action on approval timeout. |
approvals.forwardTo |
number |
(none) | Telegram chat ID to forward approval requests to when running headless. |
voice – Voice Transcription
| Field | Type | Default | Description |
|---|---|---|---|
voice.enabled |
boolean |
false |
Enable voice message transcription. |
voice.provider |
"groq" | "openai" | "deepgram" |
"groq" |
Transcription provider. |
voice.apiKey |
string |
(none) | API key for the transcription provider. |
voice.model |
string |
(none) | Model name (provider-specific). |
voice.language |
string |
(none) | Language hint (e.g., "en"). |
retry – Error Retry
| Field | Type | Default | Description |
|---|---|---|---|
retry.maxRetries |
number |
3 |
Maximum retry attempts on transient errors. |
retry.backoffMs |
number |
1000 |
Base backoff delay in milliseconds (multiplied per attempt). |
lanes – Concurrency Lanes
| Field | Type | Default | Description |
|---|---|---|---|
lanes.main |
number |
3 |
Max concurrent runs in the main lane. |
lanes.cron |
number |
1 |
Max concurrent cron runs. |
lanes.subagent |
number |
5 |
Max concurrent sub-agent runs. |
agents – Multi-Agent Definitions
Type: Record<string, AgentDef> – Default: {}
Each agent entry has:
| Field | Type | Default | Description |
|---|---|---|---|
name |
string |
(required) | Display name for the agent. |
admin |
boolean |
false |
Whether this agent has admin privileges. |
model |
string |
(none) | Override model for this agent. |
systemPrompt |
string |
(none) | Override system prompt. |
thinking |
"off" | "low" | "medium" | "high" |
(none) | Override thinking level. |
effort |
"low" | "medium" | "high" | "max" |
(none) | Override effort level. |
maxTurns |
number |
(none) | Override max turns. |
telegram |
object |
(none) | Telegram config for this agent (see below). |
discord |
object |
(none) | Discord config for this agent (see below). |
Agent telegram sub-object:
botToken(string, required) – Separate bot token for this agent.allowedUsers(number[], default[]) – Allowed user IDs.groups.mentionOnly(boolean, defaulttrue).
Agent discord sub-object:
botToken(string, required) – Discord bot token.allowedChannels(string[], default[]) – Channel IDs to respond in. Empty = all channels.allowedRoles(string[], default[]) – Role names allowed. Empty = all users.mentionOnly(boolean, defaulttrue) – Only respond when @mentioned in servers.
cron – Scheduled Jobs
Type: CronJob[] – Default: []
Each cron entry:
| Field | Type | Default | Description |
|---|---|---|---|
id |
string |
(required) | Unique job identifier. |
name |
string |
"" |
Human-readable name. |
schedule |
string |
(required) | Schedule expression: duration ("5m", "1h", "1d"), */N cron ("*/5 * * * *"), one-shot relative ("+20m"), or ISO timestamp. See Extensions docs for details and limitations. |
prompt |
string |
(required) | Prompt sent to the agent on each trigger. |
session |
string |
(none) | Session ID to use (optional, for context continuity). |
enabled |
boolean |
true |
Whether the job is active. |
Environment Variable Overrides
Seven environment variables are recognized. They override values from the YAML file:
| Env Var | Overrides | Notes |
|---|---|---|
CAMELAGI_PROVIDER |
provider |
|
CAMELAGI_MODEL |
model |
|
ANTHROPIC_API_KEY |
apiKey |
Takes priority over OPENAI_API_KEY. |
OPENAI_API_KEY |
apiKey |
Only used if ANTHROPIC_API_KEY is not set. |
CAMELAGI_BASE_URL |
baseUrl |
|
CAMELAGI_TOKEN |
serve.token |
|
TELEGRAM_BOT_TOKEN |
telegram.botToken |
The .env file is loaded automatically via dotenv/config.
Config Precedence
Values are resolved in this order (later wins):
1. Zod schema defaults
2. ~/.camelagi/config.yaml (file config)
3. Environment variables (CAMELAGI_*, ANTHROPIC_API_KEY, etc.)
4. Runtime overrides (passed to loadConfig(overrides))
In code, loadConfig() builds a merged object:
const merged = {
...fileConfig, // from YAML
...envOverrides, // from env vars
...overrides, // from function argument
};
return schema.parse(merged); // Zod fills in defaults for missing fields
Note: env vars for nested sections (CAMELAGI_TOKEN and TELEGRAM_BOT_TOKEN) are applied separately by merging into the existing serve / telegram sub-objects to avoid clobbering other nested fields.
loadConfig() Mechanics
export function loadConfig(overrides: Partial<Config> = {}): Config
- If
~/.camelagi/config.yamlexists, read and parse it as YAML. Otherwise start with{}. - Spread env var overrides on top of the file config.
- Spread runtime
overridesargument on top. - For
CAMELAGI_TOKEN: merge into the existingservesub-object (preservingport,host,rateLimit). - For
TELEGRAM_BOT_TOKEN: merge into the existingtelegramsub-object (preservingallowedUsers,groups,chats). - Pass the merged object through
schema.parse()– Zod validates and fills in all defaults. - Return the fully typed
Configobject.
saveConfig() Mechanics
export function saveConfig(values: Record<string, unknown>): void
- Ensure
~/.camelagi/exists (mkdirSyncwithrecursive: true). - If
config.yamlexists, read and parse the current contents. - Deep-merge the new
valuesinto the existing config (see deep merge behavior below). - Write the merged result back to
config.yamlas YAML. - If an
onConfigSavedcallback is registered, callloadConfig()to produce a freshConfig(which re-applies env var overrides), then invoke the callback synchronously.
The onConfigSaved callback is used by serve.ts to immediately update the in-memory gateway state without waiting for the file-watcher debounce.
Deep Merge Behavior
The deepMerge(target, source) function follows these rules:
- Plain objects: recursively merged (keys from source overwrite or add to target).
- Arrays: replaced entirely, not concatenated. If
source.tools.denyis["exec"], it replaces whatever was intarget.tools.deny. - Scalars: source value overwrites target value.
- Null / undefined in source: overwrites the target value (no special handling).
This means if you call saveConfig({ tools: { deny: ["exec"] } }), only tools.deny is changed; tools.allow is preserved because the deep merge recurses into the tools object. But arrays like deny are replaced wholesale.
Hot-Reload (watchConfig)
Defined in src/serve.ts. The gateway watches for config changes at runtime.
function watchConfig(
_initialConfig: Config,
onChange: (config: Config) => void,
): fs.FSWatcher | null
Key details:
- Watches the directory (
~/.camelagi/), not the file directly. This ensures detection even ifconfig.yamlis deleted and recreated (e.g., after a reset + onboarding flow). - Filters by filename: only reacts when the changed file is
config.yaml. - Debounced: uses a 500ms
setTimeoutdebounce to coalesce rapid writes. - On change: calls
loadConfig()(re-applies env vars and defaults), then invokes theonChangecallback. - What
onChangedoes in serve.ts:- Updates
state.configandstate.systemPrompt. - Reconfigures concurrency lanes (
Lane.Main,Lane.Cron,Lane.Subagent). - Updates cron context.
- Reconciles all channels (starts new agents, stops removed ones).
- Updates
- Error handling: if
loadConfig()throws (e.g., invalid YAML), the error is logged and the old config remains in effect.
ensureDirs() – Created Directories
Called at startup to guarantee the directory tree exists:
| Directory | Purpose |
|---|---|
~/.camelagi/ |
Root config directory. |
~/.camelagi/sessions/ |
Session history storage. |
~/.camelagi/workspace/ |
Bootstrap files (AGENTS.md, SOUL.md, etc.). |
~/.camelagi/hooks/ |
Lifecycle hook scripts. |
~/.camelagi/skills/ |
User-defined skills. |
~/.camelagi/usage/ |
Token usage tracking data. |
~/.camelagi/cron/ |
Runtime cron job definitions. |
~/.camelagi/logs/ |
Log files. |
All directories are created with recursive: true, so parent directories are created as needed.
Provider-Specific Config Examples
Anthropic (default)
provider: anthropic
model: claude-sonnet-4-20250514
apiKey: sk-ant-...
Or via environment variable:
export ANTHROPIC_API_KEY=sk-ant-...
No baseUrl needed – the OpenAI SDK compatibility layer routes to api.anthropic.com/v1/.
OpenAI
provider: openai
model: gpt-4o
apiKey: sk-...
Or:
export CAMELAGI_PROVIDER=openai
export OPENAI_API_KEY=sk-...
export CAMELAGI_MODEL=gpt-4o
OpenRouter
provider: openai
model: anthropic/claude-sonnet-4-20250514
apiKey: sk-or-...
baseUrl: https://openrouter.ai/api/v1
Ollama (local)
provider: openai
model: llama3
baseUrl: http://localhost:11434/v1
No apiKey needed for local Ollama.
Custom OpenAI-Compatible Endpoint
provider: openai
model: my-custom-model
apiKey: my-key
baseUrl: https://my-llm-proxy.example.com/v1
Full Annotated Config Example
# ── Provider ──────────────────────────────────────────────
provider: anthropic # "anthropic" | "openai"
model: claude-sonnet-4-20250514 # Model name
apiKey: sk-ant-... # API key (prefer env var instead)
# baseUrl: https://custom.api/v1 # Custom endpoint (optional)
# ── Prompt & Behavior ────────────────────────────────────
systemPrompt: "You are CamelAGI, a helpful AI assistant."
thinking: "off" # "off" | "low" | "medium" | "high"
effort: "high" # "low" | "medium" | "high" | "max"
# maxBudgetUsd: 5.0 # Spending cap (optional)
maxTurns: 25 # Max agent loop turns per run
timeoutSeconds: 300 # Per-run timeout
boot: true # Run BOOT.md on server start
# ── Gateway Server ───────────────────────────────────────
serve:
port: 18789
host: "127.0.0.1"
# token: "my-secret-token" # Bearer auth (optional)
rateLimit:
windowMs: 60000 # 1 minute window
max: 60 # 60 requests per window
# ── Telegram ─────────────────────────────────────────────
telegram:
botToken: "123456:ABC..." # Legacy top-level bot
allowedUsers: [12345678] # Allowed Telegram user IDs
groups:
mentionOnly: true # Only respond when @mentioned
chats: # Per-chat overrides
"-100123456789":
name: "Dev Chat"
model: claude-sonnet-4-20250514
maxTurns: 10
# ── Compaction ───────────────────────────────────────────
compaction:
enabled: true
maxTokens: 80000 # Triggers at 80% (~64K tokens)
keepTurns: 6 # Keep last 6 turns after compaction
# ── Tools ────────────────────────────────────────────────
tools:
allow: [] # Empty = allow all
deny: [] # e.g. ["exec"] to block shell
# ── Skills ───────────────────────────────────────────────
skills:
enabled: true
deny: [] # e.g. ["risky-skill"]
# ── Hooks ────────────────────────────────────────────────
hooks:
enabled: false
# ── Approvals ────────────────────────────────────────────
approvals:
mode: "off" # "off" | "smart" | "always"
allowlist: [] # Tools that skip approval
timeoutSeconds: 120
fallback: "deny" # "deny" | "allow"
# forwardTo: 12345678 # Telegram chat for headless approvals
# ── Retry ────────────────────────────────────────────────
retry:
maxRetries: 3
backoffMs: 1000 # Base delay, multiplied per attempt
# ── Concurrency Lanes ───────────────────────────────────
lanes:
main: 3 # Max concurrent main runs
cron: 1 # Max concurrent cron runs
subagent: 5 # Max concurrent sub-agent runs
# ── Agents (multi-bot) ──────────────────────────────────
agents:
researcher:
name: "Research Bot"
admin: false
model: claude-sonnet-4-20250514
systemPrompt: "You are a research assistant."
thinking: "medium"
effort: "high"
maxTurns: 15
telegram:
botToken: "654321:XYZ..."
allowedUsers: [12345678]
groups:
mentionOnly: true
# ── Cron Jobs ────────────────────────────────────────────
cron:
- id: daily-summary
name: "Daily Summary"
schedule: "1d" # every 24 hours (see extensions docs for cron limitations)
prompt: "Summarize yesterday's activity."
# session: "summary-session" # Optional session for continuity
enabled: true