Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration

OpenCrabs uses two config files stored at ~/.opencrabs/:

FilePurpose
config.tomlProvider settings, features, channel connections
keys.tomlAPI 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)
FieldDefaultDescription
working_directoryhome dirDefault 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_concurrent4Max tool calls running in parallel
context_limit200000Context window limit in tokens. When exceeded, oldest messages are dropped
max_tokens65536Max output tokens per single API call
auto_updatetrueAutomatically install new releases on startup (binary mode only)
silent_compactionfalseWhen true, suppresses the agent’s playful post-compaction narration. Useful for corporate/formal deployments
lazy_toolstrueShips 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_datatrueRedacts 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_providerNone (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_modelNone (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
FieldDefaultDescription
subagent_providerNone (uses session provider)Provider for spawned sub-agents. Keeps sub-agents off your main provider
subagent_modelNone (uses session model)Model for spawned sub-agents
self_improvement_providerNone (uses session provider)Provider for RSI self-improvement cycles. RSI runs on its own provider chain
self_improvement_modelNone (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)
FieldDefaultDescription
enabledfalseEnable the Telegram bot channel
tokenNoneTelegram 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_hoursNone (no timeout)Idle timeout in hours for non-owner sessions. Owner sessions never expire
rich_messagesfalseSend 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_starttrueSilently 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
FieldDefaultDescription
enabledfalseEnable the Discord bot channel
tokenNoneDiscord 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_hoursNoneIdle 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
FieldDefaultDescription
enabledfalseEnable the Slack bot channel
tokenNoneBot token (xoxb-...)
app_tokenNoneApp-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_hoursNoneIdle timeout for non-owner sessions

WhatsApp

[channels.whatsapp]
enabled = true
allowed_phones = ["+15551234567"]      # E.164 format
session_idle_hours = 24.0
FieldDefaultDescription
enabledfalseEnable the WhatsApp channel
allowed_phones[] (accept all)E.164 phone numbers. Empty = accept everyone (not recommended for business numbers)
session_idle_hoursNoneIdle 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
FieldDefaultDescription
enabledfalseEnable the Trello channel
tokenNoneTrello API token
app_tokenNoneTrello 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_secsNone (tool-only)Polling interval in seconds. Absent or 0 = no polling (tool-only mode)
session_idle_hoursNoneIdle 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"
FieldDefaultDescription
default_providerNone (uses active provider)Default provider for cron jobs without an explicit provider
default_modelNone (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
FieldDefaultDescription
vector_enabledtrue (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.urlNoneOpenAI-compatible API base URL. The /embeddings path is appended automatically
embedding.modelNoneEmbedding model name (e.g. text-embedding-3-small, nomic-embed-text)
embedding.api_keyNoneAPI key for the embedding endpoint. Also loaded from keys.toml under [providers.memory_embedding]
embedding.dimensionsNone (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
FieldDefaultDescription
strip_empty_sectionstrueStrip empty header stubs (## Header with no body) from brain-file reads. Writes are never affected, only the loaded view is filtered
default_cap500Per-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"
FieldDefaultDescription
cdp_endpointNone (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
FieldDefaultDescription
enabledfalseEnable the A2A JSON-RPC 2.0 gateway
bind"127.0.0.1"Bind address
port18790Gateway port
allowed_origins[]CORS origins. Must be set explicitly, no cross-origin requests allowed by default
api_keyNoneBearer token for authenticating incoming A2A requests. If unset, no authentication required

Daemon Mode

[daemon]
health_port = 8080
FieldDefaultDescription
health_portNone (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)
SectionFieldDefaultDescription
image.generationenabledfalseEnable image generation via the generate_image tool
image.generationmodel"gemini-3.1-flash-image-preview"Model for image generation
image.visionenabledfalseEnable 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.visionmodel"gemini-3.1-flash-image-preview"Model for image/vision analysis
image.visionproviderNone (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"]
ChainValid labels
STTvoicebox, openai_compatible, groq, local (aliases: whisper, local_whisper)
TTSvoicebox, 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.”