Anti vibe-coding AI agent for your terminal.
Use any OpenAI-compatible provider — OpenAI, xAI, DeepSeek, Kimi, and more, plus custom endpoints. Providers and models come from models.dev.
- Build / Ask / Plan — implement with tools, read-only Q&A, or plan first then approve into Build
- Permission prompts — shell, file changes, and reads outside the workspace ask before running, with optional remembered rules
- Folder trust — first open in a directory asks before loading project files
- Local sessions — chat history stays on your machine; resume anytime with
/resume - Auto-compaction — long chats summarize older context when the model window fills up
- Multi-provider — API keys and models managed in-app with
/config
curl -fsSL https://zeta.asy.sh/install.sh | shThen run zeta from a project directory.
- Start Zeta in the repo you care about.
- On first open in a folder, confirm you trust it (git root when in a repo). Choice is saved under
~/.zeta/trusted.json. - Run
/config, add a provider API key (or a custom endpoint). - Switch models with
/modelif you want. - Type a prompt and press Enter.
Each launch opens a new session. Use /resume to continue an earlier one.
Cycle modes with Shift+Tab.
| Mode | What it does |
|---|---|
| Build | Writes code and runs commands. Shell, file edits, and outside-workspace reads ask first. |
| Ask | Questions only — reads the codebase, no edits. Outside-workspace reads still prompt. |
| Plan | Plans without changing files. When ready: approve, revise, or discard. Approve picks a build model, clears context, switches to Build, and starts implementing. |
| Key | Action |
|---|---|
Enter |
Send (busy: queue text; empty+queue: send now) |
↑ / ↓ or Ctrl+P / Ctrl+N |
Prompt history |
Ctrl+Q |
Manage follow-ups (↑/↓, Enter send, e edit, d remove) |
Shift+Tab |
Cycle mode (build → ask → plan) |
Shift+Enter / Ctrl+J / Alt+Enter |
Newline |
@ |
File mention picker (gitignore-aware; Tab/Enter insert) |
Tab (in /model) |
Cycle reasoning (low → medium → high) |
Esc |
Cancel edit / leave queue / cancel turn (queue kept) |
Ctrl+C |
Leave edit/focus → interrupt → clear queue → quit |
Mouse / PgUp / PgDn |
Scroll |
| Drag transcript | Select text and copy on release (no scrollbar) |
Type @ in the composer to fuzzy-find a workspace file (respects .gitignore via ripgrep). Tab or Enter inserts @path and a trailing space; Esc closes the list without clearing the draft.
When the agent wants to run a shell command, change a file, or read outside the workspace:
| Action | Keys |
|---|---|
| Shell | [a] allow once · [p] always allow · [s] allow for session · [d] deny |
| Edit / write | [a] allow · [p] always allow · [d] deny |
| Read (outside workspace) | [a] allow once · [s] allow this directory for session · [d] deny |
Read (.env / .env.*) |
[a] allow · [p] always allow this file · [d] deny |
You can also click, or use ↑/↓ + Enter. Esc cancels. Ask and Plan have no shell or edit tools, but they still prompt for outside-workspace reads. The [p] row appears only when a rule can be remembered — see Remembered rules.
Edit/write/read paths resolve relative to the workspace root, but absolute paths and .. escapes are allowed — when the target is outside the workspace, the prompt is marked (outside workspace). Outside edits/writes need per-call approval. In-workspace reads never prompt, except dotenv secrets (.env, .env.*; .env.example is allowed). An outside read asks to access that file's directory; a session grant covers later reads under it, not every outside path, and does not skip dotenv files. grep/glob and bash's workdir stay inside the workspace.
When a prompt can be remembered it also offers [p] always allow — for a shell command prefix, an in-workspace edit/write file, or an in-workspace dotenv read. Out-of-workspace edit/write targets, outside reads, and shell commands with chaining (&&, |, ;, redirects, $(…)), keep the plain allow/deny prompt (outside reads also offer a directory-scoped session grant).
Remembered rules live in ~/.zeta/permissions.json (or $ZETA_HOME/permissions.json), separate from config.json, and are loaded at startup. The prompt only ever writes allow rules; add deny rules by hand-editing the file.
Rules are evaluated with deny-precedence: any matching deny wins (even over an allow or a session grant); otherwise a matching allow runs; otherwise zeta asks as usual. Rules are checked for every tool call, so a tool-level (or "*") deny also blocks tools that usually auto-run (grep, glob, in-workspace read, …). A tool-level read allow skips outside-workspace and dotenv read prompts; allow for other auto-run tools is redundant. Prompts that are always interactive — ask_user — still reach you regardless of rules. A rule matches on tool ("bash", "edit", "write", "read", or "*") plus one optional field:
command_prefix(bash) — "commands that start with…", matched on whole words. Rememberinggo test ./...storesgo testand also coversgo test -v. A command whose second word is a flag keeps the whole string (rm -rf /), so it never broadens to everyrm.bashis unsandboxed, so this is a guardrail, not a sandbox.command(bash) — a glob against the raw command string:*matches within a segment,**crosses/,?matches one character, and a pattern without*is exact.path(edit/write/read) — a glob against the workspace-relative path; out-of-workspace edit/write never match one. Out-of-workspace reads match the absolute path instead, so a deny like**/.ssh/**still applies.
{
"rules": [
{ "tool": "bash", "command_prefix": "go test", "action": "allow" },
{ "tool": "bash", "command_prefix": "git push", "action": "deny" },
{ "tool": "edit", "path": "src/**", "action": "deny" },
{ "tool": "read", "path": "**/.ssh/**", "action": "deny" }
]
}Hand-edit the file to add rules or to broaden one beyond what the prompt remembers.
Sometimes the agent asks a multiple-choice question (plus freeform Other):
| Key | Action |
|---|---|
↑ / ↓ |
Move |
Enter |
Confirm |
1–9 |
Jump to option |
| Type | Fill Other |
Esc |
Cancel |
Shift+Enter works best in terminals that report modified keys (Kitty keyboard protocol): Ghostty, Kitty, iTerm2 3.5+, Alacritty, WezTerm (enable_kitty_keyboard = true).
If Shift+Enter is remapped (common in iTerm), remove that binding or use Ctrl+J / Alt+Enter for newlines.
Copy: drag in the transcript to select (the scrollbar is not included); releasing the mouse copies to the clipboard. Leaving the terminal mid-drag counts as release.
While the agent is working, Enter with text queues a follow-up. Empty Enter (or queue-focus Enter on an item) interrupts the current turn and sends that follow-up immediately. Open the queue with Ctrl+Q (↑/↓ move, Enter send selected, e edit, d remove, Esc back). Queued items also drain one at a time when a turn finishes on its own (unless you are editing the next item or typing a draft). Esc cancels an edit, leaves queue focus, or cancels the turn (queue kept). Ctrl+C leaves edit/queue focus first, then runs the interrupt ladder (dismiss overlays, cancel turn), then clears any remaining follow-ups, then quits.
Type / for autocomplete.
| Command | Description |
|---|---|
/clear |
Start a new session |
/compact |
Summarize older context now |
/resume |
Open a previous session |
/model |
Switch model; Tab cycles reasoning (low / medium / high) |
/config |
Manage providers and models |
/update |
Update to the latest release |
/review |
Strict code-quality review of the current branch |
Long sessions compact automatically when context runs low; /compact does the same on demand.
Use /config in the app. Settings live at ~/.zeta/config.json (or $ZETA_HOME/config.json).
In /config:
- Configured — turn models on/off for providers you already set up
- Providers — add a catalog provider (API key, then enable models;
Ctrl+Atoggles all) - Custom — your own OpenAI-compatible endpoint
Optional: set a preferred build model after plan approve with "defaults": { "build": "provider/model" } in the config file. Per-model reasoning_effort (low / medium / high) is set from /model with Tab.
Example shape (prefer the UI over hand-editing):
{
"active": "deepseek/deepseek-v4-flash",
"providers": {
"deepseek": {
"name": "DeepSeek",
"base_url": "https://api.deepseek.com/v1",
"api_key": "sk-...",
"models": {
"deepseek-v4-flash": { "name": "V4 Flash", "context_window": 1000000, "reasoning_effort": "medium" }
}
}
}
}When the agent searches the web, Exa is used by default (shared free quota, no key required). Optional env vars:
| Variable | Purpose |
|---|---|
EXA_API_KEY |
Your own Exa quota (dashboard) |
PARALLEL_API_KEY |
Parallel search quota |
ZETA_WEBSEARCH_PROVIDER |
exa (default) or parallel |
Fetching a URL is separate and works without those keys. Private/loopback addresses are blocked.
Everything is under ~/.zeta (override with ZETA_HOME):
~/.zeta/
config.json
permissions.json
sessions/… # your chat history
Sessions appear after the first message; Zeta generates a short title for the /resume picker.
Development setup and project layout: CONTRIBUTING.md.