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
~/work
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
One bad diff in one shell: reviewed by an alias, judged by Jev's fixed questions, judged again through a JSON schema, then fixed in a loop that ends only when go test passes.

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.

Read more: Skills and shell context →
~/work
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`).
The same question twice. Without -asc the model can't see the repo; with it, the branch, the dirty files and the last commit are in the prompt.

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.

Read more: Profiles and setup →
~/work
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.
Three profiles on three models, in parallel, from one for-loop. Each writes its own file, so the replies never interleave.

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.

Read more: Multi-vendor transcripts →
~/work
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 }
```
-cm swaps the model for one turn. ask is clai -dre q, so the second model picks up the first one's chat.

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.

Read more: Chats and multi-vendor import →
~/work/api
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.
ai is clai q and ask is clai -dre q. Back in api/, ask finds the conversation that started there, not the one from web/.

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.

Read more: Photo and video →
~/work
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.
-t generate_image draws in one tool call; -re asks the same chat what it drew.

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.

Read more: Community config hub →
~
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.
The hub serves the file as-is; clai reads it from profiles/. Read a config before you install it: it can name commands and servers.

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.

    Browse the Hub How the Hub works

  • Run it yourself

    One static Go binary. 100% local.

    Install clai with Go or Nix, set your vendor API key (or point at local Ollama), and keep every config and conversation on your own disk.

    Install the CLI

Install locally. Three steps from a fresh binary to a first query; Installation covers Go, Nix, and source builds.

  1. Install the CLI

    Run go install github.com/baalimago/clai@latest or install the Nix flake. One static binary, no runtime dependencies.

  2. Set a key and configure

    Export a vendor key such as ANTHROPIC_API_KEY, OPENAI_API_KEY, or GEMINI_API_KEY, then run clai setup for models, profiles, and shell contexts.

  3. Query, pipe, and chat

    Run clai q for one-shot queries, pipe logs and diffs into it, enable tools with -t, or continue a thread with clai 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/.

Type to search the documentation.