https://github.com/adolfousier/opencrabs
The self-improving all channels AI agent. Self-healing. Fully autonomous. Single Rust binary.
https://github.com/adolfousier/opencrabs
agent-orchestration agentic-ai agentic-framework autonomous-agents harness harness-framework open-source opencrabs orchestration-framework recursive-self-improvement rsi self-healing self-learning
Last synced: 5 days ago
JSON representation
The self-improving all channels AI agent. Self-healing. Fully autonomous. Single Rust binary.
- Host: GitHub
- URL: https://github.com/adolfousier/opencrabs
- Owner: adolfousier
- License: mit
- Created: 2026-02-14T00:05:58.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-06-14T03:32:49.000Z (5 days ago)
- Last Synced: 2026-06-14T04:14:40.816Z (5 days ago)
- Topics: agent-orchestration, agentic-ai, agentic-framework, autonomous-agents, harness, harness-framework, open-source, opencrabs, orchestration-framework, recursive-self-improvement, rsi, self-healing, self-learning
- Language: Rust
- Homepage: https://opencrabs.com
- Size: 32.2 MB
- Stars: 792
- Watchers: 5
- Forks: 76
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE.md
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
- awesome-claws - OpenCrabs - Rust - The self-improving autonomous AI agent. Every channel. Multi-provider LLM support, tool use, TUI, fallback chains, and cron jobs. (Main Projects)
- awesome-agent-frameworks - adolfousier/opencrabs - square) (By Category / Rust Native)
- awesome-ratatui - opencrabs - Open-claw inspired orchestration layer for software development. (π» Apps / β¨οΈ Development Tools)
README
[](https://www.rust-lang.org)
[](https://www.rust-lang.org/)
[](https://ratatui.rs)
[](https://docker.com)
[](https://github.com/adolfousier/opencrabs/actions/workflows/ci.yml)
[](https://github.com/adolfousier/opencrabs)
# π¦ OpenCrabs
**The autonomous, self-improving AI agent. Single Rust binary. Every channel.**
> Autonomous, self-improving multi-channel AI agent built in Rust. Inspired by [Open Claw](https://github.com/openclaw/openclaw).
```
___ ___ _
/ _ \ _ __ ___ _ _ / __|_ _ __ _| |__ ___
| (_) | '_ \/ -_) ' \ | (__| '_/ _` | '_ \(_-<
\___/| .__/\___|_||_| \___|_| \__,_|_.__//__/
|_|
π¦ The autonomous, self-improving AI agent. Single Rust binary. Every channel.
```
**Author:** [Adolfo Usier](https://github.com/adolfousier)
β Star us on [GitHub](https://github.com/adolfousier/opencrabs) if you like what you see!
---
## Why OpenCrabs?
OpenCrabs runs as a **single binary on your terminal** β no server, no gateway, no infrastructure. It makes direct HTTPS calls to LLM providers from your machine. Nothing else leaves your computer.
### OpenCrabs vs Node.js Agent Frameworks
| | **OpenCrabs** (Rust) | **Node.js Frameworks** (e.g. Open Claw) |
|---|---|---|
| **Binary size** | **26β29 MB** single binary, zero dependencies | **1 GB+** `node_modules` with hundreds of transitive packages |
| **Runtime** | None β runs natively | Requires Node.js runtime + npm install |
| **Attack surface** | Zero network listeners. Outbound HTTPS only | Server infrastructure: open ports, auth layers, middleware |
| **API key security** | Keys on your machine only. `zeroize` clears them from RAM on drop, `[REDACTED]` in all debug output | Keys in env vars or config. GC doesn't guarantee memory clearing. Heap dumps can leak secrets |
| **Data residency** | 100% local β SQLite DB, embeddings, brain files, all in `~/.opencrabs/` | Server-side storage, potential multi-tenant data, network transit |
| **Supply chain** | Single compiled binary. Rust's type system prevents buffer overflows, use-after-free, data races at compile time | npm ecosystem: typosquatting, dependency confusion, prototype pollution |
| **Memory safety** | Compile-time guarantees β no GC, no null pointers, no data races | GC-managed, prototype pollution, type coercion bugs |
| **Concurrency** | tokio async + Rust ownership = zero data races guaranteed | Single-threaded event loop, worker threads share memory unsafely |
| **Native TTS/STT** | Built-in local speech-to-text (whisper.cpp) and text-to-speech β ~130 MB total stack, fully offline | No native voice. Requires external APIs (Google, AWS, Azure) or heavy Python dependencies (PyTorch, ~5 GB+) |
| **Telemetry** | Zero. No analytics, no tracking, no remote logging | Server infra typically includes monitoring, logging pipelines, APM |
### What stays local (never leaves your machine)
- All chat sessions and messages (SQLite)
- Tool executions (bash, file reads/writes, git)
- Memory and embeddings (local vector search)
- Voice transcription in local STT mode (whisper.cpp, on-device)
- Brain files, config, API keys
### What goes out (only when you use it)
- Your messages to the LLM provider API (Anthropic, OpenAI, GitHub Copilot, etc.)
- Web search queries (optional tool)
- GitHub API via `gh` CLI (optional tool)
- Browser automation (optional, `browser` feature β auto-detects Chromium-based browsers via CDP, not Firefox)
- Dynamic tool HTTP requests (only when you define HTTP tools in `tools.toml`)
---
## Table of Contents
- [Screenshots](#-screenshots)
- [Why OpenCrabs?](#why-opencrabs)
- [Core Features](#-core-features)
- [CLI Commands](#cli)
- [Migrating from Other Tools](#-migrating-from-other-tools)
- [Supported AI Providers](#-supported-ai-providers)
- [Agent-to-Agent (A2A) Protocol](#-agent-to-agent-a2a-protocol)
- [Quick Start](#-quick-start)
- [Onboarding Wizard](#-onboarding-wizard)
- [API Keys (keys.toml)](#-api-keys-keystoml)
- [Configuration (config.toml)](#-configuration-configtoml)
- [Commands (commands.toml)](#-commands-commandstoml)
- [Dynamic Tools (tools.toml)](#-dynamic-tools-toolstoml)
- [Using Local LLMs](#-using-local-llms)
- [Configuration](#-configuration)
- [Tool System](#-tool-system)
- [Keyboard Shortcuts](#-keyboard-shortcuts)
- [Debug and Logging](#-debug-and-logging)
- [Cron Jobs & Heartbeats](#-cron-jobs--heartbeats)
- [Architecture](#-architecture)
- [Project Structure](#-project-structure)
- [Development](#-development)
- [Platform Notes](#-platform-notes)
- [Troubleshooting](#-troubleshooting)
- [Companion Tools](#-companion-tools)
- [Disclaimers](#-disclaimers)
- [Contributing](#-contributing)
- [License](#-license)
- [Acknowledgments](#-acknowledgments)
---
## π¬ Full onboard
https://github.com/user-attachments/assets/833dd5e9-3bcc-432a-96ac-3a5bb97b5966
## π¬ Demo
https://github.com/user-attachments/assets/7f45c5f8-acdf-48d5-b6a4-0e4811a9ee23
---
## π― Core Features
### AI & Providers
| Feature | Description |
|---------|-------------|
| **Multi-Provider** | **Xiaomi (default β free, no key during the launch window)**, Anthropic Claude, OpenAI, GitHub Copilot (uses your Copilot subscription), OpenRouter (400+ models), MiniMax, Google Gemini, z.ai GLM (General API + Coding API), Claude CLI, OpenCode CLI, Codex CLI (uses your ChatGPT/Codex subscription), Qwen Native (free OAuth with multi-account rotation), Qwen Code CLI (1k free req/day), and any OpenAI-compatible API (Ollama, LM Studio, LocalAI). Model lists fetched live from provider APIs β new models available instantly. Custom provider dialog: paste-by-default for API keys, Enter-to-load live models, typed-not-in-list models accepted and merged. Each session remembers its provider + model and restores it on switch |
| **Fallback Providers** | Configure a chain of fallback providers β if the primary fails, each fallback is tried in sequence automatically. Any configured provider can be a fallback. Config: `[providers.fallback] providers = ["openrouter", "anthropic"]` |
| **Per-Provider Vision** | Set `vision_model` per provider β the LLM calls `analyze_image` as a tool, which uses the vision model on the same provider API to describe images. The chat model stays the same and gets vision capability via tool call. Gemini vision takes priority when configured. Auto-configured for known providers (e.g. MiniMax) on first run |
| **Real-time Streaming** | Character-by-character response streaming with animated spinner showing model name and live text |
| **Local LLM Support** | Run with LM Studio, Ollama, or any OpenAI-compatible endpoint β 100% private, zero-cost |
| **Usage Dashboard** | Per-message token count and cost displayed in header; `/usage` opens an interactive dashboard with daily activity charts, cost breakdowns by project/model/activity, core tool usage stats, and period filtering (Today/Week/Month/All-Time). Sessions are auto-categorized on startup (Development, Bug Fixes, Features, Refactoring, Testing, Documentation, CI/Deploy, etc.). Estimated costs for historical sessions shown as `~$X.XX` |
| **Context Awareness** | Live context usage indicator showing actual token counts (e.g. `ctx: 45K/200K (23%)`); auto-compaction at 70% with tool overhead budgeting; accurate tiktoken-based counting calibrated against API actuals |
| **3-Tier Memory** | (1) **Brain MEMORY.md** β user-curated durable memory loaded every turn, (2) **Daily Logs** β auto-compaction summaries at `~/.opencrabs/memory/YYYY-MM-DD.md`, (3) **Hybrid Memory Search** β FTS5 keyword search + vector embeddings combined via Reciprocal Rank Fusion. Three modes: **Local** (embeddinggemma-300M, 768-dim, no API key, works offline), **API** (any OpenAI-compatible `/v1/embeddings` endpoint: OpenAI, Ollama, Jina, etc.), or **FTS5-only** (no embeddings, VPS-friendly, ~0 RAM overhead). Auto-detects VPS environments and disables local embeddings |
| **Dynamic Brain System** | System brain assembled from workspace MD files (SOUL, USER, AGENTS, TOOLS, MEMORY) β all editable live between turns |
| **Multi-Agent Orchestration** | Spawn typed child agents (General, Explore, Plan, Code, Research) for parallel task execution. Five tools: `spawn_agent`, `wait_agent`, `send_input`, `close_agent`, `resume_agent`. Each type gets a role-specific system prompt and filtered tool registry. Configurable subagent provider/model. Children run in isolated sessions with auto-approve β no recursive spawning |
| **Recursive Self-Improvement** | β οΈ Experimental. Automatic feedback ledger tracks every tool execution, user correction, and provider error. Three tools: `feedback_record` (log observations), `feedback_analyze` (query patterns), `self_improve` (autonomously apply brain file changes β no human approval). Changes logged to `~/.opencrabs/rsi/improvements.md` with daily archives. Startup digest injects performance summary into system prompt. **Upstream template sync** β automatically detects new releases, fetches updated brain file templates from the repo, diffs against local files, and appends only new sections (never overwrites user customizations). Backups created before every merge. Zero tokens spent when version unchanged. Zero setup β works out of the box via auto-migration |
### Multimodal Input
| Feature | Description |
|---------|-------------|
| **Image Attachments** | Paste image paths or URLs into the input β auto-detected and attached as vision content blocks for multimodal models. Also supports pasting raw image data from the clipboard (copied from a browser, screenshot tool, or any app) β on macOS via the clipboard as PNG, on Linux via wl-paste/xclip. The bytes are written to a temp file and routed through the existing image pipeline |
| **Video Attachments** | Send a video on any channel (mp4, m4v, mov, webm, mkv, avi, 3gp, flv) or paste a video path in the TUI β the agent calls the `analyze_video` tool, which routes through Google Gemini's multimodal video API (inline β€18 MB, resumable Files API for larger). Requires `image.vision.enabled = true` with a Gemini API key in `config.toml`. Phase 1 is Gemini-native; a frame-extraction fallback for non-Gemini providers (ffmpeg β analyze_image per frame) is on the roadmap |
| **PDF Support** | Attach PDF files by path β native Anthropic PDF support; for other providers, text is extracted locally via `pdf-extract` |
| **Document Parsing** | Built-in `parse_document` tool extracts text from PDF, DOCX, HTML, TXT, MD, JSON, XML |
| **Voice (STT)** | Voice notes transcribed via **Groq Whisper API** (`whisper-large-v3-turbo`), any **OpenAI-compatible STT endpoint** (set `stt_base_url` + `stt_model` β works with self-hosted Whisper, Deepgram-compatible proxies, etc.), **Voicebox STT** (self-hosted open-source voice stack β point `voicebox_stt_base_url` at your instance; 2s liveness probe runs before each request so a dead voicebox fails fast), or **Local** whisper.cpp via `whisper-rs` (runs on-device, Tiny 75 MB / Base 142 MB / Small 466 MB / Medium 1.5 GB, zero API cost). All dispatched through a single entry point so every channel gets the same provider priority chain β and an optional `[providers.stt].fallback_chain` lets the user codify "if my local voicebox is down, try Groq, then OpenAI" so transient outages auto-route to the next provider with zero user action. Choose mode in `/onboard:voice`. Included by default |
| **Voice (TTS)** | Agent replies to voice notes with audio via **OpenAI TTS API** (`gpt-4o-mini-tts`), any **OpenAI-compatible TTS endpoint** (set `tts_base_url` + `tts_model` + `tts_voice` β works with self-hosted Coqui/Bark, ElevenLabs-compatible proxies, etc.), **Voicebox TTS** (async `/generate` β poll `/generate/{id}/status` β fetch audio; set `voicebox_tts_base_url` + `voicebox_tts_profile_id`), or **Local** Piper TTS (runs on-device via Python venv, Ryan / Amy / Lessac / Kristin / Joe / Cori, zero API cost). All outputs normalised to OGG/Opus via `ensure_opus` before delivery β consistent format across every channel regardless of backend. `[providers.tts].fallback_chain` provides the same auto-failover behaviour as the STT side. Falls back to text if disabled |
| **Attachment Indicator** | Attached images show as `[IMG1:filename.png]` in the input title bar |
| **Image Generation** | Agent generates images via Google Gemini (`gemini-3.1-flash-image-preview` "Nano Banana") using the `generate_image` tool β enabled via `/onboard:image`. Returned as native images/attachments in all channels |
#### Vision setup β two paths, pick one
**Path A (preferred, simpler).** Set `vision_model = ""` on your active `[providers.]` block in `config.toml`. Works for every built-in and custom provider β the agent calls the vision model on the **same provider endpoint** via the `analyze_image` tool, so no second API key is needed. Pick a vision-capable model on that provider (DeepSeek chat models like `deepseek-v4-flash` reject `image_url` content, so point `vision_model` at a vision-capable variant of the same family β every provider has at least one).
```toml
[providers.opencode]
enabled = true
vision_model = "mimo-v2-omni" # any vision-capable model on this provider
```
**Path B (fallback).** Enable Gemini globally. Use this only when your active provider has no vision-capable model. Easiest way: run `/onboard:image` and the wizard walks you through. Manual setup:
```toml
# config.toml
[image.vision]
enabled = true
model = "gemini-3.1-flash-image-preview"
```
```toml
# keys.toml β the Gemini key MUST live here, NOT in config.toml
[image]
api_key = "YOUR_GEMINI_KEY"
```
> **Gotcha:** `[image.vision] api_key = "..."` in `config.toml` is silently ignored β the field carries `#[serde(skip)]` for security. Use `keys.toml` `[image]` section, or `[providers.image.gemini]` in config.toml + the key in keys.toml.
**Diagnostic:** when vision is unavailable for any reason, `is_vision_available` logs the exact cause at INFO level in `~/.opencrabs/logs/opencrabs.YYYY-MM-DD` β search for `target=vision`.
### Messaging Integrations
| Feature | Description |
|---------|-------------|
| **Telegram Bot** | Full-featured Telegram bot β owner DMs share TUI session, groups get isolated per-group sessions (keyed by chat ID). Photo/voice support (STT transcribes incoming voice notes; TTS replies as OGG/Opus voice notes via `send_voice` when input was audio). Allowed user IDs, allowed chat/group IDs, `respond_to` filter (`all`/`dm_only`/`mention`). Passive group message capture β all messages stored for context even when bot isn't mentioned |
| **WhatsApp** | Connect via QR code pairing at runtime or from onboarding wizard. Text + image + voice (STT transcribes incoming voice notes; TTS replies as voice notes when input was audio and `tts_enabled=true`). Shared session with TUI, phone allowlist (`allowed_phones`), session persists across restarts |
| **Discord** | Full Discord bot β text + image + voice. Owner DMs share TUI session, guild channels get isolated per-channel sessions. Allowed user IDs, allowed channel IDs, `respond_to` filter. Full proactive control via `discord_send` (17 actions): `send`, `reply`, `react`, `unreact`, `edit`, `delete`, `pin`, `unpin`, `create_thread`, `send_embed`, `get_messages`, `list_channels`, `add_role`, `remove_role`, `kick`, `ban`, `send_file`. Generated images sent as native Discord file attachments |
| **Slack** | Full Slack bot via Socket Mode β owner DMs share TUI session, channels get isolated per-channel sessions. Text + image + voice (STT transcribes incoming audio attachments; TTS replies upload an OGG/Opus audio file via `files.upload` β renders inline with waveform UI β when input was audio and `tts_enabled=true`). Allowed user IDs, allowed channel IDs, `respond_to` filter. Full proactive control via `slack_send` (17 actions): `send`, `reply`, `react`, `unreact`, `edit`, `delete`, `pin`, `unpin`, `get_messages`, `get_channel`, `list_channels`, `get_user`, `list_members`, `kick_user`, `set_topic`, `send_blocks`, `send_file`. Generated images sent as native Slack file uploads. Bot token + app token from `api.slack.com/apps` (Socket Mode required). **Required Bot Token Scopes:** `chat:write`, `channels:history`, `groups:history`, `im:history`, `mpim:history`, `users:read`, `files:read`, `files:write`, `reactions:write`, `app_mentions:read` |
| **Trello** | Tool-only by default β the AI acts on Trello only when explicitly asked via `trello_send`. Opt-in polling via `poll_interval_secs` in config; when enabled, only `@bot_username` mentions from allowed users trigger a response. Full card management via `trello_send` (22 actions): `add_comment`, `create_card`, `move_card`, `find_cards`, `list_boards`, `get_card`, `get_card_comments`, `update_card`, `archive_card`, `add_member_to_card`, `remove_member_from_card`, `add_label_to_card`, `remove_label_from_card`, `add_checklist`, `add_checklist_item`, `complete_checklist_item`, `list_lists`, `get_board_members`, `search`, `get_notifications`, `mark_notifications_read`, `add_attachment`. API Key + Token from `trello.com/power-ups/admin`, board IDs and member-ID allowlist configurable |
#### File & Media Input Support
When users send files, images, or documents across any channel, the agent receives the content automatically β no manual forwarding needed. Example: a user uploads a dashboard screenshot to a Trello card with the comment _"I'm seeing this error"_ β the agent fetches the attachment, passes it through the vision pipeline, and responds with full context.
| Channel | Images (in) | Text files (in) | Documents (in) | Audio (in) | Audio reply (out) | Image gen (out) |
|---------|-------------|-----------------|----------------|------------|-------------------|-----------------|
| **Telegram** | β
vision pipeline | β
extracted inline | β
/ PDF note | β
STT | β
TTS via `send_voice` (OGG/Opus) | β
native photo |
| **WhatsApp** | β
vision pipeline | β
extracted inline | β
/ PDF note | β
STT | β
TTS via upload + `audio_message` (OGG/Opus, `ptt=true`) | β
native image |
| **Discord** | β
vision pipeline | β
extracted inline | β
/ PDF note | β
STT | β
TTS as `response.ogg` attachment | β
file attachment |
| **Slack** | β
vision pipeline | β
extracted inline | β
/ PDF note | β
STT | β
TTS via `files.upload` (OGG/Opus, inline waveform) | β
file upload |
| **Trello** | β
card attachments β vision | β
extracted inline | β | β | β | β
card attachment + embed |
| **TUI** | β
paste path β vision | β
paste path β inline | β | β
STT | β (terminal has no native audio) | β
`[IMG: name]` display |
Images are passed to the active model's vision pipeline if it supports multimodal input, or routed to the `analyze_image` tool (Google Gemini vision) otherwise. Text files (`.txt`, `.md`, `.json`, `.csv`, source code, etc.) are extracted as UTF-8 and included inline up to 8 000 characters β in the TUI simply paste or type the file path.
Videos uploaded on any channel (mp4, m4v, mov, webm, mkv, avi, 3gp, flv) auto-route to `analyze_video` when `image.vision.enabled = true` with a Gemini API key. The TUI also detects pasted video paths and labels them `Video #N` in the attachment indicator. Provider-side limits to keep in mind: Gemini's inline-bytes mode caps at ~20 MB (we use β€18 MB), and the resumable Files API supports up to 2 GB / ~1 hour videos. Channel-side limits are tighter β Telegram's Bot API hard-caps `getFile` downloads at **20 MB** even though chats accept larger uploads, so videos over that size will get a friendly "compress to under 20 MB and resend" reply. Slack file downloads use the bot token (`files:read` scope) and inherit the workspace's per-file upload cap. Frame-extraction fallback for non-Gemini providers is not yet wired β without a Gemini key, video uploads return an "unsupported" notice.
### Terminal UI
| Feature | Description |
|---------|-------------|
| **Cursor Navigation** | Full cursor movement: Left/Right arrows, Ctrl+Left/Right word jump, Home/End, Delete, Backspace at position |
| **Input History** | Persistent command history (`~/.opencrabs/history.txt`), loaded on startup, capped at 500 entries |
| **Inline Tool Approval** | Claude Code-style `β― Yes / Always / No` selector with arrow key navigation |
| **Inline Plan Approval** | Interactive plan review selector (Approve / Reject / Request Changes / View Plan) |
| **Session Management** | Create, rename, delete sessions with persistent SQLite storage; each session remembers its provider + model β switching sessions auto-restores the provider (no manual `/models` needed); token counts and context % per session. New sessions auto-generate a meaningful title from the first user message (no more "New Chat") |
| **Split Panes** | Horizontal (`\|` in sessions) and vertical (`_` in sessions) pane splitting β tmux-style. Each pane runs its own session with independent provider, model, and context. Run 10 sessions side by side, all processing in parallel. `Tab` to cycle focus, `Ctrl+X` to close pane |
| **Parallel Sessions** | Multiple sessions can have in-flight requests to different providers simultaneously. Send a message in one session, switch to another, send another β both process in parallel. Background sessions auto-approve tool calls; you'll see results when you switch back |
| **Scroll While Streaming** | Scroll up during streaming without being yanked back to bottom; auto-scroll re-enables when you scroll back down or send a message |
| **Path Normalization** | Home paths (`/home/user/...`) automatically collapsed to `~` in system prompt, tool call display, and brain files β keeps context lean and readable |
| **Recent File Memory** | Agent remembers recently accessed file paths across sessions β no need to re-specify paths you were just working on |
| **Compaction Summary** | Auto-compaction shows the full summary in chat as a system message β see exactly what the agent remembered |
| **Syntax Highlighting** | 100+ languages with line numbers via syntect |
| **Markdown Rendering** | Rich text formatting with code blocks, headings, lists, and inline styles |
| **Tool Context Persistence** | Tool call groups saved to DB and reconstructed on session reload β no vanishing tool history |
| **Multi-line Input** | Alt+Enter / Shift+Enter for newlines; Enter to send |
| **Abort Processing** | EscapeΓ2 within 3 seconds to cancel any in-progress request |
| **Clipboard Image Paste** | Copy an image from a browser, screenshot tool, or any app and paste it directly into the input. Raw image bytes are read from the OS clipboard (macOS: osascript, Linux: wl-paste/xclip), written to a temp file, and attached through the existing image pipeline. No need to save to disk first |
| **Bang Operator (`!cmd`)** | Run any shell command directly from the input β no LLM round-trip. Output is shown as a system message in the working directory context |
| **Auto-Update** | Checks GitHub for new releases on startup and once every 24h in the background. When a new version is found it silently installs and hot-restarts. Disable via `[agent] auto_update = false` in `config.toml` to be prompted instead |
### Agent Capabilities
| Feature | Description |
|---------|-------------|
| **Full Terminal Access** | 30+ built-in tools (file I/O, glob, grep, web search, code execution, image gen/analysis, memory search, cron jobs) plus **any CLI tool on your system** via `bash` β GitHub CLI, Docker, SSH, Python, Node, ffmpeg, curl, and everything else just work |
| **RTK Token Savings** | Automatic bash output optimization via [RTK](https://github.com/rtk-ai/rtk) integration β enabled by default, zero config. Prepends `rtk` to supported commands (git, cargo, npm, pnpm, yarn, docker, kubectl, grep, find, ls, tree, curl, and 100+ more) to filter noise from command output. Reduces token usage on bash commands by 60-90% without losing critical information. Check savings with `/rtk` command. RTK binary bundled with prebuilt OpenCrabs releases and installed by `/evolve` on update; if it is ever missing, OpenCrabs auto-downloads the right binary for your platform on first use |
| **Per-Session Isolation** | Each session is an independent agent with its own provider, model, context, and tool state. Sessions can run tasks in parallel against different providers β ask Claude a question in one session while Kimi works on code in another |
| **Self-Healing** | Detects and recovers from phantom tool calls, gaslighting preambles, text repetition loops, XML tool call failures, and provider errors. Short-circuits repeated failing bash commands and rejects interactive commands that would hang. Automatic context compaction at 65% (soft) and 90% (hard). Sticky fallback promotion when primary recovers |
| **Self-Sustaining** | Agent can modify its own source, build, test, and hot-restart via Unix `exec()` |
| **Self-Improving** | Learns from experience β saves reusable workflows as custom commands, writes lessons learned to memory, updates its own brain files. All local, no data leaves your machine |
| **Dynamic Tools** | Define custom tools at runtime via `~/.opencrabs/tools.toml` β the agent can call them autonomously like built-in tools. HTTP and shell executors, template parameters (`{{param}}`), enable/disable without restart. The `tool_manage` meta-tool lets the agent create, remove, and reload tools on the fly |
| **Skills (cross-harness)** | Multi-stage workflow templates in the de-facto `SKILL.md` format used by Claude Code, Anthropic managed agents, and OpenClaw. Drop a `SKILL.md` under `~/.opencrabs/skills//` and it auto-registers as `/` β no `commands.toml` entry needed. Works in the TUI **and** every connected channel (Telegram, Discord, Slack, WhatsApp). Built-ins ship with the binary (always version-matched); user skills override by file presence. Two built-ins out of the box: `/security-audit` (language-agnostic CVE & static-analysis audit, scores 0-100) and `/cost-estimate` (codebase valuation with AI-assisted ROI). Same `SKILL.md` is portable across harnesses |
| **Mission Control** | Full-screen `/mission-control` dialog showing every actionable artifact in one place: pending RSI proposals (inbox cards), recent RSI activity (improvements log feed), the schedule queue (cron jobs + paused/active state), and a live **Analytics** panel (brain file sizes, tool usage with proportional bars, failure rates, RSI applied by dimension). Apply or reject inbox proposals inline with `a` / `r` β same machinery as the agent's `rsi_proposals` tool, byte-identical install. Tab between panels, j/k to navigate, Enter for the detail popup, Esc to close. Cron paused jobs flag in orange, active in teal β at-a-glance state |
| **Analytics** | `/analytics` shows brain sizes, tool usage and failure rates, and RSI applications. In the TUI it opens the Mission Control Analytics panel; in any channel (Telegram, Slack, Discord, WhatsApp) it returns the report as a message. Also exposed as the `analytics_report` agent tool, so you can ask in plain language ("send me my analytics") and the agent ships it to the chat. Reads only local aggregate stats and brain file sizes, never message content or secrets, and nothing leaves the machine |
| **Skills picker** | Full-screen `/skills` dialog with a live filter input β start typing to narrow the list (case-insensitive on name + description), Tab / Shift-Tab cycle the filtered cards (wraps at the edges), Enter runs the selected skill (sends its body as a prompt to the agent), Esc closes. Built-in skills badge orange; user-installed skills badge teal. When the filter narrows to a single match, Enter just fires it β fastest path to launch a skill |
| **Browser Automation** | Native browser control via CDP (Chrome DevTools Protocol). Auto-detects your default Chromium-based browser (Chrome, Brave, Edge, Arc, Vivaldi, Opera, Chromium) and uses its profile β your logins, cookies, and extensions carry over. 7 browser tools: navigate, click, type, screenshot, eval JS, extract content, wait for elements. Headed or headless mode with display auto-detection. **Note:** Firefox is not supported (no CDP) β if Firefox is your default, OpenCrabs falls back to the first available Chromium browser. Feature-gated under `browser` (included by default) |
| **Natural Language Commands** | Tell OpenCrabs to create slash commands β it writes them to `commands.toml` autonomously via the `config_manager` tool |
| **Live Settings** | Agent can read/write `config.toml` at runtime; Settings TUI screen (press `S`) shows current config; approval policy persists across restarts. Default: auto-approve (use `/approve` to change) |
| **Web Search** | DuckDuckGo (built-in, no key needed) + EXA AI (neural, free via MCP) by default; Brave Search optional (key in `keys.toml`) |
| **Debug Logging** | `--debug` flag enables file logging; `DEBUG_LOGS_LOCATION` env var for custom log directory |
| **Agent-to-Agent (A2A)** | HTTP gateway implementing A2A Protocol RC v1.0 β peer-to-peer agent communication via JSON-RPC 2.0. Supports `message/send`, `message/stream` (SSE), `tasks/get`, `tasks/cancel`. Built-in `a2a_send` tool lets the agent proactively call remote A2A agents. Optional Bearer token auth. Includes multi-agent debate (Bee Colony) with confidence-weighted consensus. Task persistence across restarts |
| **Profiles** | Run multiple isolated instances from the same installation. Each profile gets its own config, keys, memory, sessions, and database. Create with `opencrabs profile create `, switch with `-p `. Migrate config between profiles with `profile migrate`. Export/import for sharing. Token-lock isolation prevents two profiles from using the same bot credential |
### CLI
| Command | Description |
|---------|-------------|
| `opencrabs` | Launch interactive TUI (default) |
| `opencrabs chat` | Launch TUI with optional `--session ` to resume, `--onboard` to force wizard |
| `opencrabs run ` | Execute a single prompt non-interactively. `--auto-approve` / `--yolo` for unattended. `--format text\|json\|markdown` |
| `opencrabs agent` | Interactive CLI agent β multi-turn conversation in your terminal, no TUI. `-m ` for single-message mode |
| `opencrabs status` | System overview: version, provider, channels, database, brain, cron, dynamic tools |
| `opencrabs doctor` | Full diagnostics: config, provider connectivity, database, brain, channels, CLI tools in PATH |
| `opencrabs init` | Initialize configuration (`--force` to overwrite) |
| `opencrabs config` | Show current configuration (`--show-secrets` to reveal keys) |
| `opencrabs onboard` | Run the onboarding setup wizard |
| `opencrabs channel list` | List all configured channels with enabled/disabled status |
| `opencrabs channel doctor` | Run health checks on all enabled channels |
| `opencrabs memory list` | List brain files and memory entries |
| `opencrabs memory get ` | Show contents of a specific memory or brain file |
| `opencrabs memory stats` | Memory statistics: file count, total size, entry count |
| `opencrabs session list` | List all sessions with provider, model, token count (`--all` includes archived) |
| `opencrabs session get ` | Show session details and recent messages |
| `opencrabs db init` | Initialize database |
| `opencrabs db stats` | Show database statistics |
| `opencrabs db clear` | Clear all sessions and messages (`--force` to skip confirmation) |
| `opencrabs cron add\|list\|remove\|enable\|disable\|test` | Manage scheduled cron jobs |
| `opencrabs logs status\|view\|clean\|open` | Log management |
| `opencrabs service install\|start\|stop\|restart\|status\|uninstall` | OS service management (launchd on macOS, systemd on Linux) |
| `opencrabs daemon` | Run in headless daemon mode β channels only, no TUI |
| `opencrabs completions ` | Generate shell completions (bash, zsh, fish, powershell) |
| `opencrabs migrate ` | Migrate from OpenClaw or Hermes. Scans the system, shows interactive picker, spawns agent to handle migration. `--dry-run` to preview |
| `opencrabs version` | Print version and exit |
Global flags: `--debug` (enable file logging), `--config ` (custom config file), `--profile ` / `-p ` (run as a named profile).
### Profiles β Multi-Instance Crab Agents
Run multiple isolated OpenCrabs instances from the same installation. Each profile gets its own config, brain files, memory, sessions, database, and gateway service.
| Command | Description |
|---------|-------------|
| `opencrabs profile create ` | Create a new profile with fresh config and brain files |
| `opencrabs profile list` | List all profiles with last-used timestamps |
| `opencrabs profile delete ` | Delete a profile and all its data |
| `opencrabs profile export -o profile.tar.gz` | Export a profile as a portable archive |
| `opencrabs profile import profile.tar.gz` | Import a profile from an archive |
| `opencrabs profile migrate --from --to ` | Copy config and brain files between profiles (no DB or sessions) |
| `opencrabs -p ` | Launch OpenCrabs as the specified profile |
**Default profile:** `~/.opencrabs/` β works exactly as before. No migration needed. Users who never touch profiles see zero difference.
**TUI footer:** when you launch with `-p ` (or `OPENCRABS_PROFILE` set), the bottom status bar adds a `profile: ` chip so multi-profile users can tell at a glance which instance a given pane is bound to. Without `-p`, no chip is shown β the agent is using the base `~/.opencrabs/` directory and there is no real profile by that name to label.
**Named profiles** live at `~/.opencrabs/profiles//` with full isolation:
```
~/.opencrabs/
βββ config.toml # default profile
βββ opencrabs.db
βββ profiles.toml # profile registry
βββ locks/ # token-lock files
βββ profiles/
βββ hermes/ # named profile
β βββ config.toml
β βββ keys.toml
β βββ opencrabs.db
β βββ SOUL.md
β βββ memory/
βββ scout/
βββ ...
```
**Token-lock isolation:** Two profiles cannot use the same bot credential (Telegram token, Discord token, etc.). On startup, each profile acquires a lock on its channel tokens. If another profile already holds the lock, the channel refuses to start β preventing two instances from fighting over the same bot.
**Profile migration:** Use `opencrabs profile migrate --from default --to hermes` to copy all `.md` brain files, `.toml` config files, and `memory/` entries to a new profile. Sessions and database are not copied β the new profile starts clean. Add `--force` to overwrite existing files in the target profile. After migrating, customize the new profile's `SOUL.md`, `IDENTITY.md`, and `config.toml` to give it a different personality and provider setup.
### Daemon & Service
Run profiles as background services:
```bash
# Install as system service (macOS launchd / Linux systemd)
opencrabs -p hermes service install
opencrabs -p hermes service start
# Each profile gets its own service
# macOS: com.opencrabs.daemon.hermes
# Linux: opencrabs-hermes.service
# Manage independently
opencrabs -p hermes service status
opencrabs -p hermes service stop
opencrabs -p hermes service uninstall
```
Multiple profiles can run as simultaneous daemon services with full isolation.
**Environment variable:** Set `OPENCRABS_PROFILE=hermes` to select a profile without the `-p` flag. Useful for systemd services, cron jobs, and daemon mode.
---
## π Migrating from Other Tools
Switching to OpenCrabs from another AI agent tool? There are two ways to do it.
### Option 1: CLI Command (automated)
The `migrate` command scans your system for existing tool instances, shows an interactive picker if it finds multiple, then spawns an agent to handle the actual migration:
```bash
# Migrate from OpenClaw
opencrabs migrate openclaw
# Migrate from Hermes
opencrabs migrate hermes
# Preview what would be migrated without making changes
opencrabs migrate openclaw --dry-run
```
What it migrates:
- **Brain files** (SOUL.md, USER.md, MEMORY.md, AGENTS.md, TOOLS.md)
- **Config** (provider, model, channels)
- **API keys and secrets**
- **Memory logs and skills** (if present)
The agent reads each source file, maps it to OpenCrabs format, and writes the result. If a brain file already has content, it merges rather than overwrites. After the agent finishes, a verification summary shows exactly which files were created, updated, or left unchanged.
Use `-p ` to migrate into a specific profile instead of the default:
```bash
opencrabs -p my-openclaw-setup migrate openclaw
```
### Option 2: Just Ask the Agent
No CLI command needed. Once OpenCrabs is running, tell it:
> *"I'm migrating from [tool name]. Research what files I have, audit the config structure, and report what I need to move over for a seamless migration."*
The agent will use its tools (read_file, web_search, grep) to inspect the source tool's directory, understand the config format, and either do the migration directly or give you a step-by-step plan. This works for **any** tool, not just OpenClaw and Hermes. Claude Code, Cursor, Aider, Windsurf, whatever. If it has config files and brain/personality files, OpenCrabs can figure out the mapping.
The agent-native approach handles edge cases that hardcoded parsers can't: custom configs, non-standard setups, partial installations, format changes across versions.
### Supported Sources
| Source | Directory | Config Format | Status |
|--------|-----------|---------------|--------|
| **OpenClaw** | `~/.openclaw/` | JSON5 (`openclaw.json`) | CLI + agent |
| **Hermes** | `~/.hermes/` | YAML (`config.yaml`) + `.env` | CLI + agent |
| **Claude Code** | `~/.claude/` | JSON | Agent only |
| **Anything else** | varies | varies | Agent only |
The CLI `migrate` command currently supports OpenClaw and Hermes. For everything else, the agent handles it via chat. Both approaches use the same underlying mechanism: read source files, map to OpenCrabs format, write the result.
---
## π Supported AI Providers
| Provider | Auth | Models | Streaming | Tools | Notes |
|----------|------|--------|-----------|-------|-------|
| **Xiaomi** β | Free, no key\* | mimo-v2.5-pro (1M ctx), v2-pro, v2.5, omni, flash | β
| β
| **Default provider.** opencrabs Γ Xiaomi collab β free inference, no API key needed during the launch window (until 2026-06-25). New users land almost straight in chat |
| [Anthropic Claude](#anthropic-claude) | API key | Claude Opus 4.6, Sonnet 4.5, Haiku 4.5+ | β
| β
| Cost tracking, automatic retry |
| [OpenAI](#openai) | API key | GPT-5 Turbo, GPT-5 | β
| β
| |
| [GitHub Copilot](#github-copilot) | OAuth | GPT-4o, Claude Sonnet 4+ | β
| β
| Uses your Copilot subscription β no API charges |
| [OpenRouter](#openrouter--400-models-one-key) | API key | 400+ models | β
| β
| Free models available (DeepSeek-R1, Llama 3.3, etc.) |
| [Google Gemini](#google-gemini) | API key | Gemini 2.5 Flash, 2.0, 1.5 Pro | β
| β
| 1M+ context, vision, image generation |
| [MiniMax](#minimax) | API key | M2.7, M2.5, M2.1, Text-01 | β
| β
| Competitive pricing, auto-configured vision |
| [z.ai GLM](#zai-glm) | API key | GLM-4.5 through GLM-5 Turbo | β
| β
| General API + Coding API endpoints |
| [Claude CLI](#claude-code-cli) | CLI auth | Via `claude` binary | β
| β
| Uses your Claude Code subscription |
| [OpenCode CLI](#opencode-cli) | None | Free models (Mimo, etc.) | β
| β
| Free β no API key or subscription needed |
| [Codex CLI](#codex-cli) | CLI auth | GPT-5.5, 5.4, 5.4-mini, 5.3-codex | β
| β
| Uses your ChatGPT/Codex subscription |
| [Qwen (Native)](#qwen-native) | OAuth | Qwen3.6-Plus, Qwen3.5-Plus, Qwen3-Max | β
| β
| Free tier (60 req/min, 1k/day). Multi-account rotation multiplies quota |
| [Qwen Code CLI](#qwen-code-cli) | OAuth / API key | Qwen3-Coder-Plus, Qwen3.5-Plus, Qwen3.6-Plus | β
| β
| 1k free req/day via Qwen OAuth β no API key needed |
| [Ollama](#ollama) | Optional | Any pulled model | β
| β
| Local-first, zero API cost. Auto-detects localhost:11434 |
| [Custom](#custom-openai-compatible) | Optional | Any | β
| β
| LM Studio, Groq, NVIDIA, any OpenAI-compatible API |
### Anthropic Claude
**Models:** `claude-opus-4-6`, `claude-sonnet-4-5-20250929`, `claude-haiku-4-5-20251001`, plus legacy Claude 3.x models
**Setup** in `keys.toml`:
```toml
[providers.anthropic]
api_key = "sk-ant-api03-YOUR_KEY"
```
> **OAuth tokens no longer supported.** Anthropic disabled OAuth (`sk-ant-oat`) for third-party apps as of Feb 2026. Only console API keys (`sk-ant-api03-*`) work. See [anthropics/claude-code#28091](https://github.com/anthropics/claude-code/issues/28091).
**Features:** Streaming, tools, cost tracking, automatic retry with backoff
#### Claude Code CLI
Use your Claude Code CLI. OpenCrabs spawns the local `claude` CLI for completion.
**Setup:**
1. Install [Claude Code CLI](https://github.com/anthropics/claude-code) and authenticate (`claude login`)
2. Enable in `config.toml`:
```toml
[providers.claude_cli]
enabled = true
```
OpenCrabs handles all tools, memory, and context locally β the CLI is just the LLM backend.
### OpenAI
**Models:** GPT-5 Turbo, GPT-5
**Setup** in `keys.toml`:
```toml
[providers.openai]
api_key = "sk-YOUR_KEY"
```
### GitHub Copilot
**Use your GitHub Copilot subscription** β no API charges, no tokens to manage. OpenCrabs authenticates via the same OAuth device flow used by VS Code and other Copilot tools.
**Setup** β select GitHub Copilot in the onboarding wizard and press Enter. You'll see a one-time code to enter at [github.com/login/device](https://github.com/login/device). Once authorized, models are fetched from the Copilot API automatically.
**Requirements:** An active [GitHub Copilot](https://github.com/features/copilot) subscription (Individual, Business, or Enterprise).
Manual config (without wizard)
The OAuth token is saved automatically during onboarding. If you need to re-authenticate, run `/onboard:provider` and select GitHub Copilot.
Enable in `config.toml`:
```toml
[providers.github]
enabled = true
default_model = "gpt-4o"
base_url = "https://api.githubcopilot.com/chat/completions"
```
**Features:** Streaming, tools, OpenAI-compatible API at `api.githubcopilot.com`. Copilot-specific headers (`copilot-integration-id`, `editor-version`) are injected automatically. Short-lived API tokens are refreshed in the background every ~25 minutes.
### OpenRouter β 400+ Models, One Key
**Setup** in `keys.toml` β get a key at [openrouter.ai/keys](https://openrouter.ai/keys):
```toml
[providers.openrouter]
api_key = "sk-or-YOUR_KEY"
```
Access 400+ models from every major provider through a single API key β Anthropic, OpenAI, Google, Meta, Mistral, DeepSeek, Qwen, and many more. Includes **free models** (DeepSeek-R1, Llama 3.3, Gemma 2, Mistral 7B) and stealth/preview models as they drop.
Model list is **fetched live** from the OpenRouter API during onboarding and via `/models` β no binary update needed when new models are added.
### Google Gemini
**Models:** `gemini-2.5-flash`, `gemini-2.0-flash`, `gemini-1.5-pro` β fetched live from the Gemini API
**Setup** in `keys.toml` β get a key at [aistudio.google.com](https://aistudio.google.com):
```toml
[providers.gemini]
api_key = "AIza..."
```
Enable and set default model in `config.toml`:
```toml
[providers.gemini]
enabled = true
default_model = "gemini-2.5-flash"
```
**Features:** Streaming, tool use, vision, 1M+ token context window, live model list from `/models` endpoint
> **Image & video generation & vision:** Gemini also powers the separate `[image]` section for `generate_image`, `analyze_image`, and `analyze_video` agent tools. See [Image Generation & Vision](#-image-generation--vision) below.
### MiniMax
**Models:** `MiniMax-M2.7`, `MiniMax-M2.5`, `MiniMax-M2.1`, `MiniMax-Text-01`
**Setup** β get your API key from [platform.minimax.io](https://platform.minimax.io). Add to `keys.toml`:
```toml
[providers.minimax]
api_key = "your-api-key"
```
MiniMax is an OpenAI-compatible provider with competitive pricing. It does not expose a `/models` endpoint, so the model list comes from `config.toml` (pre-configured with available models).
### z.ai GLM
**Models:** `glm-4.5`, `glm-4.5-air`, `glm-4.6`, `glm-4.7`, `glm-5`, `glm-5-turbo` β fetched live from the z.ai API
**Setup** β get your API key from [open.bigmodel.cn](https://open.bigmodel.cn). Add to `keys.toml`:
```toml
[providers.zhipu]
api_key = "your-api-key"
```
Enable and choose endpoint type in `config.toml`:
```toml
[providers.zhipu]
enabled = true
default_model = "glm-4.7"
endpoint_type = "api" # "api" (General API) or "coding" (Coding API)
```
z.ai GLM (Zhipu AI) offers two endpoint types selectable during onboarding or via `/models`:
- **General API** (`api`) β standard chat completions at `open.bigmodel.cn/api/paas/v4`
- **Coding API** (`coding`) β code-optimized endpoint at `codeapi.bigmodel.cn/api/paas/v4`
Both use the same API key and model names. The endpoint type can be toggled in the onboarding wizard or `/models` dialog.
**Features:** Streaming, tools, OpenAI-compatible API, live model list from `/models` endpoint
### OpenCode CLI
Use the [OpenCode](https://github.com/opencode-ai/opencode) CLI as a free LLM backend β no API key or subscription needed. OpenCrabs spawns the local `opencode` binary for completions.
**Setup:**
1. Install [OpenCode CLI](https://github.com/opencode-ai/opencode) (`go install github.com/opencode-ai/opencode@latest` or download from releases)
2. Enable in `config.toml`:
```toml
[providers.opencode_cli]
enabled = true
default_model = "opencode/mimo-v2-pro-free"
```
Models are fetched live from `opencode models`. Free models like `mimo-v2-pro-free` work without any authentication.
**Features:** Streaming, tools, extended thinking support, NDJSON event protocol
### Codex CLI
Use your **ChatGPT/Codex subscription** β no API key, no API charges. OpenCrabs spawns the local `codex` binary for completions and piggybacks on the auth stored in `~/.codex/auth.json`.
**Setup:**
1. Install [Codex CLI](https://github.com/openai/codex): `npm install -g @openai/codex`
2. Run `codex` once and sign in with your ChatGPT account (or paste an API key).
3. Enable in `config.toml`:
```toml
[providers.codex_cli]
enabled = true
default_model = "gpt-5.5" # falls back to gpt-5.4 if 5.5 isn't in your account yet
```
**Models** (per [developers.openai.com/codex/models](https://developers.openai.com/codex/models)):
- `gpt-5.5` β newest frontier model (ChatGPT-auth only during rollout)
- `gpt-5.4` β recommended fallback when 5.5 isn't available; supports API access
- `gpt-5.4-mini` β fast, lower-cost option for lighter coding work
- `gpt-5.3-codex` β coding-optimized model that powers GPT-5.4 underneath
- `gpt-5.3-codex-spark` β research preview for ChatGPT Pro (real-time iteration)
- `gpt-5.2` β alternative tier for hard debugging
OpenCrabs handles all tools, memory, and context locally; codex is just the LLM backend. The CLI runs `codex exec --json --ephemeral --dangerously-bypass-approvals-and-sandbox` so each turn is a fresh session driven by OpenCrabs' conversation state.
**Features:** Streaming, tools (codex executes its own shell commands and they're surfaced to the TUI for display), JSONL event protocol
### Qwen (Native)
Direct integration with Qwen's API via OAuth device flow β **no API key needed**. Free tier gives 60 req/min and 1,000 req/day per account.
**Setup:** Select Qwen in `/onboard` or `/models` and follow the browser OAuth flow.
**Multi-account rotation:** Multiply your free quota by authenticating multiple Qwen accounts. When one account hits rate limits, OpenCrabs automatically rotates to the next β only falling to the fallback provider when all accounts are exhausted.
To enable rotation during setup:
1. Select Qwen in `/onboard` or `/models`
2. Press `Space` to toggle rotation
3. Press `2-9` to set the number of accounts (`1` = 10)
4. Press `Enter` β authenticate each account in sequence (sign out in your browser between accounts)
Adding more accounts later (e.g. 3β5) is incremental β only the new accounts need authentication. Configured providers show a green **β** in the provider list.
Rotation accounts are stored in `keys.toml`:
```toml
[[providers.qwen_accounts]]
api_key = "..."
refresh_token = "..."
expiry_date = 1234567890
resource_url = "portal.qwen.ai"
[[providers.qwen_accounts]]
api_key = "..."
refresh_token = "..."
expiry_date = 1234567891
resource_url = "portal.qwen.ai"
```
With 3 accounts you get **180 req/min** and **3,000 req/day** before fallback kicks in.
### Qwen Code CLI
Use the [Qwen Code](https://github.com/qwen-code/qwen-code) CLI as a free LLM backend β **1,000 free requests/day** via Qwen OAuth. OpenCrabs spawns the local `qwen` binary for completions.
**Setup:**
1. Install Qwen Code CLI (`npm install -g @qwen-code/qwen-code` or `brew install qwen-code`)
2. Authenticate: run `qwen` and follow the OAuth flow (or set `DASHSCOPE_API_KEY` for API key auth)
3. Enable in `config.toml`:
```toml
[providers.qwen_code_cli]
enabled = true
default_model = "qwen3-coder-plus"
```
**Available models:** `qwen3-coder-plus`, `qwen3.5-plus`, `qwen3.6-plus`, `qwen3-coder-480a35`, `qwen3-coder-30ba3b`, `qwen3-max-2026-01-23`
**Features:** Streaming, tools, 256K context window, NDJSON event protocol (Gemini CLI fork)
### Ollama
Native built-in provider for [Ollama](https://ollama.com/) β no custom config needed. OpenCrabs auto-detects `localhost:11434` and lists your pulled models via `/models`.
**Setup:**
1. Install Ollama and pull a model: `ollama pull qwen2.5-coder:7b`
2. Enable in `config.toml`:
```toml
[providers.ollama]
enabled = true
default_model = "qwen2.5-coder:7b"
```
**Features:** Streaming, tools, zero API cost, local-first. Optional `api_key` for cloud Ollama (`api.ollama.com`). Models fetched live from Ollama API.
### Custom (OpenAI-Compatible)
**Use for:** LM Studio, Groq, or any OpenAI-compatible API.
**Setup** in `config.toml` β every custom provider needs a name (the label after `custom.`):
```toml
[providers.custom.lm_studio]
enabled = true
base_url = "http://localhost:1234/v1" # or your endpoint
default_model = "qwen2.5-coder-7b-instruct"
# Optional: list your available models β shows up in /models and /onboard
# so you can switch between them without editing config
models = ["qwen2.5-coder-7b-instruct", "llama-3-8B", "mistral-7B-instruct"]
```
> **Local LLMs (Ollama, LM Studio):** No API key needed β just set `base_url` and `default_model`.
>
> **Remote APIs (Groq, Together, etc.):** Add the key in `keys.toml` using the same name:
> ```toml
> [providers.custom.groq]
> api_key = "your-api-key"
> ```
> **Note:** `/chat/completions` is auto-appended to base URLs that don't include it.
> **Local reasoning models (`enable_thinking`):** when a custom provider's `base_url` points at a local host (`localhost`, `127.0.0.1`, `*.local`, or an RFC1918 private IP like `192.168.x.x` / `10.x.x.x` / `172.16.x.x`β`172.31.x.x`), OpenCrabs injects `chat_template_kwargs: {"enable_thinking": true}` into every request. This mirrors `llama-server --jinja --chat-template-kwargs '{"enable_thinking":true}'` β what Unsloth Studio launches with by default β so Qwen3 / Kimi / DeepSeek-R1 templates render `` tags and reasoning blocks correctly, and tool calls actually execute instead of being hallucinated as text. Set `enable_thinking = false` in the provider block to disable (falls back to fast, non-thinking mode). Cloud providers are unaffected.
>
> ```toml
> [providers.custom.lm_studio]
> enabled = true
> base_url = "http://localhost:1234/v1"
> default_model = "qwen3-30b-a3b"
> enable_thinking = false # optional β default is true for local providers
> ```
> **Qwen / Alibaba cache (zero-config):** when a custom provider's `base_url` points at a known Qwen / Alibaba endpoint (`dashscope.aliyuncs.com`, `dashscope-intl.aliyuncs.com`, `aliyun.com`, `dialagram.me`) or the request model name starts with `qwen-`, OpenCrabs auto-injects `cache_control: {"type": "ephemeral"}` markers on the system message, the last message (streaming), and the last tool definition. This unlocks Alibaba's [explicit context cache](https://www.alibabacloud.com/help/en/model-studio/explicit-cache-best-practice) β cache hits bill input tokens at 10% of the standard price (β90% off), with a 25% surcharge on first creation and a 5-minute TTL auto-renewed on every hit. The detection runs per-request so a provider routing between Qwen and non-Qwen models only marks the Qwen ones. Non-Qwen backends ignore the marker (it's an unknown JSON field), so the only cost on a mismatch is a few wasted bytes per request.
>
> ```toml
> [providers.custom.dashscope]
> enabled = true
> base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
> default_model = "qwen3-max"
> # No cache flags needed β auto-enabled on URL match.
> ```
**Multiple custom providers** coexist β define as many as you need with different names and switch between them via `/models`:
```toml
[providers.custom.lm_studio]
enabled = true
base_url = "http://localhost:1234/v1"
default_model = "qwen2.5-coder-7b-instruct"
[providers.custom.ollama]
enabled = false
base_url = "http://localhost:11434/v1"
default_model = "mistral"
```
The name after `custom.` is a label you choose (e.g. `lm_studio`, `nvidia`, `groq`). The one with `enabled = true` is active. Keys go in `keys.toml` using the same label. All configured custom providers persist β switching via `/models` just toggles `enabled`.
#### Free Prototyping with NVIDIA API + Kimi K2.5
[Kimi K2.5](https://build.nvidia.com/moonshotai/kimi-k2.5) is a frontier-scale multimodal Mixture-of-Experts (MoE) model available **for free** on the NVIDIA API Catalog β no billing setup or credit card required. It handles complex reasoning and image/video understanding, making it a strong free alternative to paid models like Claude or Gemini for experimentation and agentic workflows.
**Tested and verified** with OpenCrabs Custom provider setup.
**Quick start:**
1. Sign up at the [NVIDIA API Catalog](https://build.nvidia.com/) and verify your account
2. Go to the [Kimi K2.5 model page](https://build.nvidia.com/moonshotai/kimi-k2.5) and click **Get API Key** (or "View Code" to see an auto-generated key)
3. Configure in OpenCrabs via `/models` or `config.toml`:
```toml
[providers.custom.nvidia]
enabled = true
base_url = "https://integrate.api.nvidia.com/v1"
default_model = "moonshotai/kimi-k2.5"
```
```toml
# keys.toml
[providers.custom.nvidia]
api_key = "nvapi-..."
```
**Provider priority:** Claude CLI > OpenCode CLI > Codex CLI > OpenCode > Qwen > Anthropic > OpenAI > GitHub Copilot > Gemini > OpenRouter > MiniMax > z.ai GLM > Ollama > Custom. The first provider with `enabled = true` is used on new sessions. Each provider has its own API key in `keys.toml` β no sharing or confusion.
**Per-session provider:** Each session remembers which provider and model it was using. Switch to Claude in one session, Kimi in another β when you `/sessions` switch between them, the provider restores automatically. No need to `/models` every time. New sessions inherit the current provider.
### Fallback Providers
If your primary provider goes down, fallback providers are tried automatically in sequence. Any provider with API keys already configured can be a fallback:
```toml
[providers.fallback]
enabled = true
providers = ["openrouter", "anthropic"] # tried in order on failure
```
At runtime, if a request to the primary fails, each fallback is tried until one succeeds. Supports single (`provider = "openrouter"`) or multiple providers.
### Per-Provider Vision Model
If your default model doesn't support vision but another model on the same provider does, set `vision_model`. The LLM calls `analyze_image` as a tool β the vision model describes the image and returns the description to the chat model as context:
```toml
[providers.minimax]
default_model = "MiniMax-M2.7"
vision_model = "MiniMax-Text-01" # describes images for the chat model
```
MiniMax auto-configures this on first run. Works with any provider β just set `vision_model` to a vision-capable model on the same API.
---
## πΌοΈ Image Generation & Vision
OpenCrabs supports image generation and vision analysis via Google Gemini by default β independent of the main chat provider, so you can use Claude for chat and Gemini for images. The `generate_image` tool can also be routed at any OpenAI-compatible images endpoint (OpenAI, OpenRouter, Together, etc.) via the per-provider `generation_model` override; see [Picking a different generation model](#picking-a-different-generation-model) below.
### Setup
1. Get a free API key from [aistudio.google.com](https://aistudio.google.com)
2. Run `/onboard:image` in chat (or go through onboarding Advanced mode) to configure
3. Or add manually to `keys.toml`:
```toml
[image]
api_key = "AIza..."
```
And `config.toml`:
```toml
[image.generation]
enabled = true
model = "gemini-3.1-flash-image-preview"
[image.vision]
enabled = true
model = "gemini-3.1-flash-image-preview"
```
### Agent Tools
When enabled, three tools become available to the agent automatically:
| Tool | Description |
|------|-------------|
| `generate_image` | Generate an image from a text prompt β saves to `~/.opencrabs/images/` and returns the file path |
| `analyze_image` | Analyze an image file or URL via Gemini vision β works even when your main model doesn't support vision |
| `analyze_video` | Analyze a video file via Gemini's multimodal video API β inline β€18 MB, resumable Files API for larger (up to 2 GB / ~1 hour). Requires Gemini config; frame-extraction fallback for non-Gemini providers not yet wired |
**Example prompts:**
- _"Generate a pixel art crab logo"_ β agent calls `generate_image`, returns file path
- _"What's in this image: /tmp/screenshot.png"_ β agent calls `analyze_image` via Gemini
- _"Summarize what happens in /tmp/clip.mp4"_ β agent calls `analyze_video` via Gemini
### Model
Both tools use `gemini-3.1-flash-image-preview` ("Nano Banana") by default β Gemini's dedicated image-generation model that supports both vision input and image output in a single request.
### Picking a different generation model
The seeded Gemini default works out of the box, but you can override the model used by `generate_image` per-provider without leaving the TUI:
- **From the wizard** β `/onboard:image` shows a "Generation model" input below the toggle. Empty keeps the seeded default; type any model name to override.
- **From config.toml** β set `generation_model = "..."` under any `[providers.]` block. The active session's provider wins over `image.generation.model`.
Backend dispatch is automatic, based on the provider's `base_url`:
| Provider URL | Wire backend | Example `generation_model` |
|--------------|--------------|----------------------------|
| `generativelanguage.googleapis.com` | Gemini `:generateContent` | `imagen-4.0-generate-001` |
| Anything else (openai, openrouter, custom, β¦) | OpenAI `/v1/images/generations` (Bearer auth) | `gpt-image-1`, `black-forest-labs/flux-1.1-pro`, `stable-diffusion-3.5-large` |
The OpenAI backend prefers `response_format=b64_json` and falls back to fetching the `url` field for providers that don't honour the format flag, so the saved file is always a local PNG.
---
## π€ Agent-to-Agent (A2A) Protocol
OpenCrabs includes a built-in A2A gateway β an HTTP server implementing the [A2A Protocol RC v1.0](https://google.github.io/A2A/) for peer-to-peer agent communication. Other A2A-compatible agents can discover OpenCrabs, send it tasks, and get results back β all via standard JSON-RPC 2.0. The agent can also proactively call remote A2A agents using the built-in `a2a_send` tool.
### Enabling A2A
Add to `~/.opencrabs/config.toml`:
```toml
[a2a]
enabled = true
bind = "127.0.0.1" # Loopback only (default) β use "0.0.0.0" to expose
port = 18790 # Gateway port
# api_key = "your-secret" # Optional Bearer token auth for incoming requests
# allowed_origins = ["http://localhost:3000"] # CORS (empty = blocked)
```
### Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/.well-known/agent.json` | GET | Agent Card β discover skills, capabilities, supported content types |
| `/a2a/v1` | POST | JSON-RPC 2.0 β `message/send`, `message/stream` (SSE), `tasks/get`, `tasks/cancel` |
| `/a2a/health` | GET | Health check |
### `a2a_send` Tool
The agent has a built-in `a2a_send` tool to communicate with remote A2A agents:
| Action | Description |
|--------|-------------|
| `discover` | Fetch a remote agent's Agent Card (no approval needed) |
| `send` | Send a task message to a remote agent |
| `get` | Check status of a remote task |
| `cancel` | Cancel a running remote task |
The tool supports optional `api_key` for authenticated endpoints and `context_id` for multi-turn conversations.
### Connecting Two Agents
**VPS agent** (`config.toml`):
```toml
[a2a]
enabled = true
bind = "0.0.0.0"
port = 18790
api_key = "shared-secret"
```
**Local agent** β connect via SSH tunnel (recommended, no ports to open):
```bash
ssh -L 18791:127.0.0.1:18790 user@your-vps
```
Now the local agent can reach the VPS agent at `http://127.0.0.1:18791`. The agent will use `a2a_send` with that URL automatically.
### Quick Start Examples
```bash
# Discover the agent
curl http://127.0.0.1:18790/.well-known/agent.json | jq .
# Send a message (creates a task)
curl -X POST http://127.0.0.1:18790/a2a/v1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"text": "What tools do you have?"}]
}
}
}'
# Poll a task by ID
curl -X POST http://127.0.0.1:18790/a2a/v1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret" \
-d '{"jsonrpc":"2.0","id":2,"method":"tasks/get","params":{"id":"TASK_ID"}}'
# Cancel a running task
curl -X POST http://127.0.0.1:18790/a2a/v1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret" \
-d '{"jsonrpc":"2.0","id":3,"method":"tasks/cancel","params":{"id":"TASK_ID"}}'
```
### Bee Colony Debate
OpenCrabs supports multi-agent structured debate via the **Bee Colony** protocol β based on [ReConcile (ACL 2024)](https://arxiv.org/abs/2309.13007) confidence-weighted voting. Multiple "bee" agents argue across configurable rounds, each enriched with knowledge context from QMD memory search, then converge on a consensus answer with confidence scores.
### Security & Persistence
- **Loopback only** by default β binds to `127.0.0.1`, not `0.0.0.0`
- **Bearer token auth** β set `api_key` to require `Authorization: Bearer ` on all JSON-RPC requests
- **CORS locked down** β no cross-origin requests unless `allowed_origins` is explicitly set
- **Task persistence** β active tasks survive restarts via SQLite
- For public exposure, use a reverse proxy (nginx/Caddy) with TLS + the `api_key` auth
---
## π Quick Start
### Option 1: Download Binary (just run it)
Grab a pre-built binary from [GitHub Releases](https://github.com/adolfousier/opencrabs/releases) β available for Linux (amd64/arm64), macOS (amd64/arm64), and Windows.
```bash
# Download, extract, run
tar xzf opencrabs-linux-amd64.tar.gz
./opencrabs
```
The onboarding wizard handles everything on first run.
> **Terminal permissions required.** OpenCrabs reads/writes brain files, config, and project files. Your terminal app needs filesystem access or the OS will block operations.
>
> | OS | What to do |
> |---|---|
> | **macOS** | **System Settings β Privacy & Security β Full Disk Access** β toggle your terminal app ON (Alacritty, iTerm2, Terminal, etc.). If not listed, click "+" and add it from `/Applications/`. Without this, macOS repeatedly prompts "would like to access data from other apps". |
> | **Windows** | Run your terminal (Windows Terminal, PowerShell, cmd) **as Administrator** on first run, or grant the terminal **write access** to `%USERPROFILE%\.opencrabs\` and your project directories. Windows Defender may also prompt β click "Allow". |
> | **Linux** | Ensure your user owns `~/.opencrabs/` and project directories. On SELinux/AppArmor systems, the terminal process needs read/write access to those paths. Flatpak/Snap terminals may need `--filesystem=home` or equivalent permission. |
> **Linux runtime dependencies:** The pre-built binary links against system libraries that may not be installed on minimal/VPS images:
> ```bash
> # Debian/Ubuntu
> sudo apt-get install libgomp1 libasound2
> # Fedora/RHEL
> sudo dnf install libgomp alsa-lib
> # Arch
> sudo pacman -S gcc-libs alsa-lib
> ```
> `libgomp` (GCC OpenMP) is required by the local embedding engine (`llama-cpp-2`). `libasound` is required for local speech-to-text audio I/O. macOS and Windows binaries have no extra prerequisites.
>
> **Local TTS (Piper) additionally requires Python 3 with the `venv` module:**
> ```bash
> # Debian/Ubuntu
> sudo apt-get install python3 python3-venv
> # Fedora/RHEL
> sudo dnf install python3
> # macOS
> brew install python3
> # Windows
> winget install Python.Python.3
> ```
> Not needed if you use API TTS (OpenAI) or disable TTS entirely.
> **Note:** `/rebuild` works even with pre-built binaries β it auto-clones the source to `~/.opencrabs/source/` on first use, then builds and hot-restarts. For active development or adding custom tools, Option 2 gives you the source tree directly.
### Option 2: Install via Cargo
```bash
cargo install opencrabs
```
> **Linux (Debian/Ubuntu):** Install system deps first: `sudo apt-get install build-essential pkg-config clang libclang-dev libasound2-dev libssl-dev cmake`
>
> **Large build:** The build can use 8GB+ in `/tmp`. If you run out of space: `CARGO_TARGET_DIR=~/.cargo/target cargo install opencrabs`
#### Feature Flags
All features are enabled by default. To customize, use `--no-default-features` and pick what you need:
```bash
# Install with only Telegram and Discord
cargo install opencrabs --no-default-features --features "telegram,discord"
# Everything except browser automation
cargo install opencrabs --no-default-features --features "telegram,whatsapp,discord,slack,trello,local-stt,local-tts"
```
| Feature | Crate | Description |
|---------|-------|-------------|
| `telegram` | teloxide | Telegram bot channel |
| `whatsapp` | whatsapp-rust | WhatsApp channel via multi-device API |
| `discord` | serenity | Discord bot channel |
| `slack` | slack-morphism | Slack bot channel (Socket Mode) |
| `trello` | β | Trello webhook channel |
| `local-stt` | rwhisper | On-device speech-to-text (requires CMake + C++ compiler) |
| `local-tts` | opusic-sys | On-device text-to-speech (requires `python3` + `python3-venv` at runtime) |
| `browser` | chromey | Browser automation via CDP (Chrome, Brave, Edge, Arc, Vivaldi, Opera β not Firefox) |
### Option 3: Build from Source (full control)
Required for `/rebuild`, adding custom tools, or modifying the agent.
**Prerequisites:**
- **Rust stable (1.91+)** β [Install Rust](https://rustup.rs/). The project includes a `rust-toolchain.toml` that selects the correct toolchain automatically
- **An API key** from at least one supported provider
- **SQLite** (bundled via sqlx)
- **macOS:** Xcode CLI Tools + `brew install cmake pkg-config` (requires macOS 15+)
- **Linux (Debian/Ubuntu):** `sudo apt-get install build-essential pkg-config clang libclang-dev libasound2-dev libssl-dev cmake`
- **Linux (Fedora/RHEL):** `sudo dnf install gcc gcc-c++ make pkg-config clang openssl-devel cmake alsa-lib-devel libgomp`
- **Linux (Arch):** `sudo pacman -S base-devel pkg-config clang openssl cmake alsa-lib gcc-libs`
> **One-liner setup:** `bash <(curl -sL https://raw.githubusercontent.com/adolfousier/opencrabs/main/src/scripts/setup.sh)` β detects your platform, installs all dependencies, and sets up Rust.
```bash
# Clone
git clone https://github.com/adolfousier/opencrabs.git
cd opencrabs
# Build & run (development)
cargo run --bin opencrabs
# Or build release and run directly
cargo build --release
./target/release/opencrabs
```
> **Linux on older CPUs (Sandy Bridge / AVX1-only, no AVX2):** The local STT and embedding engine require at minimum AVX instructions. If your CPU has AVX but not AVX2 (e.g. Intel Sandy Bridge, Ivy Bridge β roughly 2011β2012), you must build with:
> ```bash
> RUSTFLAGS="-C target-cpu=native" cargo run --bin opencrabs
> # or for release:
> RUSTFLAGS="-C target-cpu=native" cargo build --release
> ```
> CPUs without AVX at all are not supported for local STT/embedding. API STT mode works on any machine.
> **API Keys:** OpenCrabs uses `keys.toml` instead of `.env` for API keys. The onboarding wizard will help you set it up, or edit `~/.opencrabs/keys.toml` directly. Keys are handled at runtime β no OS environment pollution.
> **First run?** The onboarding wizard will guide you through provider setup, workspace, and more. See [Onboarding Wizard](#-onboarding-wizard).
### Option 3: Docker (sandboxed)
Run OpenCrabs in an isolated container. Build takes ~15min (Rust release + LTO).
```bash
# Clone and run
git clone https://github.com/adolfousier/opencrabs.git
cd opencrabs
# Run with docker compose
# API keys are mounted from keys.toml on host
docker compose -f src/docker/compose.yml up --build
```
Config, workspace, and memory DB persist in a Docker volume across restarts. API keys in `keys.toml` are mounted into the container at runtime β never baked into the image.
### CLI Commands
```bash
# Interactive TUI (default)
cargo run --bin opencrabs
cargo run --bin opencrabs -- chat
# Onboarding wizard (first-time setup)
cargo run --bin opencrabs -- onboard
cargo run --bin opencrabs -- chat --onboard # Force wizard before chat
# Non-interactive single command
cargo run --bin opencrabs -- run "What is Rust?"
cargo run --bin opencrabs -- run --format json "List 3 programming languages"
cargo run --bin opencrabs -- run --format markdown "Explain async/await"
# Configuration
cargo run --bin opencrabs -- init # Initialize config
cargo run --bin opencrabs -- config # Show current config
cargo run --bin opencrabs -- config --show-secrets
# Database
cargo run --bin opencrabs -- db init # Initialize database
cargo run --bin opencrabs -- db stats # Show statistics
# Profiles β multi-instance isolation
cargo run --bin opencrabs -- profile create hermes
cargo run --bin opencrabs -- profile list
cargo run --bin opencrabs -- -p hermes # Run as "hermes" profile
cargo run --bin opencrabs -- profile migrate --from default --to hermes
cargo run --bin opencrabs -- profile export hermes -o backup.tar.gz
cargo run --bin opencrabs -- profile import backup.tar.gz
cargo run --bin opencrabs -- profile delete hermes
# Debug mode
cargo run --bin opencrabs -- -d # Enable file logging
cargo run --bin opencrabs -- -d run "analyze this"
# Log management
cargo run --bin opencrabs -- logs status
cargo run --bin opencrabs -- logs view
cargo run --bin opencrabs -- logs view -l 100
cargo run --bin opencrabs -- logs clean
cargo run --bin opencrabs -- logs clean -d 3
# Migrate from other AI agent tools
cargo run --bin opencrabs -- migrate openclaw
cargo run --bin opencrabs -- migrate hermes
cargo run --bin opencrabs -- migrate openclaw --dry-run
```
> **Tip:** After `cargo build --release`, run the binary directly: `./target/release/opencrabs`
### Make It Available System-Wide
After downloading or building, add the binary to your PATH so you can run `opencrabs` from any project directory:
```bash
# Symlink (recommended β always points to latest build)
sudo ln -sf $(pwd)/target/release/opencrabs /usr/local/bin/opencrabs
# Or copy
sudo cp target/release/opencrabs /usr/local/bin/
```
Then from any project:
```bash
cd /your/project
opencrabs
```
Use `/cd` inside OpenCrabs to switch working directory at runtime without restarting.
**Output formats** for non-interactive mode: `text` (default), `json`, `markdown`
---
## π§ Onboarding Wizard
First-time users are guided through a 9-step setup wizard that appears automatically after the splash screen.
### How It Triggers
- **Automatic:** When no `~/.opencrabs/config.toml` exists and no API keys are set in `keys.toml`
- **CLI:** `cargo run --bin opencrabs -- onboard` (or `opencrabs onboard` after install)
- **Chat flag:** `cargo run --bin opencrabs -- chat --onboard` to force the wizard before chat
- **Slash command:** Type `/onboard` in the chat to re-run it anytime
### The 9 Steps
| Step | Title | What It Does |
|------|-------|-------------|
| 1 | **Mode Selection** | QuickStart (sensible defaults) vs Advanced (full control) |
| 2 | **Model & Auth** | Pick provider (Anthropic, OpenAI, GitHub Copilot, Gemini, OpenRouter, Minimax, z.ai GLM, Custom) β enter token/key or sign in via OAuth β model list fetched live from API β select model. Auto-detects existing keys from `keys.toml` |
| 3 | **Workspace** | Set brain workspace path (default `~/.opencrabs/`) β seed template files (SOUL.md, etc.) |
| 4 | **Gateway** | Configure HTTP API gateway: port, bind address, auth mode |
| 5 | **Channels** | Toggle messaging integrations (Telegram, Discord, WhatsApp, Slack, Trello) |
| 6 | **Voice** | Choose STT provider: **Off** / **Groq Whisper (API)** / **OpenAI-compatible (API)** / **Voicebox (self-hosted)** / **Local whisper.cpp**. Choose TTS provider: **Off** / **OpenAI TTS (API)** / **OpenAI-compatible (API)** / **Voicebox (self-hosted)** / **Local Piper**. Local modes show model/voice picker with download progress. OpenAI-compatible modes take base URL + model + key. Voicebox modes take base URL + profile_id (TTS) / base URL (STT) |
| 7 | **Image Handling** | Enable Gemini image generation and/or vision analysis β uses a separate Google AI key |
| 8 | **Daemon** | Install background service (systemd on Linux, LaunchAgent on macOS) |
| 9 | **Health Check** | Verify API key, config, workspace β shows pass/fail summary |
| 10 | **Brain Personalization** | Tell the agent about yourself and how you want it to behave β AI generates personalized brain files (SOUL.md, USER.md, etc.) |
**QuickStart mode** skips steps 4-8 with sensible defaults. **Advanced mode** lets you configure everything.
Type `/onboard:voice` or `/onboard:image` in chat to jump directly to Voice or Image setup anytime.
#### Local STT (whisper.cpp)
Run speech-to-text on-device with zero API cost. Included by default in prebuilt binaries and `cargo install opencrabs`.
In `/onboard:voice`, select **Local** mode, pick a model size, and press Enter to download. Models are stored at `~/.local/share/opencrabs/models/whisper/`.
> **Building from source:** Local STT requires CMake and a C++ compiler (for whisper.cpp). To exclude it: `cargo install opencrabs --no-default-features --features telegram,whatsapp,discord,slack,trello`
| Model | Size | Quality |
|-------|------|---------|
| Tiny | ~75 MB | Fast, lower accuracy |
| Base | ~142 MB | Good balance |
| Small | ~466 MB | High accuracy |
| Medium | ~1.5 GB | Best accuracy |
Config (`config.toml`):
```toml
[voice]
# Core toggles
stt_enabled = true
tts_enabled = true
# --- Local mode (whisper.cpp + Piper) ---
stt_mode = "local" # "api" (default) or "local"
local_stt_model = "local-base" # local-tiny, local-base, local-small, local-medium
tts_mode = "local" # "api" (default) or "local"
local_tts_voice = "ryan" # ryan, amy, lessac, kristin, joe, cori
# --- OpenAI-compatible API mode (any Whisper/Coqui-compatible endpoint) ---
# STT
stt_base_url = "https://your-stt-host/v1/audio/transcriptions"
stt_model = "whisper-large-v3"
# TTS
tts_base_url = "https://your-tts-host/v1/audio/speech"
tts_model = "tts-1"
tts_voice = "alloy"
# --- Voicebox (self-hosted open-source voice stack) ---
voicebox_stt_enabled = false
voicebox_stt_base_url = "http://localhost:8000"
voicebox_tts_enabled = false
voicebox_tts_base_url = "http://localhost:8000"
voicebox_tts_profile_id = "your-voice-profile-uuid"
```
**Provider priority** (first match wins, enforced in `voice::transcribe` and `voice::synthesize`):
- **STT:** Voicebox β OpenAI-compatible β Groq API β Local whisper.cpp
- **TTS:** Voicebox β OpenAI-compatible β OpenAI TTS API β Local Piper
**Fallback chain** (auto-failover when the primary fails β set under `[providers.stt]` / `[providers.tts]` in `keys.toml`):
```toml
[providers.stt]
fallback_chain = ["groq", "openai_compatible", "local"]
[providers.tts]
fallback_chain = ["openai_compatible", "openai", "local"]
```
When the primary returns a 5xx, fails a liveness probe (voicebox), or is otherwise unreachable, the dispatcher walks `fallback_chain` in order and tries each entry that has the credentials/config it needs. Empty/omitted chain β default priority order with the primary removed. STT labels: `voicebox`, `openai_compatible`, `groq`, `local`. TTS labels: `voicebox`, `openai_compatible`, `openai`, `local` (note `groq` is STT-only; TTS rejects it). Aliases: `openai-compatible`, `whisper`/`local_whisper`, `piper`/`local_piper`.
All TTS outputs are normalised to OGG/Opus via `ensure_opus` before reaching a channel β channels can send the bytes directly to `send_voice` (Telegram), `audio_message` with `ptt=true` (WhatsApp), file attachment (Discord), or `files.upload` (Slack) without format checks of their own.
#### Brain Personalization (Step 10)
Two input fields: **About You** (who you are) and **Your OpenCrabs** (how the agent should behave). The LLM uses these plus the 6 workspace template files to generate personalized brain files.
- **First run:** Empty fields, static templates as reference β LLM generates β writes to workspace
- **Re-run:** Fields pre-populated with truncated preview of existing `USER.md` / `IDENTITY.md` β edit to regenerate or `Esc` to skip
- **Regeneration:** LLM receives the **current workspace files** (not static templates), so any manual edits you made are preserved as context
- **Overwrite:** Only files with new AI-generated content are overwritten; untouched files keep their current state
- No extra persistence files β the brain files themselves are the source of truth
### Wizard Navigation
| Key | Action |
|-----|--------|
| `Tab` / `Shift+Tab` | Navigate between fields |
| `Up` / `Down` | Scroll through lists |
| `Enter` | Confirm / next step |
| `Space` | Toggle checkboxes |
| `Esc` | Go back one step |
---
## π API Keys (keys.toml)
OpenCrabs uses `~/.opencrabs/keys.toml` as the **single source** for all API keys, bot tokens, and search keys. No `.env` files, no OS keyring, no environment variables for secrets. Keys are loaded at runtime and can be modified by the agent.
```toml
# ~/.opencrabs/keys.toml β chmod 600!
# LLM Providers
[providers.anthropic]
api_key = "sk-ant-api03-YOUR_KEY"
[providers.openai]
api_key = "sk-YOUR_KEY"
[providers.github]
api_key = "gho_..." # OAuth token (auto-saved by onboarding wizard)
[providers.openrouter]
api_key = "sk-or-YOUR_KEY"
[providers.minimax]
api_key = "your-minimax-key"
[providers.zhipu]
api_key = "your-zhipu-key" # Get from open.bigmodel.cn
[providers.gemini]
api_key = "AIza..." # Get from aistudio.google.com
[providers.custom.your_name]
api_key = "your-key" # not required for local LLMs
# Image Generation & Vision (independent of main chat provider)
[image]
api_key = "AIza..." # Same Google AI key as providers.gemini (can reuse)
# Messaging Channels β tokens/secrets only (config.toml holds allowed_users, allowed_channels, etc.)
[channels.telegram]
token = "123456789:ABCdef..."
[channels.discord]
token = "your-discord-bot-token"
[channels.slack]
token = "xoxb-your-bot-token"
app_token = "xapp-your-app-token" # Required for Socket Mode
[channels.trello]
app_token = "your-trello-api-key" # API Key from trello.com/power-ups/admin
token = "your-trello-api-token" # Token from the authorization URL
# Web Search
[providers.web_search.exa]
api_key = "your-exa-key"
[providers.web_search.brave]
api_key = "your-brave-key"
# Voice (STT/TTS) β dispatched in priority order: Voicebox β OpenAI-compatible β Groq β Local
# STT Groq API (legacy default): uses Groq Whisper
[providers.stt.groq]
api_key = "your-groq-key"
# STT OpenAI-compatible endpoint (any Whisper-compatible server: self-hosted, Deepgram proxy, etc.)
# Also set stt_base_url = "https://β¦/v1/audio/transcriptions" and stt_model in [voice] of config.toml
[providers.stt.openai_compatible]
api_key = "your-stt-key"
# STT Voicebox (self-hosted open-source voice stack) β no API key required
# Set voicebox_stt_enabled = true and voicebox_stt_base_url in [voice] of config.toml
# STT Local mode: no API key needed β runs whisper.cpp on device
# Set stt_mode = "local" and local_stt_model in config.toml
# STT fallback chain β when the active provider fails (5xx, liveness probe
# error, unreachable), the dispatcher walks this list in order and tries
# each entry that has the credentials/config it needs. Same shape as the
# completion-side fallback_providers chain. Labels (case-insensitive):
# "voicebox", "openai_compatible" (alias: "openai-compatible", "openai"),
# "groq", "local" (alias: "whisper", "local_whisper"). Empty/omitted means
# "use the default priority order with the primary removed".
[providers.stt]
fallback_chain = ["groq", "openai_compatible", "local"]
# TTS OpenAI API (legacy default): uses OpenAI TTS
[providers.tts.openai]
api_key = "your-openai-key"
# TTS OpenAI-compatible endpoint (self-hosted Coqui / Bark / ElevenLabs-compatible proxy)
# Also set tts_base_url = "https://β¦/v1/audio/speech" + tts_model + tts_voice in [voice]
[providers.tts.openai_compatible]
api_key = "your-tts-key"
# TTS Voicebox (self-hosted) β no API key required
# Set voicebox_tts_enabled = true, voicebox_tts_base_url, voicebox_tts_profile_id in [voice]
# TTS Local mode: no API key needed β runs Piper TTS on device
# Set tts_mode = "local" and local_tts_voice in config.toml
# TTS fallback chain β mirror of STT. Labels: "voicebox",
# "openai_compatible" (alias: "openai-compatible"), "openai",
# "local" (alias: "piper", "local_piper"). Note "groq" is STT-only β the
# TTS chain rejects it. Empty/omitted means "use the default priority".
[providers.tts]
fallback_chain = ["openai_compatible", "openai", "local"]
```
> **Note:** Anthropic OAuth tokens (`sk-ant-oat`) are no longer supported for third-party apps as of Feb 2026 ([anthropics/claude-code#28091](https://github.com/anthropics/claude-code/issues/28091)). Use console API keys (`sk-ant-api03-*`) instead.
> **Trello note:** `app_token` holds the Trello **API Key** and `token` holds the Trello **API Token** β `app_token` is the app-level credential and `token` is the user-level credential. Board IDs are configured via `board_ids` in `config.toml`.
> **Security:** Always `chmod 600 ~/.opencrabs/keys.toml` and add `keys.toml` to `.gitignore`.
---
## π Secret Sanitization & Redaction
OpenCrabs automatically redacts API keys and tokens from all outputs β conversation history, TUI display, tool approval dialogs, and external channel delivery (Telegram, Discord, Slack, WhatsApp). Secrets never persist to the database or appear in logs.
### How It Works
**Three layers of defense:**
1. **Prefix-based detection** β Keys with recognized prefixes are caught instantly (`sk-proj-...` β `sk-proj-[REDACTED]`)
2. **Hex token detection** β Contiguous hex strings of 32+ chars are redacted
3. **Mixed alphanumeric detection** β Opaque tokens of 28+ chars containing both letters and digits are caught as a safety net
**Structural redaction** also applies to:
- JSON fields named `authorization`, `api_key`, `token`, `secret`, `password`, etc.
- Inline patterns in bash commands (`bearer ...`, `x-api-key: ...`, `api_key=...`)
- URL query params and key=value assignments in any casing (`?api_key=...`, `&token=...`)
- URL passwords (`https://user:PASSWORD@host` β `https://user:[REDACTED]@host`)
- The one-line tool-call summary and RSI self-improvement notifications, not just the expanded views
### Supported Key Prefixes
OpenCrabs recognizes **60+ industry-standard key formats** out of the box:
| Category | Prefixes |
|----------|----------|
| **AI / LLM** | `sk-proj-` (OpenAI), `sk-ant-` (Anthropic), `sk-or-v1-` (OpenRouter), `sk-` (generic), `gsk_` (Groq), `nvapi-` (NVIDIA), `AIzaSy` (Google), `pplx-` (Perplexity), `hf_` (HuggingFace), `r8_` (Replicate) |
| **Cloud** | `AKIA` / `ASIA` (AWS), `DefaultEndpointsProtocol=` (Azure), `ya29.` (Google OAuth) |
| **Payments** | `sk_live_` / `sk_test_` / `pk_live_` / `pk_test_` / `rk_live_` / `rk_test_` (Stripe), `sq0atp-` / `sq0csp-` (Square) |
| **Git / DevOps** | `ghp_` / `gho_` / `ghu_` / `ghs_` / `github_pat_` (GitHub), `glpat-` / `gloas-` (GitLab), `npm_` (npm), `pypi-AgEIcHlwaS` (PyPI) |
| **Communication** | `xoxb-` / `xoxp-` / `xapp-` / `xoxs-` (Slack), `SG.` (SendGrid), `xkeysib-` (Brevo) |
| **SaaS** | `shpat_` / `shpca_` / `shppa_` / `shpss_` (Shopify), `ntn_` (Notion), `lin_api_` (Linear), `aio_` (Airtable), `phc_` (PostHog) |
| **Infrastructure** | `sntrys_` (Sentry), `dop_v1_` (DigitalOcean), `tskey-` (Tailscale), `tvly-` (Tavily), `hvs.` / `vault:v1:` (HashiCorp Vault) |
| **Auth / Crypto** | `eyJ` (JWT), `whsec_` (webhooks), `EAA` (Facebook/Meta), `ATTA` (Trello), `AGE-SECRET-KEY-` (age encryption) |
### Best Practice: Use Prefixed Keys
If you build or configure services that integrate with OpenCrabs, **use industry-standard key prefixes**. Keys with recognized prefixes are caught by the first and most reliable layer of redaction. Opaque tokens (random alphanumeric strings with no prefix) rely on the generic safety-net regex, which has a higher threshold to avoid false positives.
**Good:** `av_38947394723jkhkrjkhdfiuo83489732` β prefix makes it instantly recognizable
**Risky:** `38947394723jkhkrjkhdfiuo83489732` β still caught by mixed-alnum regex, but no prefix context in `[REDACTED]` output
### Memory Safety
API keys stored via `SecretString` are:
- **Zeroized on drop** β memory is overwritten when the value goes out of scope (not left for GC)
- **Never serialized** β `Debug`, `Display`, and `Serialize` all output `[REDACTED]`
- **Never logged** β `expose_secret()` is the only way to access the raw value
---
## π Using Local LLMs
OpenCrabs works with any OpenAI-compatible local inference server for **100% private, zero-cost** operation.
### LM Studio (Recommended)
1. Download and install [LM Studio](https://lmstudio.ai/)
2. Download a model (e.g., `qwen2.5-coder-7b-instruct`, `Mistral-7B-Instruct`, `Llama-3-8B`)
3. Start the local server (default port 1234)
4. Add to `config.toml` β no API key needed:
```toml
[providers.custom.lm_studio]
enabled = true
base_url = "http://localhost:1234/v1"
default_model = "qwen2.5-coder-7b-instruct" # Must EXACTLY match LM Studio model name
models = ["qwen2.5-coder-7b-instruct", "llama-3-8B", "mistral-7B-instruct"]
```
> **Critical:** The `default_model` value must exactly match the model name shown in LM Studio's Local Server tab (case-sensitive).
### Ollama
```bash
ollama pull mistral
```
Add to `config.toml` β no API key needed:
```toml
[providers.custom.ollama]
enabled = true
base_url = "http://localhost:11434/v1"
default_model = "mistral"
models = ["mistral", "llama3", "codellama"]
```
### Multiple Local Providers
Want both LM Studio and Ollama configured? Use named providers and switch via `/models`:
```toml
[providers.custom.lm_studio]
enabled = true
base_url = "http://localhost:1234/v1"
default_model = "qwen2.5-coder-7b-instruct"
models = ["qwen2.5-coder-7b-instruct", "llama-3-8B", "mistral-7B-instruct"]
[providers.custom.ollama]
enabled = false
base_url = "http://localhost:11434/v1"
default_model = "mistral"
models = ["mistral", "llama3", "codellama"]
```
The name after `custom.` is just a label you choose. The first one with `enabled = true` is used. Switch anytime via `/models` or `/onboard`.
### Recommended Models
| Model | RAM | Best For |
|-------|-----|----------|
| Qwen-2.5-7B-Instruct | 16 GB | Coding tasks |
| Mistral-7B-Instruct | 16 GB | General purpose, fast |
| Llama-3-8B-Instruct | 16 GB | Balanced performance |
| DeepSeek-Coder-6.7B | 16 GB | Code-focused |
| TinyLlama-1.1B | 4 GB | Quick responses, lightweight |
**Tips:**
- Start with Q4_K_M quantization for best speed/quality balance
- Set context length to 8192+ in LM Studio settings
- Use `Ctrl+N` to start a new session if you hit context limits
- GPU acceleration significantly improves inference speed
### Cloud vs Local Comparison
| Aspect | Cloud (Anthropic) | Local (LM Studio) |
|--------|-------------------|-------------------|
| Privacy | Data sent to API | 100% private |
| Cost | Per-token pricing | Free after download |
| Speed | 1-2s (network) | 2-10s (hardware-dependent) |
| Quality | Excellent (Claude 4.x) | Good (model-dependent) |
| Offline | Requires internet | Works offline |
See [LM_STUDIO_GUIDE.md](src/docs/guides/LM_STUDIO_GUIDE.md) for detailed setup and troubleshooting.
---
## π Configuration
### Configuration Files
OpenCrabs uses three config files β all **hot-reloaded at runtime** (no restart needed):
| File | Purpose | Secret? |
|------|---------|---------|
| `~/.opencrabs/config.toml` | Provider settings, models, channels, allowed users | No β safe to commit |
| `~/.opencrabs/keys.toml` | API keys, bot tokens | **Yes** β `chmod 600`, never commit |
| `~/.opencrabs/commands.toml` | User-defined slash commands | No |
| `~/.opencrabs/tools.toml` | Runtime-defined agent tools (HTTP, shell) | No |
Changes to any of these files are picked up automatically within ~300ms while OpenCrabs is running. The active LLM provider, channel allowlists, approval policy, and slash command autocomplete all update without restart.
### Profiles (Multi-Instance)
Run multiple isolated OpenCrabs instances from the same installation. Each profile gets its own config, memory, sessions, skills, and gateway service.
```bash
# Create a new profile
opencrabs profile create hermes
# Run as a specific profile
opencrabs -p hermes
# Or set via environment variable
OPENCRABS_PROFILE=hermes opencrabs daemon
# List all profiles
opencrabs profile list
# Migrate config and brain files from one profile to another
# Copies: *.md, *.toml (config, keys, tools, commands), memory/ directory
# Skips: database, sessions, logs, locks
opencrabs profile migrate --from default --to hermes
opencrabs profile migrate --from default --to hermes --force # overwrite existing files
# Export/import for sharing or backup
opencrabs profile export hermes -o hermes-backup.tar.gz
opencrabs profile import hermes-backup.tar.gz
# Delete a profile
opencrabs profile delete hermes
```
**Directory layout:**
```
~/.opencrabs/ # "default" profile (backward compatible)
βββ config.toml
βββ keys.toml
βββ opencrabs.db
βββ memory/
βββ profiles.toml # profile registry
βββ locks/ # token-lock files
βββ profiles/
βββ hermes/ # named profile β fully isolated
βββ config.toml
βββ keys.toml
βββ opencrabs.db
βββ memory/
βββ *.md
```
**Token-lock isolation:** Two profiles cannot use the same bot token (Telegram, Discord, Slack, Trello). On startup, OpenCrabs acquires PID-based locks for each channel credential. If another profile already holds that token, the channel refuses to start and notifies the user. Stale locks from crashed processes are automatically cleaned up.
**Migration workflow:** Use `profile migrate` to clone your configuration into a new profile, then customize the brain files (SOUL.md) to give the new instance its own personality. The original profile's database and sessions stay untouched.
Search order for `config.toml`:
1. `~/.opencrabs/config.toml` (primary)
2. `~/.config/opencrabs/config.toml` (legacy fallback)
3. `./opencrabs.toml` (current directory override)
---
## π οΈ Configuration (config.toml)
Full annotated example β the onboarding wizard writes this for you, but you can edit it directly:
```toml
# ~/.opencrabs/config.toml
[agent]
approval_policy = "auto-always" # auto-always (default) | auto-session | ask
working_directory = "~/projects" # default working dir for Bash/file tools
redact_sensitive_data = true # redact IPs, tokens, passwords from tool output (set false for sysadmin work)
# ββ Channels ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[channels.telegram]
enabled = true
allowed_users = ["123456789"] # Telegram user IDs (get yours via /start)
respond_to = "all" # all | mention | dm_only
[channels.discord]
enabled = true
allowed_users = ["637291214508654633"] # Discord user IDs
allowed_channels = ["1473207147025137778"]
respond_to = "mention" # all | mention | dm_only
[channels.slack]
enabled = true
allowed_users = ["U066SGWQZFG"] # Slack user IDs
allowed_channels = ["C0AEY3C2P9V"]
respond_to = "mention" # all | mention | dm_only
[channels.whatsapp]
enabled = true
allowed_phones = ["+1234567890"] # E.164 format
[channels.trello]
enabled = true
board_ids = ["your-board-id"] # From the board URL
# ββ Providers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[providers.anthropic]
enabled = true
default_model = "claude-sonnet-4-6"
[providers.gemini]
enabled = false
[providers.openai]
enabled = false
default_model = "gpt-5-nano"
[providers.zhipu]
enabled = false
default_model = "glm-4.7"
endpoint_type = "coding" # "api" (General) or "coding" (Coding API)
# ββ Image βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[image.generation]
enabled = true
model = "gemini-3.1-flash-image-preview"
[image.vision]
enabled = true
model = "gemini-3.1-flash-image-preview"
# ββ Cron Defaults ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[cron]
default_provider = "minimax" # Provider for cron jobs that don't specify one
default_model = "MiniMax-M2.7" # Model for cron jobs that don't specify one
```
> API keys go in `keys.toml`, not here. See [API Keys (keys.toml)](#-api-keys-keystoml).
---
## π Commands (commands.toml)
User-defined slash commands β the agent writes these autonomously via the `config_manager` tool, or you can edit directly:
```toml
# ~/.opencrabs/commands.toml
[[commands]]
name = "/deploy"
description = "Deploy to staging server"
action = "prompt"
prompt = "Run ./deploy.sh staging and report the result."
[[commands]]
name = "/standup"
description = "Generate a daily standup summary"
action = "prompt"
prompt = "Summarize my recent git commits and open tasks for a standup. Be concise."
[[commands]]
name = "/rebuild"
description = "Build and restart OpenCrabs from source"
action = "prompt"
prompt = 'Run `RUSTFLAGS="-C target-cpu=native" cargo build --release` in /srv/rs/opencrabs. If it succeeds, ask if I want to restart now.'
```
Commands appear instantly in autocomplete (type `/`) after saving β no restart needed. The `action` field supports:
- `"prompt"` β sends the prompt text to the agent for execution
- `"system"` β displays the text inline as a system message
---
## π Dynamic Tools (tools.toml)
Runtime-defined tools that the agent can call autonomously β no rebuild, no restart. Unlike slash commands (`commands.toml`) which are user-triggered shortcuts, dynamic tools appear in the LLM's tool list and the agent decides when to use them.
```toml
# ~/.opencrabs/tools.toml
[[tools]]
name = "health_check"
description = "Check if a service is healthy"
executor = "http"
method = "GET"
url = "https://{{host}}/health"
headers = { "Authorization" = "Bearer {{token}}" }
timeout_secs = 10
requires_approval = false
[[tools.params]]
name = "host"
type = "string"
description = "Hostname to check"
required = true
[[tools.params]]
name = "token"
type = "string"
description = "API bearer token"
required = true
[[tools]]
name = "deploy_staging"
description = "Deploy the current branch to staging"
executor = "shell"
command = "cd ~/project && ./deploy.sh {{branch}} staging"
timeout_secs = 120
requires_approval = true
[[tools.params]]
name = "branch"
type = "string"
description = "Git branch to deploy"
required = true
```
**Executor types:**
| Executor | Fields | Description |
|----------|--------|-------------|
| `http` | `method`, `url`, `headers` | Makes an HTTP request. Template variables (`{{param}}`) are substituted in the URL, headers, and body |
| `shell` | `command` | Runs a shell command. Template variables substituted in the command string |
**Fields:**
| Field | Required | Default | Description |
|-------|----------|---------|-------------|
| `name` | Yes | | Tool name (used by the agent to call it) |
| `description` | Yes | | What the tool does (shown to the LLM) |
| `executor` | Yes | | `http` or `shell` |
| `enabled` | No | `true` | Whether the tool is active |
| `requires_approval` | No | `true` | Whether the user must approve each call |
| `timeout_secs` | No | `30` | Execution timeout in seconds |
| `params` | No | `[]` | Parameter definitions with name, type, description, required |
**Per-parameter value coercion** β each param in `params` supports optional `coerce_empty_to` and `coerce_null_to` fields. When a parameter arrives as `""` or `null`, the configured substitute value is used instead. Useful for optional flags and safe defaults:
```toml
[[tools.params]]
name = "verbose"
type = "string"
description = "Ena