crab speaks one wire protocol: the OpenAI chat-completions API. A provider is a profile with a base URL, a model id, an optional key, and a context window. That is the whole abstraction.
The config file
~/.crab/config.toml. A missing file is fine — it means "environment variables only".
default_provider names the profile used when you do not pass --provider.
default_provider = "deepseek"
[providers.deepseek]
base_url = "https://api.deepseek.com/v1"
model = "deepseek-chat"
api_key = "sk-..." # optional if CRAB_API_KEY / OPENAI_API_KEY is set
context_window = 128000
[providers.local]
base_url = "http://localhost:8080/v1"
model = "qwen"
auto_compact_threshold = 0.8 # auto-compact above 80% of the window; 0 disables
Provider fields
| Key | Type | Notes |
|---|---|---|
base_url | string | OpenAI-compatible base URL. Required. |
model | string | Model name sent to the API. Required. |
api_key | string? | Falls back to CRAB_API_KEY / OPENAI_API_KEY. Local endpoints may leave it empty; anything else without a key is an error. |
context_window | integer? | Shown in the status line; enables the out-of-window warning and drives compaction. |
Resolution order
The --provider <name> flag wins, then default_provider, then
openai. Within the chosen profile, environment variables beat the file, field by
field:
| Environment | Overrides |
|---|---|
CRAB_API_KEY / OPENAI_API_KEY | api_key |
CRAB_BASE_URL / OPENAI_BASE_URL | base_url |
CRAB_MODEL | model |
CRAB_CONTEXT_WINDOW | context_window |
So export CRAB_API_KEY=… with no config file at all is a working setup: crab uses
https://api.openai.com/v1 and gpt-4o-mini.
Switching
crab --provider deepseek # for one run
crab --provider mock # canned responses, for tests and demos
Inside a session, two slash commands do the same thing without losing history:
- /model <name>
- Switch to a named profile. The next turn uses it; the transcript is unchanged.
- /models
- Pick from the profiles in your config with ↑↓ / Enter.
Because the transcript is provider-agnostic, switching mid-conversation is normal — start on a cheap model, move to a stronger one when the task gets hard. The ledger records which model each turn used, so the cost report stays honest.
Choosing a context window
context_window is crab's belief about how many tokens the model accepts. It is used for
the occupancy display, the "running out of window" warning, and the auto-compaction threshold — not for
validation. When in doubt, understate it: an early compaction costs you a summary; a late one costs you
a failed turn.
Advertised context is not usable context. Many models reserve room for output. If your provider documents a maximum output length, subtracting it from the advertised window is a better number than the advertised window itself.
Local models
crab does not run inference. Point base_url at a server that speaks the same API:
[providers.local]
base_url = "http://127.0.0.1:8080/v1"
model = "qwen3-coder-30b"
context_window = 131072
That works with llama.cpp's server, vLLM, and Ollama's OpenAI-compatible endpoint. Local endpoints are allowed an empty key. Two things to check when a local run misbehaves: the model must support tool calling, and its chat template must survive the tool-result round trip. If it ignores tool definitions you get a chatty agent that never edits anything — a model problem, not a harness one.
Prompt caching
crab does not manage the cache; it reports it. Providers that cache prefix tokens — DeepSeek and Kimi
among them — return cache_read and cache_write counts, and crab records them
verbatim in a four-way split. Two practical consequences:
- A stable prefix is cheaper than a clever one. Prompt text that changes every turn defeats the cache.
- The first turn of a session writes cache; later turns read it. A short session can be dominated by cache writes.
See the pricing model for how those counts become money, and
~/.crab/pricing.toml for how to override the rates.
MCP servers
External tools come over the Model Context
Protocol. Add servers under [mcp.<name>] in the same file:
[mcp.brave]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-brave-search"]
enabled = true
env = { BRAVE_API_KEY = "..." }
| Field | Type | Notes |
|---|---|---|
command | string | Program to spawn. Required. |
args | string[] | Arguments. Default empty. |
enabled | bool? | Omitted means enabled. false skips the server entirely. |
env | table | Extra variables for the child, layered on top of the inherited environment — so PATH still works. |
Putting a credential in env rather than in args also keeps it out of
ps output and out of the permission prompt.
Tool naming
Every tool a server exposes is registered as <server>__<tool> — two
double underscores. Two servers may both expose search, and the model needs to tell them
apart:
[mcp.brave] → brave__brave_web_search
[mcp.docs] → docs__lookup
# two servers, same tool name, distinguishable
[mcp.alpha] → alpha__search
[mcp.beta] → beta__search
Several tools from one server share a single child process.
Startup is best-effort
A command that fails to spawn, or whose tools/list fails, prints a warning on stderr and
is skipped. One broken MCP server never prevents crab from starting.
Node servers behind a proxy
npx-style servers inherit your shell environment, including HTTP_PROXY /
HTTPS_PROXY. But Node's fetch (undici) ignores those variables by
default, so a Node server behind a corporate proxy may fail to reach the network. The fix belongs in
the server's env:
[mcp.brave]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-brave-search"]
env = { BRAVE_API_KEY = "...", NODE_OPTIONS = "--use-env-proxy" }
--use-env-proxy (equivalently NODE_USE_ENV_PROXY=1) makes Node's built-in
http/https and fetch honour HTTP_PROXY /
HTTPS_PROXY / NO_PROXY. It is opt-in and needs a recent Node (the
fetch path is 22.21.0 / 24.0.0+).
Skills
crab discovers Agent Skills —
directories containing a SKILL.md with name / description
frontmatter — in two places:
- Project
.agents/skills/in the current directory or any parent, up to and including the git repo root.- User
~/.agents/skills/.
Discovered skills appear in the system prompt by name, description and path. The body stays on disk,
and the model is told to read the SKILL.md with the read tool when a task
matches — progressive disclosure without paying for every skill on every turn. On a name collision the
first discovery wins: project beats user, nearer beats farther.
/skill:<name> [args] is the explicit path: it injects the whole
SKILL.md body as a user message with the args appended. Skills may bundle extra files
(references/, scripts/, …) relative to their directory.