Open-source LLM CLI · one Go binary
Agents as Unix commands.
Swap agents like aliases, chain them like pipes.
A clai run reads stdin, answers once, and exits. Keep each agent as a profile, call it from an alias, drop it into a script, or wire many into a swarm. OpenAI, Anthropic, Google, Mistral, DeepSeek, xAI, Ollama and OpenRouter behind the same flags.
go install github.com/baalimago/clai@latest
alias review judge fix alias review='clai -p reviewer -r q' alias judge='clai -cm jev-latest -rf $HOME/ship.json q' alias fix='clai -l -t "cat,go,apply_patch" -check "go test ./..." q' git diff --stat api/api.go | 1 + limiter.go | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) git diff | review 'Risks of this change, two lines.' Store.Incr errors are now swallowed: on failure n is likely 0, so Allow returns nil and the limiter fails open, letting all traffic through during a backend outage with no log or signal. Callers also lose the ability to distinguish a store failure from a normal allow, so outages go undetected; if fail-open is intended, make it explicit with logging/metrics. The api.go TODO comment is harmless. git diff | review 'Risks, two lines.' | judge '{}' | jq -c . {"model":"jev-1.14.0","answers":{"area":{"type":"choice","choice":"errors","confidence":0.77,"probabilities":{"errors":0.83,"performance":0.0,"security":0.17,"style":0.0}},"risk":{"type":"score","score":1.79,"confidence":0.53,"legend":{"0":"harmless","1":"minor","2":"serious","3":"outage"},"probabilities":{"0":0.12,"1":0.1,"2":0.65,"3":0.13}},"ship":{"type":"noul","noul":0.17}},"usage":{"input_tokens":546,"output_tokens":75}} git diff | clai -rf ~/verdict.json q 'Can this ship?' | jq -r .reason Do not ship: the limiter change discards the error from Store.Incr. If the store fails (e.g. backend outage), n will likely be zero, so n > l.Max is false and Allow returns nil. Every request is then allowed, which silently disables rate limiting (fail-open) and hides the failure. Restore the original error handling, or if fail-open is intended, make it explicit, log the error, and add a test. The TODO comment in api/api.go is harmless. fix 'make the tests pass'; echo exit=$? ▸ loop 1/10 assistant: The tests now pass. `Limiter.Allow` in `limiter.go` was discarding the error from `Store.Incr`, so requests were let through when the store was down. It now returns that error. `go test ./...` passes. exit=0
How clai works
One Go binary for every vendor you configure, composed with the shell you already use. Each panel is one take recorded in a terminal, played back as it ran: the commands, then every byte clai wrote.
shell context
Put your shell's state in the prompt, on demand
A model only knows what you send it. -asc git renders the template in shellContexts/git.json (the branch, git status, the last commit) and adds it to the prompt for that run. Each value is a shell command with a timeout, so a slow one can't stall the query.
ai 'What am I in the middle of? One line.' assistant: I can't tell. I don't have access to your terminal, files, or session history, only what you've typed here, so tell me what you're working on and I'll help from there. clai -asc git q 'What am I in the middle of? One line.' assistant: You're on `fix/rate-limit` with uncommitted edits to `api/api.go` and `limiter.go`, following up on your "fixed-window limiter" commit (`f660a40`).
swarm
A swarm is a for-loop
A profile in ~/.config/.clai/profiles/ bundles a system prompt, a model and its tools under one -p flag. Each run reads stdin, answers once and exits, so the shell's & and wait are all it takes to fan one diff out to a security, a performance and a style reviewer on different vendors.
for p in security perf style; do git diff | clai -p $p -r q 'One line.' > $p.txt & done; wait cat security.txt perf.txt style.txt security: Allow now discards the Store.Incr error, so if the backing store fails (n likely 0) the rate limiter fails open and lets every request through, which allows attackers to bypass limits (e.g., brute-force protection) by triggering store outages; restore the error return or explicitly fail closed. perf: swallowed store error in Allow means failed increments are treated as n=0 and silently allow unlimited requests, losing all rate limiting on store outages. style: Don’t ignore the Store.Incr error (`n, _ :=`); return/wrap it, since swallowing it can turn backend failures into false allows/denies and makes debugging impossible.
model swap
Switch models mid-conversation
clai stores a conversation in its own format, not a vendor's. -cm picks the model for one run, and -re or -dre carries the chat on, so the next turn can go to OpenAI, Anthropic, Google, Mistral, DeepSeek, xAI, Ollama or OpenRouter. A session started in Claude Code, Codex, OpenCode, Gemini CLI, Qwen or Antigravity can be imported and continued the same way.
cat limiter.go | clai -i q 'Name the bug in one line: {}' assistant: **Bug:** `Allow` discards the error from `Store.Incr` (`n, _ :=`), so a store failure returns `n == 0` and the limiter silently fails open, allowing every request. Fix: check and return the error (or deliberately choose fail-open or fail-closed): ```go n, err := l.Store.Incr(key, l.Now().Truncate(l.Window)) if err != nil { return err } if n > l.Max { return ErrLimited } return nil ``` ask -cm claude-haiku-5-5 'Now the smallest fix, two lines max.' assistant: ```go n, err := l.Store.Incr(key, l.Now().Truncate(l.Window)) if err != nil { return err } ```
directories
Each directory keeps its own conversation
A query binds its conversation to the directory it ran in. -dre replies to that one, so a decision made in api/ is still there when you come back, whatever you asked in web/ meanwhile. clai chat lists and resumes every thread.
ai 'We answer rate-limited calls with 429 and Retry-After. Acknowledge in one line.' assistant: Understood: rate-limited requests get an HTTP `429 Too Many Requests` response with a `Retry-After` header telling clients when to retry. ai 'Pick a toast library for a small Go web app, one line.' assistant: **Notyf** is a tiny, dependency-free JS toast library that works well with server-rendered Go templates (trigger it via flash messages or htmx `HX-Trigger` headers). ask 'What did we decide here? One line.' assistant: We decided that rate-limited calls get a `429 Too Many Requests` response with a `Retry-After` header.
images
Draw with a tool call, then talk about the picture
-t generate_image lets the chat model draw: the image is written to disk and the reply links it. The turn stays in the conversation, so -re can ask about the picture, and clai photo generates one without a chat.
clai -t generate_image q 'A minimal orange triangle on a dark navy background.' assistant {"prompt": "A minimal orange triangle centered on a dark navy backgrou nd. Flat vector style, clean edges, no other elements.", "rebus": "🔶 🌙 📐", "size": "1024x1024"} ▸ generate_image prompt=A minimal orange triangle centered on a dark n… ▸ [🔶 🌙 📐] image 4d0420 · gpt-image-2.5-flare · 1024×1024 (1:1) · 842 KB · 6s → file:///tmp/clai-site-demo/home/.config/.clai/conversations/assets/4d042013f5e3cafa56c6132749372d0bab1cebc948b595b8fde562d3a597b84b.png assistant: I generated the image: a flat orange triangle centered on a dark navy background, with nothing else in it. It's saved at `/tmp/clai-site-demo/home/.config/.clai/conversations/assets/4d042013f5e3cafa56c6132749372d0bab1cebc948b595b8fde562d3a597b84b.png`. clai -re q 'One line: what did you draw?' assistant: A flat orange triangle, centered on a dark navy background.
community
Download a config someone published, and rate it
Publish a profile, MCP server, shell context, SKILL.md skill, theme, or prompt to the Hub; anyone can read and download it. A signed-in reader rates a config from one to five stars, and the list ranks by the mean, so one five-star vote does not outrank a config with a history. Nothing syncs on its own, no key is uploaded, and the hub never runs a config.
curl -fsSL 'https://www.clai.chat/hub/<slug>/raw' \ -o "$(clai confdir)/profiles/reviewer.json" clai -p reviewer q "One line: what should a caller do when tx.Commit() fails?" Treat transaction outcome unknown; rollback, retry idempotently, reconcile externally.
Two ways to start
Run the open-source clai CLI on your machine, or join the community hub at clai.chat to publish and rate configs.
Share and rate configs
The public hub: profiles, MCP servers, shell contexts, skills, themes, and prompts.
- Read and download any config without an account.
- Sign in with GitHub or Google to publish one.
- Rate a config from one to five stars, one rating each.
Run it yourself
One static Go binary. 100% local.
Install
claiwith Go or Nix, set your vendor API key (or point at local Ollama), and keep every config and conversation on your own disk.
Install locally. Three steps from a fresh binary to a first query; Installation covers Go, Nix, and source builds.
Install the CLI
Run
go install github.com/baalimago/clai@latestor install the Nix flake. One static binary, no runtime dependencies.Set a key and configure
Export a vendor key such as
ANTHROPIC_API_KEY,OPENAI_API_KEY, orGEMINI_API_KEY, then runclai setupfor models, profiles, and shell contexts.Query, pipe, and chat
Run
clai qfor one-shot queries, pipe logs and diffs into it, enable tools with-t, or continue a thread withclai chat.
A profile is one JSON file:~/.config/.clai/profiles/reviewer.json
{
"name": "reviewer",
"model": "gpt-5.2",
"prompt": "You are a terse staff engineer reviewing Go code.",
"use_tools": true,
"tools": ["rg", "cat", "file_tree"],
"mcp_servers": {}
}All local configuration lives in ~/.config/.clai/ (or <os.UserConfigDir()>/.clai; run clai confdir to print it): model configs, profiles/, mcpServers/, skills/, shellContexts/, and conversations/.