Design record
Decisions, with the arguments that lost.
crab keeps a sealed decision record: twenty-one entries, each with the reasoning attached, written so that the reasoning — not the decision — is what you have to refute before reopening it. Below is the substance of the ones that shaped the code you can see.
The original document is in the repository at DESIGN.md, in Chinese, and is
explicitly not a to-do list. Its closing section names the three things most likely to kill the
project: that agent quality lives in the prompt rather than the code, that self-hosting has a
chicken-and-egg problem, and that the deferred-feature list will keep knocking.
A spiritual successor to pi, not a compatible port
crab borrows pi's concept model — an agent loop, a session log, tool descriptions that live next to the tool — but has its own format and its own extension mechanism. There is no .pi/ compatibility layer.
Acceptance is a commit, not a benchmark
The criterion: a commit whose diff was produced entirely by crab. Reached at f3fec8e. Star counts, benchmarks and 'learn Rust' were explicitly rejected as measures.
Named crab, never published to crates.io yet
crab on crates.io is a CIL scraping tool that stopped shipping in 2020. The workspace crate is crab-agent, which was verified free. Publishing waits until the acceptance criterion is met and held.
crab.Two crates, cut once
crab-core holds the loop, session, provider, tools and prompt assembly with a hard rule: no terminal dependency, no println!. crab-cli holds clap, crossterm, reedline, rendering and the confirm UI.
The loop owns cancellation, not the channel layout
The agent runs on its own tokio task: mpsc for commands, broadcast for events, a cancellation token for abort. v0 implements submit, abort and one FIFO queue; steering vs follow-up semantics were deferred.
--continue replays an unpaired tool_use and most providers answer 400. Cancel-safety is the real problem; the channel structure is not.One provider first; abstract on the second
A single OpenAI-compatible client got the loop working. The Provider trait was extracted only when a second provider arrived. Authentication is environment variables plus ~/.crab/auth.json; OAuth subscription login is explicitly out.
The SSE parser is a pure state machine
parse_chunk touches no IO. A tolerant accumulator keys slots by array index, degrading to 'append to the last slot' when the index is missing. Arguments are parsed only once complete; a parse failure feeds the raw string back to the model.
Append-only JSONL sessions, no SQLite
Entries carry id and parent_id. v0 implements linear append plus --continue; branch, fork and clone are future work, but the fields exist now.
tail -f, git diff and hand-editing — which matter enormously when you are debugging an agent. Adding parent_id now is nearly free; adding it later means migrating every historical file.Six tools, then seven — each described beside its implementation
v0 shipped read, write, edit, bash, glob, grep; web_fetch arrived later. Every tool's description text lives in the same file as its Tool implementation.
edit is literal, unique, all-or-nothing, and gated on freshness
Exact match, no whitespace tolerance. Zero or more than one match is an error. Multiple edits match against the original text. Failure applies nothing. edit and write require a prior read this session and an unchanged mtime. Empty old is rejected.
The permission gate is a seatbelt, and says so
The gate sits at the tool-call layer, not in a shell parser. Reads pass; writes, network and non-read-only bash ask; --yolo allows everything; a allows a class for the session. The README states outright that this is not a security boundary.
bash -c, eval, $(...), writing a script, or find -exec. A gate that looks like security but isn't is worse than one honestly labelled a seatbelt against typos and confused models.Extension is MCP; in-process means fork and recompile
v0 exposes Rust traits (Tool, EventHandler, Renderer) in-process. The only external mechanism is MCP.
Streaming output and a line editor, not an alt-screen app
v0 uses crossterm streaming output with a reedline input line, with an upgrade path to ratatui Viewport::Inline + insert_before. A full-screen alt-screen app is excluded.
| tee-able.AGENTS.md and TOML
Context files are AGENTS.md; configuration lives in .crab/ as TOML. User customisation may only append to the system prompt, never replace it.
CRAB.md means a second file that gets ignored — a negative-sum game. JSON cannot hold comments, and comments are exactly what a config file needs. Full prompt replacement would let a user silently remove the tool protocol and then file the resulting bug against crab.CI never calls a real model
MockProvider for the loop, recorded SSE fixtures for the parser, wiremock for transport (429 backoff, mid-stream disconnect, missing [DONE]), assert_cmd for the CLI contract, insta for snapshots. A manual live smoke test stays outside CI.
--provider mock keeps a permanent entry point because it remains the fastest way to debug the loop.Measure size in CI, measure time locally
Binary size is a hard CI assert (frozen at 16 MiB). Start-up time and idle RSS live in a local make perf script.
Unwind stays enabled
The release profile sets lto = "thin", codegen-units = 1 and strip = true — but not panic = "abort".
panic = abort conflicts with the cancellation design: tokio loses unwind capture, so a slice-overflow panic inside edit — which should become a JoinError::Panic fed back to the model as a tool failure — instead aborts the process and loses the session. The 5–10% size saving is not worth that.No local tokeniser
Only the provider's reported usage is authoritative. A chars/4 approximation is used for one thing only: the pre-request 'about to exceed the window' warning.
tiktoken-rs ships BPE tables into the binary, and is simply wrong for DeepSeek, Kimi and Claude. You would pay the size cost and still only get an estimate.Ship binaries when a second person wants one
v0 was cargo install --git only. Prebuilt tarballs followed, and then a managed installer in the style of pi: versioned releases side-by-side under ~/.crab/install/releases/, a launcher that resolves the current one, and crab update verifying downloads against the release's SHA256SUMS.
The v0 fence, and the discipline of keeping it
Kept out of v0: slash commands, prompt templates, themes, compaction, session branching, image input, @ file references, MCP, sub-agents, OAuth, exact local token counts, OS sandboxing, and any JS/WASM plugin runtime. Most have since landed; others are still out.
Consequences
What this buys, and what it costs.
A record like this is not free. It constrains future work, and it makes the project slower to add features that other agents shipped months earlier.
In exchange you get a tool whose behaviour you can predict from a document, and a short list of the reasons each surprising-looking choice was made.
Three tensions the record admits
- Prompt quality dominates code quality. If crab feels dumber than pi, no performance number will save it, and it will look like a bug in the loop. Mitigation: a fixed set of real tasks re-run by eye after every prompt change.
- Self-hosting is a chicken-and-egg problem. crab cannot write crab until
editis good enough. Mitigation: a graded threshold — the first crab commit was allowed to be "a typo fix plus a test". - The deferred list keeps knocking. Mitigation, and the only one that worked: add nothing outside the fence until you can do your own work with the tool.
Read further
The user-facing manual is separate.
The design record explains why; the README and the docs explain how. The dogfood log explains what actually happened.