Configuration
OpenCrabs uses two config files stored at ~/.opencrabs/:
| File | Purpose |
|---|---|
config.toml | Provider settings, features, channel connections |
keys.toml | API keys and secrets (never committed to git) |
Workspace Layout
~/.opencrabs/
├── config.toml # Main configuration
├── keys.toml # API keys (gitignored)
├── commands.toml # Custom slash commands
├── opencrabs.db # SQLite database
├── SOUL.md # Agent personality
├── USER.md # Your profile
├── MEMORY.md # Long-term memory
├── AGENTS.md # Agent behavior docs
├── TOOLS.md # Tool reference
├── SECURITY.md # Security policies
├── HEARTBEAT.md # Periodic check tasks
├── memory/ # Daily memory notes
│ └── YYYY-MM-DD.md
├── images/ # Generated images
├── logs/ # Application logs
└── skills/ # Custom skills/plugins
Provider Configuration
See Provider Setup for detailed provider configuration.
Quick example — add Anthropic:
# config.toml
[providers.anthropic]
enabled = true
default_model = "claude-sonnet-4-20250514"
# keys.toml
[providers.anthropic]
api_key = "sk-ant-..."
Provider Priority
When multiple providers are enabled, the first one found in this order is used for new sessions:
MiniMax > OpenRouter > Anthropic > OpenAI > Gemini > Custom
Each session remembers which provider and model it was using. Switch providers per-session via /models.
Agent Behavior
[agent]
working_directory = "/path/to/default/dir"
thinking = "on" # "on", "off", or "budget_XXk"
approval_policy = "auto-always" # "ask", "auto-session", "auto-always"
max_concurrent = 4 # max parallel tool calls
context_limit = 200000 # context window cap (tokens)
max_tokens = 65536 # max output tokens per API call
auto_update = true # auto-install releases on startup
silent_compaction = false # suppress post-compaction personality narration
lazy_tools = true # JIT tool-schema loading (ships core + tool_search only)
redact_sensitive_data = true # redact API keys, tokens, passwords, IPs from output
default_provider = "minimax" # fallback provider when no provider is active (v0.3.62)
default_model = "MiniMax-M2.7" # fallback model when no model is active (v0.3.62)
| Field | Default | Description |
|---|---|---|
working_directory | home dir | Default working directory for the agent |
thinking | "on" | Extended thinking mode: "on", "off", or "budget_XXk" |
approval_policy | "auto-always" | "ask" = confirm every tool call, "auto-session" = auto-approve for session, "auto-always" = never ask |
max_concurrent | 4 | Max tool calls running in parallel |
context_limit | 200000 | Context window limit in tokens. When exceeded, oldest messages are dropped |
max_tokens | 65536 | Max output tokens per single API call |
auto_update | true | Automatically install new releases on startup (binary mode only) |
silent_compaction | false | When true, suppresses the agent’s playful post-compaction narration. Useful for corporate/formal deployments |
lazy_tools | true | Ships only core tool schemas (~4k tokens) plus tool_search per request. The agent discovers and activates extended tools on demand via tool_search. Set false to load all ~95 schemas every request |
redact_sensitive_data | true | Redacts API keys, tokens, passwords, and IPs from tool outputs and display. Set false during sysadmin/devops work where seeing IPs/tokens/passwords is necessary |
default_provider | None (uses active provider) | Fallback provider when no provider is active in the current session. Also used for cron jobs without an explicit provider (v0.3.62) |
default_model | None (uses active model) | Fallback model when no model is active in the current session. Also used for cron jobs without an explicit model (v0.3.62) |
Sub-agent and RSI Overrides
Route spawned sub-agents and RSI (self-improvement) cycles to separate providers so they never compete with your main chat for quota:
[agent]
subagent_provider = "minimax" # provider for spawned sub-agents
subagent_model = "MiniMax-M2.7" # model for spawned sub-agents
self_improvement_provider = "minimax" # provider for RSI self-improvement cycles
self_improvement_model = "MiniMax-M2.7" # model for RSI cycles
| Field | Default | Description |
|---|---|---|
subagent_provider | None (uses session provider) | Provider for spawned sub-agents. Keeps sub-agents off your main provider |
subagent_model | None (uses session model) | Model for spawned sub-agents |
self_improvement_provider | None (uses session provider) | Provider for RSI self-improvement cycles. RSI runs on its own provider chain |
self_improvement_model | None (uses session model) | Model for RSI cycles. Prefer cheap, fast models since results are deterministic |
Channel Configuration
Telegram
[channels.telegram]
enabled = true
token = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
allowed_users = ["123456789"] # numeric Telegram user IDs
allowed_channels = ["-100123456"] # restrict to specific group/channel IDs (empty = all)
respond_to = "mention" # "all", "dm_only", "mention" (default)
session_idle_hours = 24.0 # idle timeout for non-owner sessions
rich_messages = true # native Telegram rich messages (Bot API 10.1, default since v0.3.64)
silence_group_start = true # silently ignore /start from non-allowed users in groups
bot_owner = ["123456789"] # owner IDs (gated commands, /cd hidden dirs, /profiles)
| Field | Default | Description |
|---|---|---|
enabled | false | Enable the Telegram bot channel |
token | None | Telegram Bot API token from @BotFather |
allowed_users | [] (accept all) | Numeric Telegram user IDs. Accepts int or string arrays. Empty = open mode |
allowed_channels | [] (all channels) | Restrict bot to specific channel/group IDs. DMs always pass |
respond_to | "mention" | When to respond in groups: "all" = every message, "dm_only" = ignore groups, "mention" = only when @mentioned or replied-to |
session_idle_hours | None (no timeout) | Idle timeout in hours for non-owner sessions. Owner sessions never expire |
rich_messages | false | Send structured replies as native Telegram rich messages (tables, headings, lists, math). Only works on current mobile/desktop Telegram clients. Telegram Web and older clients show a “not supported” placeholder. Enable only when your audience is on modern clients |
silence_group_start | true | Silently ignore /start from non-allowed users in group chats. Users who need their ID can DM the bot |
bot_owner | [] (first allowed_user) | Bot owner user IDs. Owners can access gated commands (/profiles, hidden files in /cd), manage profiles. When unset, defaults to first entry in allowed_users |
Discord
[channels.discord]
enabled = true
token = "your-discord-bot-token"
allowed_users = ["123456789012345678"]
allowed_channels = ["123456789012345678"]
respond_to = "mention"
session_idle_hours = 24.0
| Field | Default | Description |
|---|---|---|
enabled | false | Enable the Discord bot channel |
token | None | Discord bot token from the Developer Portal |
allowed_users | [] (accept all) | Discord user IDs. Accepts int or string arrays |
allowed_channels | [] (all channels) | Restrict bot to specific channel IDs |
respond_to | "mention" | When to respond: "all", "dm_only", "mention" |
session_idle_hours | None | Idle timeout for non-owner sessions |
Slack
[channels.slack]
enabled = true
token = "xoxb-your-bot-token"
app_token = "xapp-your-app-token" # Socket Mode token
allowed_users = ["U12345678"]
allowed_channels = ["C12345678"]
respond_to = "mention"
session_idle_hours = 24.0
| Field | Default | Description |
|---|---|---|
enabled | false | Enable the Slack bot channel |
token | None | Bot token (xoxb-...) |
app_token | None | App-level token for Socket Mode (xapp-...) |
allowed_users | [] (accept all) | Slack user IDs (U12345678) |
allowed_channels | [] (all channels) | Restrict bot to specific channel IDs |
respond_to | "mention" | When to respond: "all", "dm_only", "mention" |
session_idle_hours | None | Idle timeout for non-owner sessions |
[channels.whatsapp]
enabled = true
allowed_phones = ["+15551234567"] # E.164 format
session_idle_hours = 24.0
| Field | Default | Description |
|---|---|---|
enabled | false | Enable the WhatsApp channel |
allowed_phones | [] (accept all) | E.164 phone numbers. Empty = accept everyone (not recommended for business numbers) |
session_idle_hours | None | Idle timeout for non-owner sessions |
Trello
[channels.trello]
enabled = true
token = "your-trello-api-token"
app_token = "your-trello-api-key"
allowed_users = ["memberId1"]
board_ids = ["boardId1", "boardId2"]
poll_interval_secs = 60
session_idle_hours = 24.0
| Field | Default | Description |
|---|---|---|
enabled | false | Enable the Trello channel |
token | None | Trello API token |
app_token | None | Trello API key (stored as app_token for keys.toml symmetry) |
allowed_users | [] (accept all) | Trello member IDs |
board_ids | [] (all boards) | Board IDs to monitor for @mentions. Also accepts allowed_channels as an alias |
poll_interval_secs | None (tool-only) | Polling interval in seconds. Absent or 0 = no polling (tool-only mode) |
session_idle_hours | None | Idle timeout for non-owner sessions |
Other Channels (Preview)
Signal, Google Chat, and iMessage are available as preview placeholders:
[channels.signal]
enabled = true
allowed_phones = ["+15551234567"]
[channels.google_chat]
enabled = true
token = "your-google-chat-token"
allowed_users = ["user@example.com"]
[channels.imessage]
enabled = true
allowed_phones = ["+15551234567"]
Cron Defaults
Route cron jobs to cheaper providers so they never compete with your interactive session:
[cron]
default_provider = "minimax"
default_model = "MiniMax-M2.7"
| Field | Default | Description |
|---|---|---|
default_provider | None (uses active provider) | Default provider for cron jobs without an explicit provider |
default_model | None (uses active model) | Default model for cron jobs without an explicit model |
Memory and Embeddings
[memory]
vector_enabled = true
[memory.embedding]
url = "https://api.openai.com/v1"
model = "text-embedding-3-small"
# api_key loaded from keys.toml: [providers.memory_embedding] api_key = "sk-..."
# dimensions = 1536 # auto-detected from first API response if unset
| Field | Default | Description |
|---|---|---|
vector_enabled | true (desktop), false (VPS) | Enable vector embeddings for semantic memory search. When disabled, only FTS5 keyword search is used. Auto-disabled on systems with < 2GB RAM or detected cloud instances |
embedding.url | None | OpenAI-compatible API base URL. The /embeddings path is appended automatically |
embedding.model | None | Embedding model name (e.g. text-embedding-3-small, nomic-embed-text) |
embedding.api_key | None | API key for the embedding endpoint. Also loaded from keys.toml under [providers.memory_embedding] |
embedding.dimensions | None (auto-detected) | Embedding vector dimensions. Auto-detected from the first API response if unset. Local GGUF model always produces 768-dim vectors |
When [memory.embedding] is not set, embeddings are generated locally via the embeddinggemma-300M GGUF model (~300MB download, ~2.9GB RAM). Setting [memory.embedding] with an API endpoint eliminates the local model overhead.
Brain Files
[brain]
strip_empty_sections = true
default_cap = 500
[brain.caps]
SOUL.md = 300
AGENTS.md = 800
| Field | Default | Description |
|---|---|---|
strip_empty_sections | true | Strip empty header stubs (## Header with no body) from brain-file reads. Writes are never affected, only the loaded view is filtered |
default_cap | 500 | Per-file line cap for sync_templates. When a merged file exceeds its cap, the sync bails instead of writing |
caps | {} (empty) | Per-file line caps overrides. Keys are filenames (case-sensitive: TOOLS.md and tools.md are distinct) |
Browser
[browser]
cdp_endpoint = "http://localhost:9222"
| Field | Default | Description |
|---|---|---|
cdp_endpoint | None (spawn new browser) | CDP endpoint for an existing Chromium instance. When set, connects via Chrome DevTools Protocol instead of spawning a new browser. Useful for sharing a single browser across multiple profiles to save memory (~250-300MB per instance) |
To start a standalone Chromium with CDP enabled:
chromium --remote-debugging-port=9222 --headless --no-sandbox
A2A (Agent-to-Agent) Gateway
[a2a]
enabled = false
bind = "127.0.0.1"
port = 18790
allowed_origins = ["https://your-app.com"]
# api_key = "your-secret-key" # Bearer token for incoming requests
| Field | Default | Description |
|---|---|---|
enabled | false | Enable the A2A JSON-RPC 2.0 gateway |
bind | "127.0.0.1" | Bind address |
port | 18790 | Gateway port |
allowed_origins | [] | CORS origins. Must be set explicitly, no cross-origin requests allowed by default |
api_key | None | Bearer token for authenticating incoming A2A requests. If unset, no authentication required |
Daemon Mode
[daemon]
health_port = 8080
| Field | Default | Description |
|---|---|---|
health_port | None (no health server) | HTTP port for GET /health endpoint. Useful for systemd watchdog, uptime monitors, and external health probes |
OpenCrabs runs in two modes: TUI (interactive terminal UI with chat) and Daemon (headless background service for channels + cron). For any one profile, run only one at a time. The TUI always wins: opening it while a daemon runs shuts the daemon down and takes over the channels.
For full service lifecycle management (TUI vs Daemon comparison, opencrabs service install/start/stop, profile-aware services, OPENCRABS_PROFILE env var, troubleshooting), see the CLI Commands reference.
Image Generation and Vision
[image.generation]
enabled = true
model = "gemini-3.1-flash-image-preview"
[image.vision]
enabled = true
model = "gemini-3.1-flash-image-preview"
provider = "openrouter" # bypasses enabled gate for vision-only providers (v0.3.63)
| Section | Field | Default | Description |
|---|---|---|---|
image.generation | enabled | false | Enable image generation via the generate_image tool |
image.generation | model | "gemini-3.1-flash-image-preview" | Model for image generation |
image.vision | enabled | false | Enable vision analysis via the analyze_image tool. Since v0.3.64: setting vision_model alone is sufficient to enable vision. enabled is no longer required when vision_model is set |
image.vision | model | "gemini-3.1-flash-image-preview" | Model for image/vision analysis |
image.vision | provider | None (auto-detect) | Dedicated provider for vision. Bypasses the enabled gate so you can use a vision-only provider without enabling it for general chat (v0.3.63) |
Vision analysis automatically scans all enabled providers (Google, OpenRouter, OpenAI-compatible, Anthropic) before returning an error. No configuration needed.
Voice Provider Fallback
STT and TTS providers support automatic failover via fallback_chain. When the primary returns a 5xx, fails a liveness probe (Voicebox), or is otherwise unreachable, the dispatcher walks the chain in order and tries each entry that has the credentials/config it needs.
[providers.stt]
fallback_chain = ["groq", "openai_compatible", "local"]
[providers.tts]
fallback_chain = ["openai_compatible", "openai", "local"]
| Chain | Valid labels |
|---|---|
| STT | voicebox, openai_compatible, groq, local (aliases: whisper, local_whisper) |
| TTS | voicebox, openai_compatible, openai, local (aliases: piper, local_piper). groq is STT-only, the TTS chain rejects it |
Empty or omitted chain means “use the default priority order with the primary removed.”