What is it?
Yet another coding agent harness, lightweight and written (vibe-slopped) in go.
- one static binary.
- built-in providers for Anthropic, OpenAI/Codex/Responses, Kimi, DeepSeek, Google Gemini/Vertex, GitHub Copilot, Bedrock, Azure OpenAI, OpenRouter, Yolo-Auto, Groq, Cerebras, xAI, Together, Hugging Face, Mistral, Moonshot, Z.AI, Xiaomi, MiniMax, Fireworks, Vercel AI Gateway, OpenCode, Cloudflare AI, and Ollama/local models.
- built-in tools (read, write, edit, bash, glob).
- three run modes (interactive tui, print, json).
- built-in telegram bot.
- extensions in any language via subprocess + json-rpc. None installed by default; opt in with
zot ext install or zot --ext. See docs/extensions.md.
- user and extension themes via JSON; see docs/themes.md.
- standing instructions via
AGENTS.md files (global and per-project); see Persistent instructions.
- reusable instructions via
SKILL.md files; see docs/skills.md.
- portable agents from local directories,
.zot files, or temporary public GitHub downloads; see docs/zotfiles.md.
Install
One-liner (macOS, Linux)
curl -fsSL https://www.zot.sh/install.sh | bash
Detects your OS and architecture, downloads the latest release from GitHub, verifies the SHA-256 against the release's checksums.txt, extracts the binary, and drops it in /usr/local/bin, ~/.local/bin, or ~/bin, whichever is writable first. Pass a version or prefix to pin:
curl -fsSL https://www.zot.sh/install.sh | bash -s -- v0.0.1 ~/bin
One-liner (Windows, PowerShell)
iwr -useb https://www.zot.sh/install.ps1 | iex
Drops zot.exe into $HOME\bin and adds it to the user PATH if missing. Open a fresh terminal afterwards.
go install
go install github.com/patriceckhart/zot/cmd/zot@latest
The installed binary reports the tagged module version and supports zot update.
From source
git clone https://github.com/patriceckhart/zot
cd zot
make build # produces ./bin/zot
make install # into $GOPATH/bin
Prebuilt binaries
Every release on the releases page ships archives for Linux, macOS, and Windows on amd64 and arm64 (except windows/arm64), plus a checksums.txt file. Download, verify, chmod +x, and drop on your $PATH.
Authenticate
The easiest way is to just run zot and type /login. The TUI opens even without credentials and walks you through a browser-based login flow.
Credential lookup order
--api-key flag
- provider-specific env var (
ANTHROPIC_API_KEY, OPENAI_API_KEY, KIMI_API_KEY, MOONSHOT_API_KEY, DEEPSEEK_API_KEY, GEMINI_API_KEY, GOOGLE_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY, MISTRAL_API_KEY, XAI_API_KEY, CEREBRAS_API_KEY, TOGETHER_API_KEY, HF_TOKEN, ZAI_API_KEY, XIAOMI_API_KEY, MINIMAX_API_KEY, FIREWORKS_API_KEY, AI_GATEWAY_API_KEY, COPILOT_GITHUB_TOKEN, GITHUB_COPILOT_TOKEN, and others for provider-specific backends)
$ZOT_HOME/auth.json (API key or OAuth token; mode 0600)
$ZOT_HOME defaults to:
- All platforms:
$XDG_STATE_HOME/zot when XDG_STATE_HOME is set
- macOS fallback:
~/Library/Application Support/zot
- Linux fallback:
~/.local/state/zot
- Windows fallback:
%LOCALAPPDATA%\zot
API keys from commands
To keep an API key in a password manager instead of auth.json, configure an api_key_command for the provider:
{
"anthropic": {
"api_key_command": {
"program": "op",
"args": ["read", "op://Work/Anthropic/credential"],
"timeout_ms": 120000
}
}
}
For a provider added through models.json, put the same credential object under additional_api_key_creds using its provider ID. program is executed directly, without a shell, so each argument must be a separate args entry. timeout_ms is optional and defaults to 120 seconds.
zot runs the command only when that provider is selected, not while checking login status or refreshing model catalogs in the background. Successful output is cached in memory for the rest of the zot process and is never written to disk. The command must print one non-empty line to stdout; zot removes trailing CR/LF characters, limits output to 64 KiB, and does not include command output in errors. Saving a normal key through /login replaces the command configuration, and /logout removes it.
Treat auth.json as executable configuration: anyone who can modify it can cause zot to run a program under your user account. zot does not interpret ! prefixes or execute command strings through a shell.
/login flow
Run zot and type /login. Pick one of two methods:
- API key: a small local web server starts on
127.0.0.1:<free-port>, your browser opens a form, you pick a provider from the full API-key provider list, paste the key, and zot saves it to auth.json if accepted. Providers with a lightweight model-list endpoint are probed before saving; provider backends that need extra project/account env vars are saved directly.
- Subscription: use your Claude Pro/Max, ChatGPT Plus/Pro, Kimi Code, SuperGrok/X Premium, or GitHub Copilot subscription. DeepSeek and Google Gemini do not have a subscription login path. For those, use the API-key flow.
- For Anthropic, choose browser callback (
localhost:53692) or copy code before starting login. Browser callback also accepts a pasted code or full redirect URL from the same authorization link. Copy code uses Anthropic's hosted redirect and works over SSH or on headless machines without a local callback server. Anthropic uses the Claude Code OAuth flow. Messages go to api.anthropic.com with a bearer token and the Claude Code identity headers.
- OpenAI uses the Codex CLI OAuth flow with a browser callback on
localhost:1455. The same authorization link also accepts a pasted code or full redirect URL if the browser cannot reach zot's callback port (for example, over SSH). Copy the final redirect URL from the browser address bar, even if the page fails to load locally. The callback port must still be available on the machine running zot. Messages go to chatgpt.com/backend-api/codex/responses with the chatgpt-account-id extracted from the returned id_token.
- Kimi uses the Kimi Code device-code OAuth flow. zot opens the verification URL, polls until you approve it in the browser, then sends messages to
api.kimi.com/coding/v1 with the Kimi Code identity headers.
- xAI uses a device-code OAuth flow. zot opens a prefilled authorization URL, polls for approval, and uses the resulting token with the xAI API.
- GitHub Copilot uses GitHub's device-code login flow. zot stores the GitHub access token and exchanges it for short-lived Copilot inference tokens on demand.
Note on subscription login. The OAuth client IDs used are the ones published in Anthropic's Claude Code CLI, OpenAI's Codex CLI, Kimi Code CLI, xAI's device flow, and GitHub Copilot's device-code flow. Reusing them from a third-party tool may be against their terms of service and may be revoked at any time. Use it at your own risk; the API-key flow is the safe default.
Token refresh
OAuth access tokens are short-lived (Anthropic ~8h, OpenAI ~30d; Kimi, xAI, and GitHub Copilot also use refresh/exchange flows). zot refreshes or exchanges them automatically:
- At every credential lookup, zot checks the stored
expiry and, if past it (with a 60s safety margin), hits the provider's oauth/token endpoint with the stored refresh_token, persists the new access_token, refresh_token, and expiry back to auth.json, and hands the fresh token to the client.
- The telegram bridge additionally refreshes once per turn so a bot that runs for days keeps working without manual intervention.
- If the refresh itself fails (the
refresh_token was revoked, or the account was logged out everywhere), the error bubbles up to the caller: the TUI shows it in the status line, the bot replies with it in your DM. Run /login to get a fresh token pair.
All data lives under $ZOT_HOME:
$ZOT_HOME/
├── config.json # last-used provider/model/theme, saved automatically
├── models.json # optional: user-defined models and provider endpoints
├── auth.json # api keys and oauth tokens (mode 0600)
├── sessions/ # jsonl transcripts, one dir per cwd
├── models-cache.json # live /v1/models discovery cache (6h ttl)
├── AGENTS.md # optional: global instructions appended to the prompt
├── SYSTEM.md # optional: replaces the default system prompt
├── skills/ # optional: user SKILL.md files
├── themes/ # optional: user theme JSON files
├── extensions/ # installed extensions, one dir per extension
└── logs/ # app log files
Drop a SYSTEM.md in $ZOT_HOME to replace the built-in identity and zot-docs guidance for every run. --system-prompt still wins per-invocation. Pass an empty value (--system-prompt "") to intentionally omit the built-in identity. Custom prompts still receive appended instructions and generated context, including AGENTS.md, skills, auto-swarm guidance when enabled, and the date/cwd footer. Delete the file to revert to the default.
Configuration files
zot keeps both configuration and runtime state under $ZOT_HOME, even when that directory is under the platform's state directory. You can edit configuration files there directly while zot is not running:
config.json selects the default provider and model and stores UI preferences. zot also updates it when you change settings or select a model interactively. Explicit --provider and --model flags override the saved selection for a run.
models.json optionally defines model metadata and provider endpoints (baseUrl). Adding a model here does not select it as the default. See Custom models.
auth.json stores credentials, not model definitions. Prefer /login for supported providers, and keep secrets out of config.json and models.json.
Neither config.json nor models.json is required to start zot with an explicit provider and model. Without a saved or explicit provider, zot defaults to Anthropic and may offer login if no usable credentials are available. Ollama needs no login. See Local models with ollama for minimal configuration and remote-server examples.
HTTP proxy
To route zot-managed HTTP and HTTPS requests through one proxy, add http_proxy to $ZOT_HOME/config.json:
{
"http_proxy": "http://127.0.0.1:7890"
}
The setting is applied at startup to both HTTP and HTTPS traffic. Existing HTTP_PROXY, HTTPS_PROXY, http_proxy, and https_proxy environment variables take precedence for their corresponding protocol. NO_PROXY and no_proxy continue to control bypasses. Restart zot after changing the config file. If the URL contains proxy credentials, prefer protected environment variables because config.json is not a credential store.
Configurable shortcuts
Interactive keybindings can be added to $ZOT_HOME/config.json under keymap. Each key is a case-insensitive chord made of at least one modifier (ctrl, alt/option, shift, cmd/super) plus a single character or a named key (enter, tab, space, esc, backspace, delete, up, down, left, right, home, end, pageup, pagedown). Each value is a slash command: built-in commands, extension commands, and explicitly invoked /skill:<name> all work.
{
"keymap": {
"ctrl+s": "/settings",
"ctrl+g": "/git-tools:status",
"alt+r": "/skill:code-review"
}
}
Shortcuts run in the main interactive input and use the same dispatch path as
typing the command. They do not apply while a dialog, extension panel, or
tool confirmation has focus. Entries that cannot be bound are skipped and
reported at startup (the message clears with /clear): unmodified keys such
as enter or g, values that do not start with /, and the reserved
ctrl+c, ctrl+d, and esc chords. /help lists the active bindings.
Terminal limitations: without enhanced keyboard reporting (kitty keyboard
protocol or xterm modifyOtherKeys), a terminal cannot distinguish
ctrl+h, ctrl+i, and ctrl+m from Backspace, Tab, and Enter, and it does
not report Shift on Ctrl+letter chords. zot therefore lets a ctrl+shift+<letter>
binding also fire on plain ctrl+<letter> unless a separate ctrl+<letter>
binding exists. Chords the terminal emulator or multiplexer consumes itself
(a tmux prefix, cmd+k clearing scrollback, tab switching) never reach zot.
Persistent instructions (AGENTS.md)
Use AGENTS.md to give zot standing instructions that layer on top of the default system prompt, without replacing it. This is the friendliest way to shape behavior (for example, taming local models that jump straight to code edits) because it adds guidance rather than taking over the base identity the way SYSTEM.md does.
zot discovers AGENTS.md files automatically at startup and loads them in this order:
$ZOT_HOME/AGENTS.md (global, machine-wide instructions that apply to every project).
- Every
AGENTS.md from the filesystem root down to the current working directory. More specific (deeper) files may override earlier ones. This includes ~/AGENTS.md when the working directory is inside your home directory.
All discovered files are appended to the prompt in that order, so a global baseline can be refined per project. To list the active Zotfile agent, loaded context paths, extensions, and user-installed skills above the interactive transcript, enable show loaded resources at startup in /settings. The agent section appears only for zot run, the setting is off by default, built-in skills are omitted, and the displayed lists are not sent to the model or stored in the session transcript.
For a general, non-project-specific instruction set, put your rules in $ZOT_HOME/AGENTS.md, for example:
Treat questions and discussions as requests for explanation. Do not edit files or run tools unless explicitly asked to make a change. Ask before modifying code.
AGENTS.md vs the other mechanisms:
| Mechanism |
Scope |
Effect |
$ZOT_HOME/AGENTS.md |
global, every run |
appended to the default prompt |
./AGENTS.md (and parent dirs) |
project |
appended to the default prompt |
$ZOT_HOME/SYSTEM.md |
global, every run |
replaces the built-in identity and zot-docs guidance |
--append-system-prompt <text> |
single run |
appends for one invocation (repeatable) |
--system-prompt <text> |
single run |
replaces the built-in identity and zot-docs guidance |
Note: zot does not read a CLAUDE.md instruction file. The only Claude-compatible thing it picks up is skills under .claude/skills/. If you are migrating from Claude Code, move that content into AGENTS.md (global or per-project) and zot will use it.
Changelog on update
The first time you launch a newer zot binary, the TUI shows the GitHub release notes once in a dismissible overlay. Press any key to close. The version is recorded in config.json's last_changelog_shown so the same release notes never reappear. Fresh installs don't see a changelog (no upgrade has happened yet). The fetch is best-effort: a network failure or a missing release page silently skips, with another attempt on the next launch.
Usage
zot # interactive tui
zot "fix the failing test" # tui, pre-filled prompt
zot -p "list all go files" # print final text, exit
echo "list all go files" | zot # piped stdin implies print mode
cat README.md | zot -p "summarize this text" # combine stdin with the prompt
zot -p --stats stats.json "task" # print final text and write generation stats
zot --json "refactor main.go" # newline-delimited json events, exit
zot --continue # resume the most recent session for this cwd
zot --resume # pick a session to resume
zot --list-models # show supported models
zot --help
Print-mode stats contain provider, model, prompt_tokens, reasoning_tokens, generated_output_tokens, and elapsed_ms. Counts cover all model turns triggered by the prompt, including tool loops. Prompt tokens include cache reads and writes. reasoning_tokens is null when the provider does not report a separate count; in that case generated_output_tokens is the provider's total output count and may include reasoning. Elapsed time covers the agent run, not startup and credential resolution. The file is written only after a successful run.
Flags
| Flag |
Description |
--provider <id> |
Pick the provider (for example anthropic, openai, openai-codex, kimi, google, github-copilot, groq, openrouter, amazon-bedrock, ollama; see Providers). |
--model <id> |
Pick the model (see --list-models). |
--api-key <key> |
Override the API key. |
--base-url <url> |
Override the provider base URL (tests, self-hosted). |
--insecure |
Skip TLS certificate verification for the explicit --base-url endpoint or a baseUrl defined for a user model in models.json (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. |
--system-prompt <text> |
Replace the default system prompt for this run (also overrides $ZOT_HOME/SYSTEM.md; pass "" for no built-in identity). |
--append-system-prompt <text> |
Append text to the system prompt (repeatable). |
--reasoning off|minimum|low|medium|high|xhigh|max |
Set the reasoning level on supported models (default: off). max is a separate opt-in tier above xhigh. |
--stats <path> |
With -p/--print, write generation statistics as JSON. |
-c, --continue |
Resume the latest session for this cwd. |
-r, --resume |
Pick a session to resume. |
--session <path> |
Resume a specific session file. |
--no-session |
Don't read or write session files. |
--cwd <path> |
Use <path> as the working directory. |
--no-tools |
Disable all tools. |
--tools <csv> |
Only enable the listed tools. |
--max-steps <n> |
Cap agent loop iterations (default 50). |
-e, --ext <path> |
Load an extension from <path> for this run (repeatable; wins against installed extensions of the same name). |
--no-ext |
Skip extension discovery for this run. --ext still works on top, so --no-ext --ext ./x runs only x. |
--no-skill |
Disable all skills, including built-ins. No skill tool is registered and the system prompt has no skill manifest. |
--no-context-files, -nc |
Disable discovery and loading of global and project AGENTS.md files. |
--no-yolo |
Confirm every tool call before it runs (interactive TUI only). A dialog shows the tool name and a one-line preview of its args, while edit calls show the proposed diff in the tool panel, with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. Press / while the dialog is focused to run a slash command first; closing its input or child dialog returns to the pending confirmation. Ignored with a stderr warning in print / json / rpc modes, where tools still run freely so scripts and automation keep working. |
read: read text files, or inline images (PNG, JPEG, GIF, WebP).
write: create or overwrite files, making parent directories as needed.
edit: one or more exact-match replacements in an existing file.
bash: run a command in the session cwd with merged stdout/stderr and a timeout. On Unix, zot uses /bin/bash -c when available, then bash -c from PATH, and falls back to POSIX /bin/sh -c when Bash is unavailable. On Windows, it uses cmd /C. macOS ships Bash 3.2 by default, so newer Bash features may be unavailable.
When the sandbox is on (see /jail), filesystem tools refuse paths outside the session cwd and bash applies best-effort command checks. Jail mode is an accident-prevention guardrail, not a security boundary.
The optional powershell tool executes native PowerShell commands in the session cwd. On Windows, open /settings and toggle PowerShell tool to enable or disable it alongside the default tools. It is off by default, persists as powershell_enabled in $ZOT_HOME/config.json, and updates the live tool list without a restart. The saved preference also applies to other modes on Windows.
For a one-run tool selection, enable it instead of bash:
zot --tools read,write,edit,glob,powershell
Include bash in that list to enable both. Explicit --tools and --no-tools override the saved preference and disable the settings toggle for that run. The toggle is hidden on non-Windows platforms. ! shell escapes remain unchanged (cmd /C on Windows).
PowerShell execution prefers pwsh.exe on PATH, falling back to powershell.exe. It uses -NoProfile -NonInteractive -ExecutionPolicy Bypass, with UTF-8 output and encoded command transport to preserve Unicode and quoting. Administrator-enforced execution policies can still take precedence. Missing executables and non-Windows execution return explicit errors.
The tool accepts command and an optional timeout in seconds, with merged streaming stdout/stderr, exit status, and the same output limits as bash. On Windows, cancellation terminates the shell and bounds output draining, but does not guarantee termination of descendant processes.
PowerShell is blocked while /jail is active because the existing shell checks do not parse PowerShell syntax. Packaged agents require permissions.bash.mode: "ask" (launch-time consent); none, allowlist, and unknown modes deny PowerShell execution. Bash allowlists are not applied to PowerShell.
Modes
- Interactive (default): chat TUI with streaming output, spinner, cost meter, slash commands.
- Print:
zot -p "prompt" runs the agent to completion and writes only the final assistant text to stdout.
- Stream:
zot --stream "prompt" runs without the TUI and writes assistant text to stdout as it arrives. Tool activity goes to stderr.
- Piped input: when no mode is specified, piped stdin selects print mode. Print, stream, and JSON modes prepend piped stdin to the positional prompt, separated by a newline. For example,
echo "list all go files" | zot or cat README.md | zot -p "summarize this text".
- JSON:
zot --json "prompt" emits one JSON object per agent event to stdout, newline-delimited. The schema is documented in docs/rpc.md.
- RPC:
zot rpc runs as a long-lived child process; commands in on stdin, events and responses out on stdout, both as NDJSON. Designed for embedding zot in third-party apps written in any language. See docs/rpc.md for the wire schema and examples/rpc/{python,node,shell,go} for working clients.
zotfile agents
A zotfile packages an agent's instructions, skills, requirements, and enforced tool permissions as a shareable agent. Run one from a local directory, a packed .zot artifact, a short name, or directly from a public GitHub repository:
zot run ./my-agent
zot run ./my-agent.zot
zot run frkr/zot-archify
zot run acme/agents/code-reviewer --cwd /path/to/project
Single-part names resolve only to a matching local directory or .zot archive. Anyone can publish an agent as a public GitHub repository and run it as owner/repository, or publish a collection and run an agent directory as owner/repository/agent. zot has no built-in owner, official collection, configured registry, or allowlist. For GitHub sources, zot downloads the repository archive into a temporary directory, validates and runs the selected agent, then removes the downloaded source when the command exits. Agent data, consent receipts, and sessions still persist under $ZOT_HOME. See docs/zotfiles.md for authoring, permissions, packaging, and current limitations.
Embedding
Two ways to drive zot from another program:
- Go in-process: import
github.com/patriceckhart/zot/packages/agent/sdk. One Runtime per project; Prompt(ctx, text, images) returns a channel of Event. Small example in examples/sdk/.
- Any language, out-of-process: spawn
zot rpc as a subprocess and exchange newline-delimited JSON over its stdin/stdout. Wire format and event schema in docs/rpc.md. Reference clients live under examples/rpc/.
Both interfaces share the same event schema, so transcripts captured by one can be replayed through the other.
Slash commands
Slash command names are case-insensitive in the TUI and messaging backends; arguments keep their original case. Type / in the TUI to open the autocomplete popup. Available commands:
| Command |
Description |
/help |
Show key bindings and commands. |
/login |
Log in via API key or subscription (opens a dialog). |
/logout [provider] |
Clear credentials for any logged-in provider, or all when omitted. /logout openai-codex clears ChatGPT/Codex subscription auth while preserving a public OpenAI API key; /logout kimi also disables fallback to the official Kimi Code CLI token until you log in to Kimi through zot again. |
/model |
Pick a model from a list (or /model <id> to set directly). |
/reasoning |
Set the reasoning level for subsequent model calls. |
/llama |
Connect to the configured llama.cpp router, load, unload, or remove cached models, and search/download GGUF models from Hugging Face with live progress. Shown after llama.cpp login is configured. |
/sessions |
Resume a previous session for this directory. |
/new |
Persist the current conversation and start a fresh session in the same directory. Unavailable with --no-session. |
/session |
Five ops on the current session: inspect its timeline, export to a portable .zotsession file, import one back in, fork from a past user message, or view its branch tree. Opens a picker without an argument; direct forms include /session timeline, /session export [path], /session import <path>, /session fork, and /session tree. Default export destination is ~/Downloads. |
/jump |
Scroll the chat to a previous turn (or /jump <text> to filter). |
/btw |
Side chat with full context that doesn't add to the main thread. |
/swarm |
Spawn, monitor, and chat with background subagents. Each runs in parallel with your main session and shares its working directory. |
/skills |
List discovered skills (SKILL.md files) and preview their bodies. |
/compact |
Summarize the transcript into one message to free up context. |
/study |
Run the canned prompt "Read and understand everything in the current directory." so the agent has full project context before you start asking targeted questions. Pass a path — typed, drag-dropped, or selected via @ — to target a specific file or directory instead: /study [dir:packages/], /study cmd/zot/main.go. |
/jail |
Confine tools to the current directory. |
/unjail |
Allow tools to touch paths outside again. |
/reload-ext |
Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |
/telegram |
Connect, disconnect, or show status of the Telegram bridge (takes connect / disconnect / status as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: /tg. |
/settings |
Change persistent settings, including inline images, auto-swarm, and the auto-compact threshold. Saved to $ZOT_HOME/config.json; takes effect immediately. |
/clear |
Clear the chat transcript. |
/exit |
Exit zot. |
Extension-registered commands appear under a divider at the bottom of the popup, sorted by name.
Shell escape (!command)
Type ! followed by a command to run it directly without going through the model. Everything after the ! is passed to the same shell the bash tool uses (/bin/bash -c when available on Unix, then bash -c from PATH, with POSIX /bin/sh -c as a fallback; cmd /C on Windows), runs in the session working directory, and honors the /jail sandbox. The command, merged output, and exit status are appended to the transcript as user context, so the model can use them on the next turn. Running the command does not itself start a model turn. A running !command shares the busy state with the agent: esc cancels it, and you cannot start one while a turn (or another shell escape) is in flight.
After a shell command finishes, up / down input history recalls it with the leading !, without its output or exit status. This also works when resuming sessions saved with command metadata. Shell commands from older sessions without that metadata remain excluded from input history.
/sessions
Shows previous sessions for the current working directory, newest first, with timestamp, model, message count, cost, and the first user prompt. Pick one with up/down, enter to resume, r to rename, d to delete, or esc to cancel. Deletion requires confirmation and permanently removes the session file; the currently active session cannot be deleted. zot swaps the current session file for the selected one and replays the full transcript (including tool calls) into the agent. Sessions remember the model they ended on, so resuming picks up on that exact model even if your global default changed.
/session
Five ops on the current session. /session alone opens a picker; each is also runnable directly.
-
/session timeline. Replaces the chat area with a read-only timeline of the current system prompt, user and assistant messages, and tool calls. The header shows provider-reported context usage and an estimated split between system prompt, tool definitions, and messages. The split uses a simple byte estimate, so it is directional rather than tokenizer-exact. Use up / down or pgup / pgdn to select an event, tab / shift+tab to inspect its summary, payload, result, schema, and transcript-derived timing, and / to search. ctrl+e writes a JSON export to ~/Downloads (falling back to the home directory) with 0600 permissions on platforms that support Unix file modes. Images are represented by MIME type and byte size instead of embedding their data. Press esc to return to chat.
-
/session export [path]. Writes the running transcript to a portable .zotsession file. Default destination is ~/Downloads/<timestamp>-<session-id>-<prompt-slug>.zotsession. Pass a path to override; a directory is fine (a dated name is built inside), a bare name gets .zotsession appended. The meta's cwd is stripped on the way out so the recipient doesn't see your filesystem layout.
What's included. Only the main chat thread of the running session — messages, tool calls, tool results, compactions, and usage. /swarm subagents are NOT included. Their transcripts, unix-socket inboxes, and per-agent session files are all machine-local; a .zotsession is just a chat transcript and has no way to revive a unix socket on another box. If you want the conversation, copy it out of the dashboard manually.
-
/session import <path>. Copies a .zotsession file into $ZOT_HOME/sessions/<cwd-hash>/ with a fresh id and the current cwd, then switches the running agent onto it. Imported sessions are first-class: they show up in /sessions, /jump, and the tree. Drag-drop paths in the editor are accepted (zot strips the surrounding quotes automatically).
-
/session fork. Opens a turn picker (same shape as /jump). Pick any past user message; zot copies every message up to and including that turn into a new session, records parent + fork_point in the new meta, and switches onto the branch. The parent session stays on disk. Use it to try a different question without polluting the original transcript, or to rewind after the agent went down the wrong path.
-
/session tree. Shows every session in the current cwd arranged by parent/child relationships, depth-first with indent per level. The current session is tagged [current]. Pick any entry to switch into it. Parentless sessions are roots; branches created via /session fork nest under whichever session they were forked from. Orphaned children (whose parent file was deleted) still show as roots so they stay discoverable.
/jump
Opens a turn picker for the current session, one row per user prompt, each showing the turn number, how many tools that turn invoked, and the first line of the prompt. up/down to pick, enter to jump, esc to cancel. Any printable rune while the picker is open extends a filter; backspace narrows it back. /jump <text> pre-applies the filter; if exactly one turn matches, zot jumps straight there without showing the picker.
Jumping is non-destructive. The transcript and future model context are untouched. Zot opens a temporary history view with the chosen turn at or near the top and a viewing turn N of M indicator. Use pgup and pgdn to browse. Reaching the bottom restores the live view, including output received while browsing. History browsing uses the terminal's alternate screen rather than trying to reposition native scrollback. The normal live view still uses native terminal scrollback. Use /session fork instead if you want to branch from an earlier turn.
/btw
Opens a side-chat overlay with the full main session as frozen context, so you can ask quick clarifying questions ("does asyncio.gather() catch exceptions?", "btw the bundle budget is 10MB", "what's the default fetch timeout?") without bloating the main thread.
Each question runs an isolated agent turn against system + main transcript + side-chat history so far. The side chat has the same tools and guards as the main chat, including per-tool confirmation when --no-yolo is active. Responses, tool calls, and tool results stay in the overlay. When you press esc to close, nothing has been added to the main session and subsequent main-thread turns don't re-read any of the side-chat exchanges, keeping the running context window lean.
/btw # open the overlay, type questions interactively
/btw does PUT replace the whole resource?
Inside the overlay: enter sends, esc cancels an in-flight call (or closes the overlay if idle), ctrl+c closes immediately. Side-chat exchanges never touch the transcript and aren't persisted to the session file.
/swarm
Background subagents that run alongside your main session. Each one is a separate zot subprocess with its own model loop, its own persistent session file, and its own chat in the dashboard — but they all run in the same working directory as the host, so they see and edit the same files you do. Spawn one for a side task (“draft the migration”, “investigate this stack trace”, “write the test harness for module X”), keep going in the main thread, check in on it whenever you want.
Agents edit the same files you do. They use the same read / write / edit / bash tools as the main agent against the host's working directory. There's no per-agent worktree or branch. If you need parallel edits on isolated checkouts, set that up yourself with git worktree outside zot.
/swarm # open the dashboard
/swarm new <task> # spawn an agent
/swarm new --model gpt-5 <task> # pin the new agent to a specific model
/swarm logs <id> # jump straight into one agent's transcript
/swarm send <id> <text> # send a follow-up without opening the dashboard
/swarm resume # pick a stopped agent to bring back
/swarm resume <id> # bring a specific agent back
/swarm kill <id> # stop a running agent (its state stays)
/swarm remove <id> # delete the agent's session and state
/swarm list # alias for opening the dashboard
Dashboard (/swarm with no arg) — a list of every agent for the current session, with status, age, and current activity. Keys:
| Key |
Action |
↑ / ↓ |
Move cursor between rows. |
enter |
Open the highlighted agent's transcript view. |
n |
Spawn a new agent (opens an inline task editor; inherits the host's current model). |
p |
One-off prompt editor for the selected row (without entering the transcript). |
R |
Resume a stopped agent in place. |
k |
Kill the selected running agent. Its session and state stay so you can resume it later. |
r |
Remove the selected agent entirely (session + meta gone). |
esc |
Close the dashboard. |
Inside an agent's transcript — a chat overlay with an always-on inline composer at the bottom. The conversation flows above it; type and enter to send a follow-up. The view auto-follows streaming output and shows an inline spinner with the agent's current activity (thinking, tool: edit_file, etc.) while it's busy. esc returns to the dashboard.
Switching the spawn model from inside the editor — while composing a task in the n-prompt, type /model on its own line and enter. The standard /model picker pops up; pick a model, the picker closes, and the editor reopens with your typed task intact and the new model pinned for the spawn.
Session scoping — each agent is stamped with the host session that spawned it and only shows up in that session's dashboard. Swap sessions with /sessions and the dashboard re-narrows accordingly. Agents from other sessions keep running in the background and reappear when you switch back.
Persistence across zot restarts — every spawn writes a meta.json next to its event log and session file under $ZOT_HOME/swarm/agents/<id>/. On the next zot launch they show up in the dashboard as detached; press R (or /swarm resume <id>) to bring one back. Startup reads only the last 4 MiB of each large event log. Opening /swarm or resuming an agent loads its full log on demand, which can briefly delay that action for very large logs. The displayed transcript still retains at most 2000 lines. Resumed agents reattach to the same session and inbox socket, so the conversation continues from where it left off.
Where state lives — everything per-agent (session file, events log, inbox socket, meta) lives under $ZOT_HOME/swarm/agents/<id>/. The agent's actual code edits land directly in your repo; track them with normal git status / git diff.
/session export does NOT bundle subagents. A .zotsession is just the main chat transcript; per-agent state (session file, unix-socket inbox) is machine-local and doesn't round-trip through a JSONL file. To share what an agent said, copy it out of the transcript view manually.
Auto-swarm. With /settings -> auto-swarm on, the main agent gets a built-in swarm_spawn tool and a system-prompt nudge to use it. It can then fork sub-agents on its own when a request naturally splits into independent parallel work ("implement A and B", "investigate three files"). Each spawn returns the sub-agent id immediately and the main turn keeps going. When every sub-agent the agent spawned in that batch finishes its initial task, zot injects one [auto-swarm update] message back into the main chat recapping each agent's status, task, and complete final response. If an agent produced no assistant response, zot includes a truncated transcript tail for diagnostics instead. The main agent then writes a short follow-up summary referencing the agents by id. Off by default; toggle from /settings.
/settings
Opens a dialog with every persistent setting. up/down to navigate, enter or space to change the selected row, esc to close (rows that open a sub-view, like model shortcuts, use esc to go back one level first). Changes are written to $ZOT_HOME/config.json and take effect on the next turn (no restart needed). Current settings:
-
render images when supported — draw screenshots / read-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from TERM_PROGRAM; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol.
-
auto-swarm — let the main agent spawn background sub-agents in parallel via a built-in swarm_spawn tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an [auto-swarm update] message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See /swarm for the dashboard that lets you monitor, message, kill, or remove the spawned agents.
-
auto-compact threshold — choose off, 70%, 80%, 85% (default), or 90% of the model's advertised context window. The selected percentage controls automatic compaction before and after interactive turns and persists as auto_compact_threshold. off disables percentage-based triggers but keeps manual /compact and automatic recovery from context-window and payload-too-large responses.
-
jail new sessions by default: start every new agent with tools confined to its working directory. Off by default. The setting applies to interactive, print, JSON, RPC, and background-agent runs, persists as jail_by_default, and immediately updates the current interactive session. /jail and /unjail remain session-scoped overrides and do not change this default.
-
PowerShell tool (Windows only): enable or disable native PowerShell alongside the default tools. Off by default; saved as powershell_enabled and applied without restarting. Explicit --tools or --no-tools takes precedence and disables this toggle for the run. Execution is still blocked while jailed and remains subject to packaged-agent permissions.
-
OpenRouter Server Tools: opt in to OpenRouter's beta web search, web fetch, advisor, subagent, fusion, image generation, datetime, and model-search tools. Off by default and available only with the OpenRouter provider. zot limits the remote loop to four server-tool calls per turn (adjustable via openrouter_server_tool_call_limit in config.json). --no-tools always disables them. Presets keep their own tool configuration and are not overridden by this setting. Server tools run remotely without zot's local tool confirmation dialog and may incur additional OpenRouter charges.
config.json example:
{
"openrouter_server_tools_enabled": true,
"openrouter_server_tool_call_limit": 4
}
-
fuzzy skill suggestions: opt in to relevance-ranked, case-insensitive subsequence matching for /skill: names. For example, /skill:review or /skill:crv can find code-review. Matching characters appear in bold. Empty queries stay alphabetical, score ties use alphabetical order, and exact names retain priority. Off by default; missing or false keeps prefix matching. Changes apply immediately and persist as fuzzy_skill_suggest in $ZOT_HOME/config.json. Other slash commands and skill invocation lookup are unchanged. See Skills.
-
compact transcript rendering: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to config.json as compact_mode.
-
timestamps: opens a sub-view with an enabled on/off toggle, an interval selector, and date display options. Show stored message times in muted text on existing separator rows, aligned with the tool box's right edge and leaving the same two-column right margin, without adding rows. Off by default, persisted as chat_timestamps. Times use the local timezone in HH:MM format, with a date on the first timestamp and when the day changes. User messages, completed assistant messages, and tool results are eligible. Missing timestamps and compaction summaries are skipped. Labels that do not fit in a narrow terminal are omitted. Timestamps appear after messages complete, not during streaming, and do not mark unread portions of a long response. Existing session timestamps are reused on resume.
-
timestamps > date display: choose Start of each day (default) to include YYYY-MM-DD on the first timestamp and when the local day changes, or Every timestamp to display YYYY-MM-DD HH:MM on every shown timestamp. Stored as chat_timestamp_date with values day_start or every_message. Missing or unknown values use day_start. This does not change which messages qualify under the interval setting.
-
timestamps > interval: minimum minutes since the last selected timestamp, default 0 (every eligible message). The first eligible message and day changes always qualify. Choose every message (0), 1, 5, 10, 15, 30, or 60 minutes in /settings > timestamps > interval, or set any nonnegative integer in chat_timestamp_interval_minutes in config.json. Missing or negative values use 0. Explicitly saved intervals are preserved. All timestamp settings apply immediately to the transcript. They affect only the interactive display, not model input or print/JSON/RPC output.
-
collapse tool call rendering: retain each tool-call header and the last non-empty rendered preview line, hiding the rest of the body. The preview may be an exit status, output line, or truncation summary; failed calls also retain an explicit error indicator. Boxed, flat, and compact layouts are preserved. ctrl+o expands the full output. Off by default. Changes apply immediately and persist to config.json as collapse_tool_call.
-
show loaded resources at startup: list the active Zotfile agent (for zot run), loaded AGENTS.md paths, extensions, and user-installed skills in compact sections above the transcript. Built-in skills are omitted. Off by default. Changes apply immediately and persist to config.json as show_instructions_at_startup.
-
TUI settings: opens a sub-view for input layout and status placement. Input style can be plain (default prompt line), lines (separator lines above and below the input), or block (a user-bubble-style input block). Status position places model, usage, and working-directory information above or below the input. Working spinner position places the busy spinner above or below the input. Changes apply immediately and persist to config.json as tui_input_style, tui_status_position, and tui_working_position (above_input or below_input for the position fields).
-
reasoning level: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The max tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to config.json and applied to the next model call. Use /reasoning to open this selector directly. The selector only shows distinct levels supported by the active model; models without reasoning support only offer off.
-
color theme — choose the built-in auto/dark/light theme or any JSON theme discovered under $ZOT_HOME/themes or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See docs/themes.md.
-
model shortcuts — opens a sub-view with nine slots (model 1 ... model 9). enter on a slot opens the same /model selector and binds the chosen provider/model to that slot; backspace clears a slot. Once assigned, press Ctrl+1 ... Ctrl+9 from the editor to switch the active model instantly (the same cross-provider swap /model performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running.
/skills
Opens a picker listing every discovered SKILL.md file, built-ins hidden. Each row shows the skill name, source, and description. enter opens the body inline (scrollable with up/down/pgup/pgdn); esc goes back. Re-runs discovery each time it opens, so edits to a SKILL.md during a session are reflected immediately.
Press p or g in the skills list to toggle project or global pins. Pinned
instructions accompany your first message in a fresh conversation and after
/clear, without starting a model turn on their own. A read-only notice above
the input shows the pending skills. Resuming does not reload pins. See
pinning skills for scope, storage, and mode support.
/compact
Sends the current transcript through the model with a structured summarization prompt. The returned summary replaces the transcript as one synthetic user message, with the last few exchanges kept verbatim for continuity. The status bar's context meter resets. Use it when the context meter creeps past ~80%. Press ctrl+o to expand the summary. Expanded text wraps to the current terminal width, including after resizing.
zot also auto-compacts in the background: after any turn that reaches the configured context threshold, the agent kicks off a condense pass on its own. Choose off, 70%, 80%, 85% (default), or 90% under /settings → auto-compact threshold. You'll see condensing history, esc to cancel above the status bar and an (auto) tag next to the context percentage; esc aborts it without touching the transcript. Turning the percentage trigger off does not disable automatic compaction and retry after a context-window or payload-too-large response.
/jail
Enforces a sandbox rooted at the cwd shown in the status bar. read, write, and edit resolve their target path (including through symlinks) and refuse anything outside the sandbox. bash refuses obvious escape patterns (sudo, rm -rf /, leading cd /, cd .., cd ~, chmod -R, dd of=/, and similar) and rejects shell arguments or redirections that point outside the sandbox. The status bar shows jailed, ~/your/cwd while active. Enable jail new sessions by default in /settings to persist this behavior across launches; /unjail then unlocks only the current session.
This is a guardrail against accidents, not a hard security boundary. If you need real isolation, run zot under docker or a proper sandbox.
Sessions
Every interactive or print/json run (unless --no-session) writes a JSONL transcript under $ZOT_HOME/sessions/<cwd-hash>/. Resume any of them with --continue, --resume, --session <path>, or interactively via /sessions inside the TUI. In interactive mode, /new persists the current conversation and starts an empty session while preserving the working directory, model, tools, extensions, and runtime settings. It does not make a model call, and the previous conversation remains available through /sessions. /new is hidden from autocomplete and /help with --no-session, and invoking it directly reports that sessions are disabled. Empty sessions (the user exited without prompting) are deleted on close so the list stays tidy.
Use zot sessions prune outside the TUI to find sessions whose recorded working directories no longer exist. The command groups sessions by directory, shows the number and human-readable total size of stored sessions, lets you select groups, and requires confirmation before permanently deleting files. It preserves sessions when a directory check fails with an error other than "not found" and rechecks each selected directory immediately before deletion. A deleted directory and a path hidden by some unmounted filesystems are indistinguishable, so review the selection and ensure remote filesystems are mounted before deleting missing-directory groups.
Use --older-than to prune sessions by last activity instead, including sessions for directories that still exist. Supported age units are m (minutes), h (hours), d (24-hour days), w (7-day weeks), mo (calendar months), and y (calendar years). Add --cwd PATH to limit age pruning to one working directory. The session file is checked again immediately before deletion and preserved if it has become active recently.
zot sessions prune --dry-run # list missing-directory groups
zot sessions prune --older-than 4h --dry-run # inspect sessions inactive for four hours
zot sessions prune --older-than 1mo --cwd ~/project # select old sessions for one directory
zot sessions prune --older-than 1y --all # select every match, then confirm
zot sessions prune --older-than 30d --all --yes # delete every match without prompting
--all selects every match and still asks for confirmation. Adding --yes makes deletion non-interactive and requires --all. Malformed, unreadable, symlinked, and non-absolute session entries are reported and preserved during missing-directory pruning. The scan includes normal sessions and named-agent sessions below $ZOT_HOME/sessions/; explicit session files stored elsewhere and swarm-agent state are not included.
Providers
zot's built-in provider catalog includes:
- Subscription-capable: Anthropic Claude Pro/Max (
anthropic), OpenAI Codex / ChatGPT Plus/Pro (openai-codex), Kimi Code (kimi), SuperGrok/X Premium (xai), GitHub Copilot (github-copilot).
- Direct API providers: Anthropic, OpenAI Chat Completions, OpenAI Responses, DeepSeek, Google Gemini, Kimi/Moonshot, Moonshot CN, Groq, Cerebras, xAI, Together AI, Hugging Face Router, OpenRouter, Mistral, Z.AI, Xiaomi/MiMo token-plan regions, MiniMax global/CN, Fireworks, Vercel AI Gateway, OpenCode/OpenCode Go.
- Cloud/platform providers: Amazon Bedrock, Google Vertex AI, Azure OpenAI, Cloudflare Workers AI, Cloudflare AI Gateway.
- Local/compatible: Ollama, llama.cpp router mode, and OpenAI-compatible local endpoints via
--base-url.
Use /login to store API keys or subscription credentials. /model only shows models from providers that are currently available from env vars, auth.json, Kimi CLI fallback, local Ollama, or a configured llama.cpp router.
Models
--list-models or the /model picker shows the full catalog across all built-in providers. Three sources:
- Catalog: models baked into zot, covering Claude, GPT/Codex, Gemini/Gemma, Kimi/Moonshot, DeepSeek, Groq-hosted Llama/Gemma/Compound, OpenRouter-routed models, Bedrock model ids, Vertex model ids, Azure OpenAI deployments, Copilot models, and other provider-specific catalog entries.
- Live: IDs discovered from the provider using your stored credentials — usually
GET /v1/models, or provider-specific listing APIs for Amazon Bedrock (see Amazon Bedrock). Cached for 6h in $ZOT_HOME/models-cache.json and refreshed in the background on startup.
- Speculative: IDs that appear in the upstream generator but aren't live on the public API yet. They'll 404 today and start working the moment the provider ships them.
The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed.
Model fallback (rescue)
When a turn fails because of a recoverable provider error — expired token (401), permission denied (403), rate limit (429), provider outage (502/503/504), or a transient network failure — zot opens a rescue picker over the chat instead of just painting a red banner.
The picker is the same vertical list / fuzzy filter UI as /model, but it only shows models from providers you're currently logged in to (env vars, auth.json, Kimi CLI fallback, ollama). The failed model is excluded. Press ↑/↓ to choose, enter to retry the same prompt on the new model, esc to dismiss.
Before the actual provider request fires, the OpenAI / Anthropic / Kimi / DeepSeek / Google / OpenAI-Codex clients also do up to two silent retries with short backoff (250ms, 750ms) on 502/503/504 and connection-reset / EOF-before-headers errors. Most edge-proxy blips disappear without you ever seeing the rescue picker.
A rescue retry always drops launch-time --api-key and --base-url before rebuilding the agent. Those overrides are usually the reason the rescue triggered (bad key, typo'd base URL, corporate gateway only valid for the originally-picked provider), so the retry re-resolves credentials from env vars / auth.json / provider defaults instead. Use /model if you want overrides to stick.
No configuration is required — the candidate list is built dynamically from your active credentials. Bad-request / context-length / serialization errors are NOT routed to the rescue picker, because switching models won't fix them; those still surface as a normal error.
Custom models
Place a models.json in $ZOT_HOME ($XDG_STATE_HOME/zot/ when set, otherwise the platform default above) to add models that aren't in the baked-in catalog or to override existing entries:
{
"providers": {
"openai": {
"models": [
{
"id": "gpt-5.5",
"name": "GPT-5.5",
"reasoning": true,
"reasoningLevelMap": {"minimum": "low", "max": ""},
"contextWindow": 400000,
"maxTokens": 128000
}
]
}
}
}
Supported fields per model: id (required), name, reasoning, reasoningLevelMap, contextWindow, maxTokens, baseUrl, priceInput, priceOutput, priceCacheRead, priceCacheWrite.
reasoningLevelMap is optional. Protocol defaults apply when it is omitted. Add only model-specific exceptions using minimum, low, medium, high, xhigh, or max as keys. Map a key to another level when both inputs are equivalent, use an identity mapping such as "max": "max" to enable a level beyond the protocol default, or map it to an empty string or off to remove it. The same effective mapping drives /reasoning and provider requests.
Provider keys are normalized: openai-codex and openai-responses map to openai, anthropic-messages maps to anthropic, moonshot, moonshot-ai, and kimi-code map to kimi, and deepseek-chat and deepseek-ai map to deepseek. Built-in provider ids such as groq, openrouter, github-copilot, amazon-bedrock, google-vertex, azure-openai-responses, fireworks, vercel-ai-gateway, mistral, and xai can also be used directly.
User-defined models show source: user in --list-models and take precedence over both the baked-in catalog and live-discovered models. Adding a models.json does not hide the built-in catalog; entries are merged on top of it. Missing or invalid files are silently ignored.
Custom providers
A top-level provider key that is not a built-in id defines a custom provider. Give it a provider-level baseUrl and an api wire format (openai for OpenAI-compatible Chat Completions, the default, openai-responses for the OpenAI Responses API, or anthropic for the Anthropic Messages API). A model-level baseUrl overrides the provider-level one for that model; an unknown api value falls back to openai with a warning.
{
"providers": {
"my-company": {
"baseUrl": "https://llm.mycompany.com/v1",
"api": "openai",
"models": [
{ "id": "company-llm-v2", "name": "Company LLM v2" }
]
}
}
}
Custom providers are first-class: they appear in --list-models, /model, and /login. models.json never stores secrets. Supply the key through /login, --api-key, or a derived environment variable in upper snake case (so my-company reads MY_COMPANY_API_KEY). Because many self-hosted gateways do not expose a model-list endpoint, custom provider keys are accepted and stored without a verification probe; an invalid key surfaces on the first model call.
To retrieve this custom provider's key from a password manager, add a matching entry to $ZOT_HOME/auth.json:
{
"additional_api_key_creds": {
"my-company": {
"api_key_command": {
"program": "op",
"args": ["read", "op://Work/OpenAI/credential"],
"timeout_ms": 120000
}
}
}
}
The provider IDs in models.json and auth.json must match. Then select the custom model directly:
zot --provider my-company --model company-llm-v2
Live model discovery for custom providers
Set "discover": true on a custom provider to list its models from the OpenAI-compatible /models endpoint instead of, or in addition to, a static models array. Discovery is off by default and requires a provider-level baseUrl.
{
"providers": {
"m4": {
"baseUrl": "http://127.0.0.1:48000/v1",
"discover": true
}
}
}
zot queries <baseUrl>/models (or <baseUrl>/v1/models when the base has no version segment) with a 3 second timeout: in the background at startup, when /model opens, and when launching with a discovered model that is not yet in the catalog. The stored or environment API key is sent as a bearer token when present; a missing key is not an error. Discovered IDs get a conservative 32K context and 4K output default, and IDs containing embed are skipped. Entries in the provider's models array override discovered metadata for the same ID. The list stays in memory only, failed refreshes keep the previous snapshot, and /model reports the error. Background refresh never executes api_key_command.
Kimi Code
zot has built-in Kimi support through the Kimi Coding endpoint and Moonshot's OpenAI-compatible chat API.
zot --provider kimi
By default this uses:
- model:
kimi-for-coding
- base URL:
https://api.kimi.com/coding/v1
Credential lookup order for Kimi:
--api-key
KIMI_API_KEY
MOONSHOT_API_KEY
$ZOT_HOME/auth.json
- the official Kimi Code CLI token at
~/.kimi/credentials/kimi-code.json, unless disabled by /logout kimi
Use /login for either API-key login or Kimi Code subscription login. The subscription flow uses Kimi Code's device-code OAuth flow: zot opens the verification URL, waits for browser approval, stores the token in auth.json, and refreshes it automatically.
For direct Moonshot API keys or a custom compatible endpoint:
zot --provider kimi --model kimi-k2-0905-preview --base-url https://api.moonshot.ai/v1 --api-key "$KIMI_API_KEY"
Kimi K3 is built in as kimi/k3, moonshotai/kimi-k3, moonshotai-cn/kimi-k3, opencode-go/kimi-k3, openrouter/moonshotai/kimi-k3, and vercel-ai-gateway/moonshotai/kimi-k3. Its output limit is 131,072 tokens on every built-in route.
You can add additional Kimi/Moonshot model IDs to models.json under the kimi provider.
xAI
xAI supports either XAI_API_KEY or /login subscription authentication. The subscription option is labeled Sign in with SuperGrok or X Premium, opens a prefilled device-authorization URL, and refreshes the stored token automatically. The default xAI model is grok-4.5, sent through the Responses API.
DeepSeek
zot has built-in DeepSeek support through DeepSeek's OpenAI-compatible chat API.
zot --provider deepseek
By default this uses:
- model:
deepseek-v4-pro
- base URL:
https://api.deepseek.com/v1
Catalog ships with deepseek-v4-pro (reasoning) and deepseek-v4-flash. These are exactly the IDs returned by GET https://api.deepseek.com/models today. You can add additional model IDs to models.json under the deepseek provider. For direct DeepSeek reasoning models, /reasoning offers off, low, high, and max. DeepSeek maps medium and xhigh to high, while max sends the distinct reasoning_effort: "max" value. Other OpenAI-compatible providers retain their own effort limits.
Credential lookup order for DeepSeek:
--api-key
DEEPSEEK_API_KEY
$ZOT_HOME/auth.json
Use /login and pick api key to paste a DeepSeek key. zot probes /v1/models once and stores the key under deepseek in auth.json.
Auth model: API key only. DeepSeek does not offer a subscription OAuth flow. The /login subscription step lists only Anthropic, OpenAI, and Kimi; DeepSeek shows up only under /login → api key.
Text only at the wire level. DeepSeek's chat-completions endpoint currently rejects the multimodal content schema (unknown variant image_url, expected text). When the active provider is deepseek, zot silently drops ImageBlock parts from outgoing user/tool messages and keeps only the text. Switching back to a vision-capable model (Claude, GPT-4o/5, Gemini) re-sends the image normally because the session file still stores it.
For a custom-compatible endpoint (mirror, gateway, self-host):
zot --provider deepseek --base-url https://my-deepseek-mirror.example.com/v1 --api-key "$DEEPSEEK_API_KEY"
Google Gemini
zot has built-in Google Gemini support through the AI Studio Generative Language API.
zot --provider google
By default this uses:
- model:
gemini-2.5-pro
- base URL:
https://generativelanguage.googleapis.com
Catalog ships with gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.0-flash, and gemini-2.0-flash-lite. Live discovery against /v1beta/models adds anything else your key can see.
Credential lookup order for Google:
--api-key
GEMINI_API_KEY
GOOGLE_API_KEY
$ZOT_HOME/auth.json
Use /login and pick api key to paste an AI Studio key. zot probes /v1beta/models once and stores the key under google in auth.json.
Auth model: API key only (this provider). Google does not issue OAuth tokens for consumer Gemini Advanced / Google One AI Premium subscriptions, so there is no "log in with your Google subscription" flow. The /login subscription step quietly downgrades to the api-key form when you pick Google so you don't end up in a dead end. If you need OAuth/service-account auth instead of an API key, use the google-vertex provider below.
Free-tier rate limits. AI Studio's free tier has tight per-minute and per-day caps that vary by model: gemini-2.5-pro is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with "You exceeded your current quota" while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or enable billing on your AI Studio project to flip the same key from free to pay-as-you-go pricing ($1.25/M input, $10/M output for Pro).
Reasoning levels (--reasoning off|minimum|low|medium|high|xhigh|max, also configurable through /reasoning or in /settings as reasoning level) map differently per generation. max is a distinct opt-in tier above xhigh. GPT-5.6, GPT-6, and adaptive-thinking Claude models receive native max; unsupported providers clamp it to their highest accepted effort. Budget-based providers retain their provider/model caps. Gemini 3.x uses the thinkingLevel enum (MINIMAL/LOW/MEDIUM/HIGH), with Gemini-3-Pro pinned to LOW minimum and HIGH for any medium-or-higher request. off sends no reasoning config except on always-on thinking models. Gemini 2.0 models have no thinking config.
Claude Opus 5.5 is available through Anthropic (--provider anthropic --model claude-opus-5-5) and GitHub Copilot (--provider github-copilot --model claude-opus-5.5, subject to account availability). Both entries support a 1,000,000-token context window and up to 128,000 output tokens. Adaptive thinking is always on, with low, medium, high, xhigh, and max effort levels. The default, off, and minimum requests use low instead. The reasoning selector omits unsupported levels. Catalog prices per million tokens are $4 input, $20 output, $0.20 cache reads, and $5 cache writes. Copilot billing remains governed by your plan.
GPT-6 Sol and GPT-6 Luna are available through OpenAI API keys, OpenAI Codex subscriptions, and GitHub Copilot. Select gpt-6-sol or gpt-6-luna with --provider openai, openai-responses, openai-codex, or github-copilot. Both use the Responses API, support up to 128,000 output tokens, and expose low through max reasoning. OpenAI and Codex entries use a 272,000-token context window, while Copilot entries use 1,000,000 tokens. Access through subscription providers depends on account availability and organization policy.
You can add additional Gemini model IDs to models.json under the google provider.
zot also has built-in support for Gemini Enterprise Agent Platform, Google's enterprise/GCP-hosted Gemini endpoint. Unlike the AI Studio google provider above, the google-vertex provider supports a Google Cloud API key plus service_account and authorized_user credential files. It does not support consumer Google login or the complete ADC credential chain, such as metadata-server and workload-identity credentials.
zot --provider google-vertex
Required configuration (env vars, read at construction time):
GOOGLE_CLOUD_PROJECT — required, your GCP project id.
GOOGLE_CLOUD_LOCATION — optional, defaults to us-central1.
Credential lookup order for Vertex:
GOOGLE_CLOUD_API_KEY: simplest option. An API key created in the GCP console is sent as x-goog-api-key, without a token exchange.
GOOGLE_APPLICATION_CREDENTIALS: path to a supported credential JSON file.
- Default ADC file: if neither of the above is set, zot checks the file written by
gcloud auth application-default login. This is ~/.config/gcloud/application_default_credentials.json on Unix systems and %APPDATA%\gcloud\application_default_credentials.json on Windows.
Two credential file shapes are supported:
type: "service_account": zot signs a JWT with the private key and exchanges it for a short-lived access token.
type: "authorized_user": zot exchanges the stored client ID, client secret, and refresh token for a short-lived access token.
Access tokens are cached in memory and refreshed on demand.
If none of these are available, zot errors with vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS.
Amazon Bedrock
zot talks to Bedrock's Converse streaming API and supports two credential styles:
- Bedrock API key (bearer token) via
AWS_BEARER_TOKEN_BEDROCK. This is the simplest setup: a single opaque string, region-scoped to wherever it was minted. It requires only the bedrock:CallWithBearerToken permission.
- SigV4 via
AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY (plus optional AWS_SESSION_TOKEN), an AWS_PROFILE, or a default / SSO / assume-role / credential_process profile resolved through the AWS CLI. SigV4 (AWS Signature Version 4) is AWS's request-signing scheme: your secret key signs each request locally and only the signature travels over the wire. zot implements it directly so it does not depend on the AWS SDK.
If both are present, the bearer token wins for inference, while live model discovery uses SigV4. The region defaults to AWS_REGION / AWS_DEFAULT_REGION, falling back to us-east-1. A 403 on a freshly-copied API key almost always means a region mismatch — set AWS_REGION to the region the key was created in, or pass --base-url https://bedrock-runtime.<region>.amazonaws.com.
Catalog vs. live discovery
Unlike most providers, Bedrock model pricing, context windows, and capability flags are hand-maintained in zot's baked-in catalog. AWS does not expose usable pricing programmatically for current models — its Price List API returns only stale, input-only rates for a few legacy models — so the catalog is authoritative for cost and metadata. This means:
- Bearer-token-only users still get the full curated Bedrock model list with correct pricing from the catalog. No AWS API listing is attempted.
- Users with SigV4 credentials additionally get live model discovery, even if inference uses a bearer token: on startup, zot lists the foundation models and cross-region inference profiles your account can actually use (via
ListFoundationModels and ListInferenceProfiles) and merges them into the catalog. New Bedrock models therefore appear in /model without waiting for a zot release. Discovered IDs already known to the catalog keep their curated pricing; genuinely new IDs show up with placeholder pricing until the catalog is updated.
Live discovery requires SigV4 because the listing APIs live on the Bedrock control plane, which rejects the runtime bearer token. It runs in the background at startup and is cached for 6h like other providers, so /model reflects the latest list without any manual refresh. If discovery fails (no SigV4 creds, expired SSO login, missing IAM permissions), zot silently falls back to the catalog. Discovery needs bedrock:ListFoundationModels and bedrock:ListInferenceProfiles.
Cross-region pricing caveat
Bedrock model IDs carry a routing prefix: global. (global cross-region inference), a geography such as us. / eu. / apac. / au. / jp. (geo cross-region inference), or none (in-region). zot's catalog prices every variant at the model's base (US) rate.
AWS charges a surcharge for geo cross-region inference in some regions — for example, Anthropic models routed through the Asia Pacific (Melbourne) geo profile cost about 10% more than the base rate. zot does not yet capture these per-region uplifts, so if you use a geo-prefixed profile in a region that charges the surcharge, zot's reported cost may be lower than your actual AWS bill. Global cross-region and plain in-region usage match the base rate and are unaffected. The context/usage accounting is still correct; only the dollar figure can under-report in this edge case. If you need exact costs, check the per-region table on the Amazon Bedrock pricing page.
Local models with ollama
zot works with ollama out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot.
Quick start:
ollama pull qwen3.5:4b
zot --provider ollama --model qwen3.5:4b
No login or API key is needed for local models. zot defaults to http://localhost:11434. The model ID must match a model installed on that Ollama server, including its tag. The server must already be running.
Save a local default
To start with just zot, create $ZOT_HOME/config.json with the following fields, or merge them into your existing configuration:
{
"provider": "ollama",
"model": "qwen3.5:4b"
}
A models.json file is not required for this setup. If you want explicit model metadata, add the following to $ZOT_HOME/models.json. This registers a model but does not change the default selection in config.json:
{
"providers": {
"ollama": {
"models": [
{
"id": "qwen3.5:4b",
"name": "Qwen 3.5 4B",
"contextWindow": 32768,
"maxTokens": 8192
}
]
}
}
}
Set contextWindow and maxTokens to limits your model and server configuration actually support. These fields inform zot's context and output budgets. They do not configure Ollama's context size or install the model.
Use a remote Ollama server
For a single run, pass the endpoint explicitly:
zot --provider ollama --model qwen3.5:4b --base-url http://ollama-server:11434/v1
For an endpoint behind authentication, use HTTPS and pass --api-key with the token. The built-in ollama provider uses that flag or a placeholder credential. It does not load a saved Ollama key from auth.json.
To persist a remote endpoint, keep the config.json selection above and add baseUrl to the model in $ZOT_HOME/models.json:
{
"providers": {
"ollama": {
"models": [
{
"id": "qwen3.5:4b",
"baseUrl": "http://ollama-server:11434/v1",
"contextWindow": 32768,
"maxTokens": 8192
}
]
}
}
}
--base-url overrides the model's endpoint for that run. baseUrl is a models.json field, not a config.json field. Only connect to trusted servers and do not expose an unauthenticated Ollama endpoint to the public internet.
Use the same model on two hosts
Models are identified by the pair (provider, model ID). Two entries with the same ID under ollama cannot represent independently selectable hosts. A different display name does not change that identity.
Instead, define a custom provider for each host in $ZOT_HOME/models.json, keeping the actual Ollama model ID unchanged:
{
"providers": {
"ollama-home": {
"baseUrl": "http://localhost:11434/v1",
"api": "openai",
"discover": true,
"models": [
{ "id": "qwen3.5:4b", "contextWindow": 32768, "maxTokens": 8192 }
]
},
"ollama-server": {
"baseUrl": "http://ollama-server:11434/v1",
"api": "openai",
"discover": true,
"models": [
{ "id": "qwen3.5:4b", "contextWindow": 32768, "maxTokens": 8192 }
]
}
}
}
Select either host explicitly:
zot --provider ollama-home --model qwen3.5:4b
zot --provider ollama-server --model qwen3.5:4b
To make the second host your default, use:
{
"provider": "ollama-server",
"model": "qwen3.5:4b"
}
Save that selection in $ZOT_HOME/config.json. Custom providers support stored or environment credentials when required. Discovery-enabled custom providers also work without a key for unauthenticated local servers.
discover lists models already installed on each server. It does not download models into Ollama. Static models entries are optional when discovery is enabled, but let you override the discovered context and output defaults. See Live model discovery for custom providers for refresh behavior and limitations.
The built-in ollama provider currently has no dedicated /login endpoint-setup flow or automatic model discovery. Use explicit model IDs, static entries, or discovery-enabled custom providers as above. The ollama provider uses the OpenAI chat completions protocol internally, so it also works with OpenAI-compatible servers such as vLLM, LM Studio, and LocalAI.
Local models with LM Studio
Register an LM Studio server through /login -> API key -> lmstudio.
Enter its server URL (for example http://localhost:1234) and an optional
API key. The URL is required and is stored in auth.json. Zot does not
probe localhost or discover LM Studio models until a server URL is registered.
An API key alone does not enable discovery.
After registration, zot refreshes the model list in the background on startup,
after saving the connection, and when opening /model. Selecting or resuming
an LM Studio model also refreshes its metadata. Choose a discovered model in
/model, or use zot --provider lmstudio --model <id> after registration.
Registration never changes your selected model or provider.
Discovery prefers /api/v1/models and falls back to /v1/models if the native
endpoint returns 404, 405, or 501. Native discovery excludes embedding models
and uses loaded instance IDs and their configured context limits when present.
Unloaded models use their model keys and advertised maximum context limits.
The fallback only provides IDs, so embedding filtering there is best-effort.
Quantization-specific keys are preserved, but zot does not expand the native
API's separate variant list into additional model choices.
Missing context metadata defaults to 32,768 tokens. Output defaults to at most
4,096 tokens (or a quarter of the context window for smaller models).
These are estimates, not server guarantees. Discovered models have zero API
pricing, and reasoning controls are not enabled automatically from capability
metadata. Token usage still comes from inference responses. Override metadata
through models.json under lmstudio when needed, including the context limit
for models that LM Studio will load with a smaller context than advertised.
User overrides take precedence. Discovery does not change metadata for other
providers or the coding-agent workflow.
Listing models does not load or download them. Inference with an unloaded
model depends on LM Studio's auto-loading settings. The discovered catalog is
in-memory only, and a failed refresh retains the previous snapshot in that
process. /model reports discovery failures without blocking other providers.
Use /logout lmstudio to remove the connection and stop discovery.
Background refresh never executes API-key commands.
Local models with llama.cpp router mode
zot can connect to a recent llama.cpp router, manage its GGUF files, and use loaded models through the router's OpenAI-compatible inference API. This is separate from Ollama. An Ollama server normally listens on port 11434 and should use zot's ollama provider instead.
Install or update llama.cpp. On macOS with Homebrew:
brew install llama.cpp
# or, when already installed
brew upgrade llama.cpp
Create a directory for GGUF files and start llama-server without --model, -m, or -hf. Supplying one of those options starts a single model rather than the router API zot needs.
mkdir -p ~/llama-models
llama-server \
--models-dir ~/llama-models \
--no-models-autoload \
--jinja \
--host 127.0.0.1 \
--port 8080 \
-ngl 999 \
-c 32768
--no-models-autoload leaves load decisions to /llama. --jinja enables model chat templates and improves tool-call compatibility. -ngl 999 requests maximum GPU offload, while -c 32768 limits each loaded model to a 32K context. Adjust these values for your hardware.
Confirm that router mode is active before configuring zot:
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/models
The models request must return JSON with a data array. A 404 usually means the server is outdated, running on a different port, or was started in single-model mode.
In zot, run /login, choose api key, and select llama.cpp. Enter http://127.0.0.1:8080 as the router URL and leave the API key empty for a local-only server. Do not enter /v1; zot derives the inference URL itself. The saved URL and optional key live in $ZOT_HOME/auth.json.
You can configure the same connection through environment variables:
export LLAMA_BASE_URL=http://127.0.0.1:8080
export LLAMA_API_KEY=optional-secret
When using a key, launch the server with the matching --api-key value. Keep --host 127.0.0.1 unless remote clients must reach the router.
Run /llama to:
- inspect the router's current model states
- search Hugging Face for GGUF repositories
- choose a quantization and download it with byte progress
- explicitly load or unload a model
- press
d to ask the router to remove a downloaded cache model after confirmation
Models discovered through --models-dir or a preset cannot be removed by zot. Delete those files from their configured source instead. The router removes the selected GGUF from its Hugging Face cache, but some llama.cpp versions retain shared repository artifacts such as mmproj files. Remove the repository's cache directory manually if those artifacts are no longer needed. Hugging Face search uses HF_TOKEN when available. Gated repositories require prior access approval, and the llama-server process also needs an authorized HF_TOKEN because the server performs the download.
Opening /model refreshes the router and lists every loaded model under provider llama.cpp. Unloaded models are intentionally omitted because they cannot answer inference requests. Load them through /llama first, then select them through /model.
A model installed through Ollama is kept in Ollama's internal storage and is not automatically available as a llama.cpp GGUF file. Download a GGUF copy through /llama or place GGUF files in ~/llama-models, then restart the router so it discovers files added manually.
Child process environment
Scripts and shell commands launched by zot inherit runtime metadata:
| Variable |
Meaning |
AI_AGENT |
Generic agent marker, set to zot |
ZOT_AGENT |
Zot-specific process marker, set to 1 |
ZOT_CHILD_SESSION |
Set to 1 for commands launched by a zot tool |
ZOT_SESSION_ID |
Current session ID, when session persistence is enabled |
ZOT_SESSION_FILE |
Absolute path to the current session file, when persisted |
ZOT_PROVIDER |
Current model provider |
ZOT_MODEL |
Current model ID |
ZOT_REASONING_LEVEL |
Current reasoning level, when configured |
These values contain no credentials. Use ZOT_AGENT or AI_AGENT to distinguish execution inside zot from a standalone invocation; use the session and model values for diagnostics or attribution.
Interactive shell commands receive model metadata even with --no-session; only session ID and file are omitted. Newly launched commands reflect session switches, working-directory changes, and reasoning-level changes. Turning reasoning off removes ZOT_REASONING_LEVEL. Already-running subprocesses retain the environment they inherited at launch.
Inline images
When a tool returns an image (for example read on a PNG), zot renders it inline on terminals that support it: Ghostty, Kitty, iTerm2, WezTerm. On other terminals you see a text placeholder with MIME type, pixel dimensions, and byte size. Control with the ZOT_INLINE_IMAGES env var:
| Value |
Effect |
| unset (default) |
Auto-detect based on TERM_PROGRAM; use the text placeholder inside VS Code and Herdr. |
iterm, iterm2 |
Force the iTerm2 OSC 1337 protocol. |
kitty |
Force the Kitty graphics protocol. |
off, none |
Always use the text placeholder. |
Herdr's Kitty graphics support is currently experimental, so zot does not enable inline images there automatically. After enabling experimental.kitty_graphics in Herdr, set ZOT_INLINE_IMAGES=kitty to opt in.
Frames containing images are full-repainted (no differential diff) to prevent stale image pixels from lingering through scroll. That costs one terminal flash per image-containing frame; set ZOT_INLINE_IMAGES=off if that bothers you.
By default each tool call (bash, read, write, edit) renders inside a bordered panel — a ┌─ header ─┐, │-prefixed body rows, and a └─┘ footer. On a screen with many calls the borders can read as busy, so zot also offers a flat mode: a single quiet header line per call (▌ bash …) with indented, border-free output. Same information — tool name, arg summary, streamed output, the ... (N more lines, ctrl+o to expand) truncation — just no frame.
Set the tool_render key in $ZOT_HOME/config.json:
{
"tool_render": "flat"
}
| Value |
Effect |
unset / "box" (default) |
Each tool call is wrapped in a bordered panel. |
"flat" |
Boxless: a quiet header line plus indented output. |
The ZOT_FLAT_TOOLS env var overrides the config for a single run, which is handy for trying it without editing the file:
| Value |
Effect |
1, true, yes, on, flat |
Force flat rendering. |
0, false, no, off, box |
Force the bordered panel. |
| unset |
Fall back to the tool_render config key. |
ZOT_FLAT_TOOLS=1 zot # flat, just this run
ZOT_FLAT_TOOLS=0 zot # boxes, even if config.json says "flat"
Either way, theme colors still drive the rendering (the header uses your accent/foreground, output uses the tool-output color) and ctrl+o still expands a truncated result.
The header line for a tool call shows the tool name plus a one-line summary of its primary argument — a path, a command, or a query. That summary is truncated to 60 cells by default (web_answer What is the best architecture to implement resilience wit...). On a wide terminal that can clip long queries more than you'd like, so set the ZOT_TOOL_ARG_WIDTH env var to raise (or lower) the limit:
ZOT_TOOL_ARG_WIDTH=120 zot # allow up to 120 cells before truncating
| Value |
Effect |
| unset (default) |
Truncate the arg summary at 60 cells. |
integer in [20, 500] |
Truncate at that many cells instead. |
| anything else |
Ignored; falls back to the 60-cell default. |
By default a message you send renders as a padded, background-tinted bubble: a blank tinted row above and below the text, with a ▌ accent bar down the left. So even a one-line prompt occupies three rows. Set compact_input to collapse it to a single quiet ▌ your text gutter line per wrapped row — no padding rows, no background tint.
{
"compact_input": true
}
| Value |
Effect |
unset / false (default) |
Padded, background-tinted user bubble. |
true |
One quiet gutter line per wrapped row. |
The ZOT_COMPACT_INPUT env var overrides the config for a single run (1/true/on/compact force compact; 0/false/off/bubble force the bubble):
ZOT_COMPACT_INPUT=1 zot # compact, just this run
Queued messages
You can keep typing while the agent is working. Pressing enter during a turn queues the message instead of interrupting: it shows up above the status bar as sliding in: <text> and is delivered as the next user turn the moment the current one finishes. Queue as many as you want; they run in order. esc cancels the active turn and drops the queue so a runaway turn doesn't flood you with stale follow-ups; ctrl+c while busy arms the exit hint instead of interrupting, a second ctrl+c within two seconds exits zot.
To recover the most recently queued message back into the editor (to tweak it before it runs), press Option+↑. In VS Code's integrated terminal that chord doesn't survive xterm.js's macOS key handling — use Option+Shift+↑ there. zot's hint line under the sliding-in queue adapts automatically based on $TERM_PROGRAM.
Slash commands also work while the agent is busy. Non-destructive ones (/help, /jump, /btw, /sessions, /skills, /reasoning, /settings, /jail, /unjail, /exit) take effect immediately. Destructive ones (/new, /clear, /compact, /login, /logout, /model, /reload-ext) cancel the active turn first and then run.
Keys (interactive mode)
| Key |
Action |
enter |
Submit (queued if the agent is busy). |
alt+enter |
Newline. |
tab |
Complete the selected slash command. |
esc |
Cancel the current turn (while busy); clear input (while idle). |
ctrl+c |
Clear the input and queue (while idle) or arm the exit hint (while busy). Press again within 2s to exit. Use esc to cancel a running turn. |
ctrl+d |
Exit on empty input. |
ctrl+l |
Redraw the screen. |
ctrl+v |
Paste clipboard text into the focused chat, side chat, dialog, filter, or credential input. In the main chat, image clipboard content is normalized to PNG and attached to the next prompt on macOS, Windows, Linux Wayland with wl-paste, and Linux X11 with xclip. Linux text paste also supports xsel; terminal-native bracketed text paste remains available without clipboard commands. |
ctrl+o |
Expand or collapse long tool results (read, write, edit, bash, glob outputs over ~12 lines). |
ctrl+1 ... ctrl+9 |
Switch to the model bound to that quick-model slot (configured in /settings -> model shortcuts). No-op while a turn is running. |
@ |
Open the file picker. Browse files and directories in the working directory. |
File picker (@)
| Key |
Action |
@ |
Open the file picker (type after a space or at the start of input). |
up, down |
Navigate the file list. |
right |
Open the selected directory. |
left |
Go back to the parent directory. |
enter |
Select the file or directory and insert it as a chip ([file:name] or [dir:name/]). |
esc |
Close the file picker. |
Type @ followed by a filter string to narrow the list (e.g. @read shows only entries containing "read"). Selected files are inserted as compact chips that expand to the full path on submit. Dragged-and-dropped files and directories also collapse to chips automatically.
Editor line navigation
| Key |
Action |
ctrl+a, ctrl+e |
Jump to start or end of line. |
alt+left, alt+right |
Jump one word back or forward. |
ctrl+u, ctrl+k |
Delete to start or end of line. |
ctrl+w, alt+backspace |
Delete the previous word. |
up, down |
Move within multi-line input. At the top edge, up recalls previous prompts and down moves forward through prompt history. |
| Key |
Action |
pgup, pgdn |
Scroll one page up or down. |
up, down (editor empty, not browsing prompt history) |
Scroll three lines up or down. This is how the mouse wheel reaches the scroll logic on most terminals. |
Extensions
zot can be extended in any language via a subprocess + JSON-RPC protocol. Extensions can register slash commands, expose tools to the model, intercept tool calls (block or rewrite args), gate whole turns before the model is called, and rewrite the assistant's visible text before it reaches the user. None are installed by default; opt in explicitly. Hot-reload any time with /reload-ext.
Install and manage
zot ext install <path|git-url> # copy / clone into $ZOT_HOME/extensions/
zot ext list # show installed extensions
zot ext doctor # diagnose load, registration, and conflict issues
zot ext logs <name> [-f] # cat or tail the extension's stderr log
zot ext enable <name> # re-enable a disabled extension
zot ext disable <name> # disable without removing
zot ext remove <name> # delete an extension directory
zot ext doctor keeps normal extension startup fail-soft, but gives
you an explicit troubleshooting view: manifest errors, disabled or
shadowed extensions, subprocess load errors, ready/auto-ready status,
registered commands/tools, registration conflicts, warnings, and the
stderr log path.
For development, point zot --ext <path> at a working directory and skip the install step entirely. Repeatable; takes precedence over installed extensions of the same name.
Updating extensions
zot update refreshes the zot binary and every installed extension that lives in a git checkout. Per-extension behaviour:
- Disabled extensions are skipped.
- Extensions without a
.git/ directory (installed by zot ext install ./local-path) are skipped — there is no remote to pull from.
- For the rest, zot stashes any dirty worktree state (including untracked runtime files like
todos.json or config.json), runs git pull --ff-only, and pops the stash. If the pop produces conflicts, the conflict markers are left in place and you'll see a warning.
- Diverged branches, offline pulls, or any other git failure are reported as
failed and the next extension is processed. zot update itself never aborts because of an extension.
- zot does not run any build step (
go build, npm install, make) after the pull. Extension authors are expected to commit the runnable artifact (binary, transpiled JS, etc.). If you need a build, rebuild manually and use /reload-ext.
Theme-only extensions
An extension may ship only a theme: extension.json plus theme.json (or themes/theme.json) and no executable. zot loads it without spawning a subprocess and shows it in /settings with source information. See docs/themes.md.
Reference
examples/extensions/ ships reference implementations in Go, TypeScript, Node, and shell. See docs/extensions.md for the full protocol, the SDK API (packages/agent/ext), and the phase roadmap.
Skills
A skill is a per-folder SKILL.md file with a YAML frontmatter header. zot discovers skills at startup, surfaces their names in the system prompt, and exposes a built-in skill tool the model uses to load the body on demand.
By default zot loads built-in skills plus user-installed skills from:
./.zot/skills/<name>/SKILL.md (project)
$ZOT_HOME/skills/<name>/SKILL.md (global)
./.claude/skills/<name>/SKILL.md, ~/.claude/skills/<name>/SKILL.md (Claude-compatible layout)
./.agents/skills/<name>/SKILL.md, ~/.agents/skills/<name>/SKILL.md (agent-compatible layout)
See docs/skills.md for the frontmatter fields, authoring tips, and example skills under examples/skills/.
Telegram bot (bridge)
zot can run as a telegram bot so you can DM it from your phone. Two ways to run it: from inside the TUI (the running session mirrors into Telegram) or as a standalone background daemon (a headless bot with its own independent agent).
From inside the TUI
Type /telegram in the running TUI to open a picker with connect, disconnect, and status. When connected:
- DMs from the paired user become prompts in the same session you're typing in, so you can continue a conversation from the terminal on your phone and back again.
- Messages you type in the TUI are mirrored into the Telegram thread prefixed
you: ... and the assistant's replies come back prefixed zot: ..., so the Telegram chat stays a complete record of both sides of the conversation.
- Messages sent from Telegram show up as your own bubble in Telegram (no mirror) and the assistant's reply to them comes back bare (no prefix).
- The status bar shows a
- tg - tag while the bridge is active.
/telegram connect / /telegram disconnect / /telegram status (or /tg) also work as direct commands without the picker.
The in-TUI bridge refuses to start while the standalone daemon (below) is running, since two concurrent long-poll consumers of the same bot race on every update and silently drop messages.
Standalone daemon
For headless servers or long-running bots unattached to a TUI:
zot telegram-bot setup # paste a BotFather token, verify, save
zot telegram-bot run # foreground: long-poll in this terminal (ctrl+c to stop)
zot telegram-bot start # background: detach and return immediately
zot telegram-bot stop # SIGTERM the background bot (SIGKILL after 5s)
zot telegram-bot logs -f # tail $ZOT_HOME/logs/bot.log (omit -f to just cat)
zot telegram-bot status # config (token masked) + running/stopped
zot telegram-bot reset # forget the token and paired user
# short alias: `zot tg ...` is accepted for every subcommand
The background flavor writes the child's PID to $ZOT_HOME/bot.pid and redirects stdout and stderr to $ZOT_HOME/logs/bot.log. zot telegram-bot stop reads that PID, sends SIGTERM, waits up to five seconds, then escalates to SIGKILL if the child is still alive. Running two instances at once is refused at startup.
Use the installed binary for start. go run ./cmd/zot telegram-bot start won't work. go run builds a binary in a temp directory and deletes it when it exits, which kills the detached child. Run make install (or go build) first and invoke the installed binary.
Setup flow:
- Talk to @BotFather on telegram, run
/newbot, copy the token it gives you.
- Run
zot telegram-bot setup and paste the token when prompted.
- Run
zot telegram-bot run in the directory you want the agent to operate in.
- Open your bot on telegram, send
/start. The first user to do this claims the bridge (stored as allowed_user_id); every other user is rejected.
From then on, any DM you send is forwarded to the agent as a user prompt. Attached photos or image/* documents are downloaded and passed to vision-capable models. In-bot telegram commands are case-insensitive: /help, /status, /stop (cancel the current turn). Config lives in $ZOT_HOME/bot.json (mode 0600).
Starting either Telegram bridge removes any webhook configured for that bot before long polling begins, while preserving pending updates. Telegram does not allow webhooks and getUpdates polling at the same time, so do not share the bot token with another service that expects to keep a webhook active.
Bot mode respects the usual zot flags: --provider, --model, --cwd, --reasoning, --continue, --no-session, --no-tools, and so on. Run zot tg run -c --model claude-opus-4-1 to resume the latest session on Opus, for example.
Architecture: protocol-agnostic bot core
The messenger functionality is split in two layers. A generic, protocol-agnostic core lives in packages/agent/modes/bot: it owns the turn queue, agent prompting, built-in command dispatch (/start, /help, /status, /stop), status formatting, and per-turn credential refresh. Concrete transports implement the small BotAdapter interface (inbound polling, sending replies, a typing indicator, and optional protocol-specific status text); the Telegram support in packages/agent/modes/telegram is one such adapter.
This means additional messaging backends (Discord, Slack, Signal, and similar) can be added by implementing BotAdapter in a new package and wiring up a subcommand. No changes to the runner, agent, or core are required. Channel IDs are opaque strings owned by the adapter, so the shared runner stays free of protocol-specific types.
Development
make build # build ./bin/zot
make test # go test -race ./...
make lint # go vet + gofmt check
make fmt # gofmt -w .
make release # cross-compile linux/darwin/windows on amd64 and arm64
Source layout (single Go module, four packages under packages/):
cmd/zot/ main()
packages/provider/ LLM client surface, model catalog, streaming clients
packages/provider/auth/ credential store, api-key probe, oauth, login server
packages/core/ agent loop, sessions, cost tracking, compaction
packages/tui/ terminal raw-mode, input parser, editor, renderer, markdown, view
packages/agent/ cli wiring, arg parsing, system prompt, config
packages/agent/extensions/ extension subprocess manager
packages/agent/extproto/ extension wire-format types
packages/agent/modes/ interactive tui, print, json, dialogs
packages/agent/modes/bot/ protocol-agnostic bot runner (BotAdapter interface)
packages/agent/modes/telegram/ telegram adapter, api client, daemon
packages/agent/tools/ read, write, edit, bash, glob, sandbox
packages/agent/skills/ skill discovery, frontmatter parser, skill tool
packages/agent/swarm/ background subagent runtime
packages/agent/sdk/ public Go SDK for embedding zot in-process (package sdk)
packages/agent/ext/ public Go SDK for writing extensions (package ext)
Downstream consumers can depend on individual packages:
go get github.com/patriceckhart/zot/packages/core pulls only core and its transitive deps (today: provider), no agent or TUI code.
License
MIT
source
1<div align="center">
2 <a href="https://www.zot.sh">
3 <img src="packages/provider/auth/assets/zot-logo.png" alt="zot coding agent harness" width="130" height="130" />
4 </a>
5</div>
6<br>
7<p align="center">
8 <a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-blue"></a>
9 <img alt="Go 1.25+" src="https://img.shields.io/badge/Go-1.25+-00ADD8?logo=go&logoColor=white">
10 <img alt="30+ providers" src="https://img.shields.io/badge/providers-30+-34E2EA">
11</p>
12<p align="center">
13 <a href="https://www.zot.sh">zot.sh</a>
14</p>
15
16## What is it?
17
18Yet another coding agent harness, lightweight and written (vibe-slopped) in go.
19
20- one static binary.
21- built-in providers for Anthropic, OpenAI/Codex/Responses, Kimi, DeepSeek, Google Gemini/Vertex, GitHub Copilot, Bedrock, Azure OpenAI, OpenRouter, Yolo-Auto, Groq, Cerebras, xAI, Together, Hugging Face, Mistral, Moonshot, Z.AI, Xiaomi, MiniMax, Fireworks, Vercel AI Gateway, OpenCode, Cloudflare AI, and Ollama/local models.
22- built-in tools (read, write, edit, bash, glob).
23- three run modes (interactive tui, print, json).
24- built-in telegram bot.
25- extensions in any language via subprocess + json-rpc. None installed by default; opt in with `zot ext install` or `zot --ext`. See [docs/extensions.md](docs/extensions.md).
26- user and extension themes via JSON; see [docs/themes.md](docs/themes.md).
27- standing instructions via `AGENTS.md` files (global and per-project); see [Persistent instructions](#persistent-instructions-agentsmd).
28- reusable instructions via `SKILL.md` files; see [docs/skills.md](docs/skills.md).
29- portable agents from local directories, `.zot` files, or temporary public GitHub downloads; see [docs/zotfiles.md](docs/zotfiles.md).
30
31## Install
32
33### One-liner (macOS, Linux)
34
35```bash
36curl -fsSL https://www.zot.sh/install.sh | bash
37```
38
39Detects your OS and architecture, downloads the latest release from GitHub, verifies the SHA-256 against the release's `checksums.txt`, extracts the binary, and drops it in `/usr/local/bin`, `~/.local/bin`, or `~/bin`, whichever is writable first. Pass a version or prefix to pin:
40
41```bash
42curl -fsSL https://www.zot.sh/install.sh | bash -s -- v0.0.1 ~/bin
43```
44
45### One-liner (Windows, PowerShell)
46
47```powershell
48iwr -useb https://www.zot.sh/install.ps1 | iex
49```
50
51Drops `zot.exe` into `$HOME\bin` and adds it to the user PATH if missing. Open a fresh terminal afterwards.
52
53### go install
54
55```bash
56go install github.com/patriceckhart/zot/cmd/zot@latest
57```
58
59The installed binary reports the tagged module version and supports `zot update`.
60
61### From source
62
63```bash
64git clone https://github.com/patriceckhart/zot
65cd zot
66make build # produces ./bin/zot
67make install # into $GOPATH/bin
68```
69
70### Prebuilt binaries
71
72Every release on the [releases page](https://github.com/patriceckhart/zot/releases) ships archives for Linux, macOS, and Windows on amd64 and arm64 (except windows/arm64), plus a `checksums.txt` file. Download, verify, `chmod +x`, and drop on your `$PATH`.
73
74## Authenticate
75
76The easiest way is to just run `zot` and type `/login`. The TUI opens even without credentials and walks you through a browser-based login flow.
77
78### Credential lookup order
79
801. `--api-key` flag
812. provider-specific env var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `KIMI_API_KEY`, `MOONSHOT_API_KEY`, `DEEPSEEK_API_KEY`, `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `GROQ_API_KEY`, `OPENROUTER_API_KEY`, `MISTRAL_API_KEY`, `XAI_API_KEY`, `CEREBRAS_API_KEY`, `TOGETHER_API_KEY`, `HF_TOKEN`, `ZAI_API_KEY`, `XIAOMI_API_KEY`, `MINIMAX_API_KEY`, `FIREWORKS_API_KEY`, `AI_GATEWAY_API_KEY`, `COPILOT_GITHUB_TOKEN`, `GITHUB_COPILOT_TOKEN`, and others for provider-specific backends)
823. `$ZOT_HOME/auth.json` (API key or OAuth token; mode 0600)
83
84`$ZOT_HOME` defaults to:
85- All platforms: `$XDG_STATE_HOME/zot` when `XDG_STATE_HOME` is set
86- macOS fallback: `~/Library/Application Support/zot`
87- Linux fallback: `~/.local/state/zot`
88- Windows fallback: `%LOCALAPPDATA%\zot`
89
90### API keys from commands
91
92To keep an API key in a password manager instead of `auth.json`, configure an `api_key_command` for the provider:
93
94```json
95{
96 "anthropic": {
97 "api_key_command": {
98 "program": "op",
99 "args": ["read", "op://Work/Anthropic/credential"],
100 "timeout_ms": 120000
101 }
102 }
103}
104```
105
106For a provider added through `models.json`, put the same credential object under `additional_api_key_creds` using its provider ID. `program` is executed directly, without a shell, so each argument must be a separate `args` entry. `timeout_ms` is optional and defaults to 120 seconds.
107
108zot runs the command only when that provider is selected, not while checking login status or refreshing model catalogs in the background. Successful output is cached in memory for the rest of the zot process and is never written to disk. The command must print one non-empty line to stdout; zot removes trailing CR/LF characters, limits output to 64 KiB, and does not include command output in errors. Saving a normal key through `/login` replaces the command configuration, and `/logout` removes it.
109
110Treat `auth.json` as executable configuration: anyone who can modify it can cause zot to run a program under your user account. zot does not interpret `!` prefixes or execute command strings through a shell.
111
112### `/login` flow
113
114Run `zot` and type `/login`. Pick one of two methods:
115
116- **API key**: a small local web server starts on `127.0.0.1:<free-port>`, your browser opens a form, you pick a provider from the full API-key provider list, paste the key, and zot saves it to `auth.json` if accepted. Providers with a lightweight model-list endpoint are probed before saving; provider backends that need extra project/account env vars are saved directly.
117- **Subscription**: use your Claude Pro/Max, ChatGPT Plus/Pro, Kimi Code, SuperGrok/X Premium, or GitHub Copilot subscription. DeepSeek and Google Gemini do **not** have a subscription login path. For those, use the API-key flow.
118 - For Anthropic, choose browser callback (`localhost:53692`) or copy code before starting login. Browser callback also accepts a pasted code or full redirect URL from the same authorization link. Copy code uses Anthropic's hosted redirect and works over SSH or on headless machines without a local callback server. Anthropic uses the Claude Code OAuth flow. Messages go to `api.anthropic.com` with a bearer token and the Claude Code identity headers.
119 - OpenAI uses the Codex CLI OAuth flow with a browser callback on `localhost:1455`. The same authorization link also accepts a pasted code or full redirect URL if the browser cannot reach zot's callback port (for example, over SSH). Copy the final redirect URL from the browser address bar, even if the page fails to load locally. The callback port must still be available on the machine running zot. Messages go to `chatgpt.com/backend-api/codex/responses` with the `chatgpt-account-id` extracted from the returned id_token.
120 - Kimi uses the Kimi Code device-code OAuth flow. zot opens the verification URL, polls until you approve it in the browser, then sends messages to `api.kimi.com/coding/v1` with the Kimi Code identity headers.
121 - xAI uses a device-code OAuth flow. zot opens a prefilled authorization URL, polls for approval, and uses the resulting token with the xAI API.
122 - GitHub Copilot uses GitHub's device-code login flow. zot stores the GitHub access token and exchanges it for short-lived Copilot inference tokens on demand.
123
124> **Note on subscription login.** The OAuth client IDs used are the ones published in Anthropic's Claude Code CLI, OpenAI's Codex CLI, Kimi Code CLI, xAI's device flow, and GitHub Copilot's device-code flow. Reusing them from a third-party tool may be against their terms of service and may be revoked at any time. Use it at your own risk; the API-key flow is the safe default.
125
126### Token refresh
127
128OAuth access tokens are short-lived (Anthropic ~8h, OpenAI ~30d; Kimi, xAI, and GitHub Copilot also use refresh/exchange flows). zot refreshes or exchanges them automatically:
129
130- At every credential lookup, zot checks the stored `expiry` and, if past it (with a 60s safety margin), hits the provider's `oauth/token` endpoint with the stored `refresh_token`, persists the new `access_token`, `refresh_token`, and `expiry` back to `auth.json`, and hands the fresh token to the client.
131- The telegram bridge additionally refreshes once per turn so a bot that runs for days keeps working without manual intervention.
132- If the refresh itself fails (the `refresh_token` was revoked, or the account was logged out everywhere), the error bubbles up to the caller: the TUI shows it in the status line, the bot replies with it in your DM. Run `/login` to get a fresh token pair.
133
134All data lives under `$ZOT_HOME`:
135
136```
137$ZOT_HOME/
138├── config.json # last-used provider/model/theme, saved automatically
139├── models.json # optional: user-defined models and provider endpoints
140├── auth.json # api keys and oauth tokens (mode 0600)
141├── sessions/ # jsonl transcripts, one dir per cwd
142├── models-cache.json # live /v1/models discovery cache (6h ttl)
143├── AGENTS.md # optional: global instructions appended to the prompt
144├── SYSTEM.md # optional: replaces the default system prompt
145├── skills/ # optional: user SKILL.md files
146├── themes/ # optional: user theme JSON files
147├── extensions/ # installed extensions, one dir per extension
148└── logs/ # app log files
149```
150
151Drop a `SYSTEM.md` in `$ZOT_HOME` to replace the built-in identity and zot-docs guidance for every run. `--system-prompt` still wins per-invocation. Pass an empty value (`--system-prompt ""`) to intentionally omit the built-in identity. Custom prompts still receive appended instructions and generated context, including `AGENTS.md`, skills, auto-swarm guidance when enabled, and the date/cwd footer. Delete the file to revert to the default.
152
153### Configuration files
154
155zot keeps both configuration and runtime state under `$ZOT_HOME`, even when that directory is under the platform's state directory. You can edit configuration files there directly while zot is not running:
156
157- `config.json` selects the default `provider` and `model` and stores UI preferences. zot also updates it when you change settings or select a model interactively. Explicit `--provider` and `--model` flags override the saved selection for a run.
158- `models.json` optionally defines model metadata and provider endpoints (`baseUrl`). Adding a model here does not select it as the default. See [Custom models](#custom-models).
159- `auth.json` stores credentials, not model definitions. Prefer `/login` for supported providers, and keep secrets out of `config.json` and `models.json`.
160
161Neither `config.json` nor `models.json` is required to start zot with an explicit provider and model. Without a saved or explicit provider, zot defaults to Anthropic and may offer login if no usable credentials are available. Ollama needs no login. See [Local models with ollama](#local-models-with-ollama) for minimal configuration and remote-server examples.
162
163### HTTP proxy
164
165To route zot-managed HTTP and HTTPS requests through one proxy, add `http_proxy` to `$ZOT_HOME/config.json`:
166
167```json
168{
169 "http_proxy": "http://127.0.0.1:7890"
170}
171```
172
173The setting is applied at startup to both HTTP and HTTPS traffic. Existing `HTTP_PROXY`, `HTTPS_PROXY`, `http_proxy`, and `https_proxy` environment variables take precedence for their corresponding protocol. `NO_PROXY` and `no_proxy` continue to control bypasses. Restart zot after changing the config file. If the URL contains proxy credentials, prefer protected environment variables because `config.json` is not a credential store.
174
175### Configurable shortcuts
176
177Interactive keybindings can be added to `$ZOT_HOME/config.json` under `keymap`. Each key is a case-insensitive chord made of at least one modifier (`ctrl`, `alt`/`option`, `shift`, `cmd`/`super`) plus a single character or a named key (`enter`, `tab`, `space`, `esc`, `backspace`, `delete`, `up`, `down`, `left`, `right`, `home`, `end`, `pageup`, `pagedown`). Each value is a slash command: built-in commands, extension commands, and explicitly invoked `/skill:<name>` all work.
178
179```json
180{
181 "keymap": {
182 "ctrl+s": "/settings",
183 "ctrl+g": "/git-tools:status",
184 "alt+r": "/skill:code-review"
185 }
186}
187```
188
189Shortcuts run in the main interactive input and use the same dispatch path as
190typing the command. They do not apply while a dialog, extension panel, or
191tool confirmation has focus. Entries that cannot be bound are skipped and
192reported at startup (the message clears with `/clear`): unmodified keys such
193as `enter` or `g`, values that do not start with `/`, and the reserved
194`ctrl+c`, `ctrl+d`, and `esc` chords. `/help` lists the active bindings.
195
196Terminal limitations: without enhanced keyboard reporting (kitty keyboard
197protocol or xterm `modifyOtherKeys`), a terminal cannot distinguish
198`ctrl+h`, `ctrl+i`, and `ctrl+m` from Backspace, Tab, and Enter, and it does
199not report Shift on Ctrl+letter chords. zot therefore lets a `ctrl+shift+<letter>`
200binding also fire on plain `ctrl+<letter>` unless a separate `ctrl+<letter>`
201binding exists. Chords the terminal emulator or multiplexer consumes itself
202(a tmux prefix, `cmd+k` clearing scrollback, tab switching) never reach zot.
203
204## Persistent instructions (AGENTS.md)
205
206Use `AGENTS.md` to give zot standing instructions that layer **on top of** the default system prompt, without replacing it. This is the friendliest way to shape behavior (for example, taming local models that jump straight to code edits) because it adds guidance rather than taking over the base identity the way `SYSTEM.md` does.
207
208zot discovers `AGENTS.md` files automatically at startup and loads them in this order:
209
2101. `$ZOT_HOME/AGENTS.md` (global, machine-wide instructions that apply to every project).
2112. Every `AGENTS.md` from the filesystem root down to the current working directory. More specific (deeper) files may override earlier ones. This includes `~/AGENTS.md` when the working directory is inside your home directory.
212
213All discovered files are appended to the prompt in that order, so a global baseline can be refined per project. To list the active Zotfile agent, loaded context paths, extensions, and user-installed skills above the interactive transcript, enable **show loaded resources at startup** in `/settings`. The agent section appears only for `zot run`, the setting is off by default, built-in skills are omitted, and the displayed lists are not sent to the model or stored in the session transcript.
214
215For a general, non-project-specific instruction set, put your rules in `$ZOT_HOME/AGENTS.md`, for example:
216
217```markdown
218Treat questions and discussions as requests for explanation. Do not edit files or run tools unless explicitly asked to make a change. Ask before modifying code.
219```
220
221`AGENTS.md` vs the other mechanisms:
222
223| Mechanism | Scope | Effect |
224|---|---|---|
225| `$ZOT_HOME/AGENTS.md` | global, every run | appended to the default prompt |
226| `./AGENTS.md` (and parent dirs) | project | appended to the default prompt |
227| `$ZOT_HOME/SYSTEM.md` | global, every run | replaces the built-in identity and zot-docs guidance |
228| `--append-system-prompt <text>` | single run | appends for one invocation (repeatable) |
229| `--system-prompt <text>` | single run | replaces the built-in identity and zot-docs guidance |
230
231> **Note:** zot does not read a `CLAUDE.md` instruction file. The only Claude-compatible thing it picks up is skills under `.claude/skills/`. If you are migrating from Claude Code, move that content into `AGENTS.md` (global or per-project) and zot will use it.
232
233## Changelog on update
234
235The first time you launch a newer zot binary, the TUI shows the GitHub release notes once in a dismissible overlay. Press any key to close. The version is recorded in `config.json`'s `last_changelog_shown` so the same release notes never reappear. Fresh installs don't see a changelog (no upgrade has happened yet). The fetch is best-effort: a network failure or a missing release page silently skips, with another attempt on the next launch.
236
237## Usage
238
239```bash
240zot # interactive tui
241zot "fix the failing test" # tui, pre-filled prompt
242zot -p "list all go files" # print final text, exit
243echo "list all go files" | zot # piped stdin implies print mode
244cat README.md | zot -p "summarize this text" # combine stdin with the prompt
245zot -p --stats stats.json "task" # print final text and write generation stats
246zot --json "refactor main.go" # newline-delimited json events, exit
247zot --continue # resume the most recent session for this cwd
248zot --resume # pick a session to resume
249zot --list-models # show supported models
250zot --help
251```
252
253Print-mode stats contain `provider`, `model`, `prompt_tokens`, `reasoning_tokens`, `generated_output_tokens`, and `elapsed_ms`. Counts cover all model turns triggered by the prompt, including tool loops. Prompt tokens include cache reads and writes. `reasoning_tokens` is `null` when the provider does not report a separate count; in that case `generated_output_tokens` is the provider's total output count and may include reasoning. Elapsed time covers the agent run, not startup and credential resolution. The file is written only after a successful run.
254
255## Flags
256
257| Flag | Description |
258|---|---|
259| `--provider <id>` | Pick the provider (for example `anthropic`, `openai`, `openai-codex`, `kimi`, `google`, `github-copilot`, `groq`, `openrouter`, `amazon-bedrock`, `ollama`; see Providers). |
260| `--model <id>` | Pick the model (see `--list-models`). |
261| `--api-key <key>` | Override the API key. |
262| `--base-url <url>` | Override the provider base URL (tests, self-hosted). |
263| `--insecure` | Skip TLS certificate verification for the explicit `--base-url` endpoint or a `baseUrl` defined for a user model in `models.json` (self-signed local/internal inference servers). Built-in providers, auth, and model discovery keep normal TLS verification. |
264| `--system-prompt <text>` | Replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`; pass `""` for no built-in identity). |
265| `--append-system-prompt <text>` | Append text to the system prompt (repeatable). |
266| `--reasoning off\|minimum\|low\|medium\|high\|xhigh\|max` | Set the reasoning level on supported models (default: off). `max` is a separate opt-in tier above `xhigh`. |
267| `--stats <path>` | With `-p`/`--print`, write generation statistics as JSON. |
268| `-c`, `--continue` | Resume the latest session for this cwd. |
269| `-r`, `--resume` | Pick a session to resume. |
270| `--session <path>` | Resume a specific session file. |
271| `--no-session` | Don't read or write session files. |
272| `--cwd <path>` | Use `<path>` as the working directory. |
273| `--no-tools` | Disable all tools. |
274| `--tools <csv>` | Only enable the listed tools. |
275| `--max-steps <n>` | Cap agent loop iterations (default 50). |
276| `-e`, `--ext <path>` | Load an extension from `<path>` for this run (repeatable; wins against installed extensions of the same name). |
277| `--no-ext` | Skip extension discovery for this run. `--ext` still works on top, so `--no-ext --ext ./x` runs only `x`. |
278| `--no-skill` | Disable all skills, including built-ins. No `skill` tool is registered and the system prompt has no skill manifest. |
279| `--no-context-files`, `-nc` | Disable discovery and loading of global and project `AGENTS.md` files. |
280| `--no-yolo` | Confirm every tool call before it runs (interactive TUI only). A dialog shows the tool name and a one-line preview of its args, while edit calls show the proposed diff in the tool panel, with four choices: yes, yes-always-this-tool-this-session, yes-always-this-session, no. Press `/` while the dialog is focused to run a slash command first; closing its input or child dialog returns to the pending confirmation. Ignored with a stderr warning in print / json / rpc modes, where tools still run freely so scripts and automation keep working. |
281
282## Tools
283
284- `read`: read text files, or inline images (PNG, JPEG, GIF, WebP).
285- `write`: create or overwrite files, making parent directories as needed.
286- `edit`: one or more exact-match replacements in an existing file.
287- `bash`: run a command in the session cwd with merged stdout/stderr and a timeout. On Unix, zot uses `/bin/bash -c` when available, then `bash -c` from `PATH`, and falls back to POSIX `/bin/sh -c` when Bash is unavailable. On Windows, it uses `cmd /C`. macOS ships Bash 3.2 by default, so newer Bash features may be unavailable.
288
289When the sandbox is on (see `/jail`), filesystem tools refuse paths outside the session cwd and `bash` applies best-effort command checks. Jail mode is an accident-prevention guardrail, not a security boundary.
290
291### PowerShell tool (Windows)
292
293The optional `powershell` tool executes native PowerShell commands in the session cwd. On Windows, open `/settings` and toggle **PowerShell tool** to enable or disable it alongside the default tools. It is off by default, persists as `powershell_enabled` in `$ZOT_HOME/config.json`, and updates the live tool list without a restart. The saved preference also applies to other modes on Windows.
294
295For a one-run tool selection, enable it instead of `bash`:
296
297```powershell
298zot --tools read,write,edit,glob,powershell
299```
300
301Include `bash` in that list to enable both. Explicit `--tools` and `--no-tools` override the saved preference and disable the settings toggle for that run. The toggle is hidden on non-Windows platforms. `!` shell escapes remain unchanged (`cmd /C` on Windows).
302
303PowerShell execution prefers `pwsh.exe` on `PATH`, falling back to `powershell.exe`. It uses `-NoProfile -NonInteractive -ExecutionPolicy Bypass`, with UTF-8 output and encoded command transport to preserve Unicode and quoting. Administrator-enforced execution policies can still take precedence. Missing executables and non-Windows execution return explicit errors.
304
305The tool accepts `command` and an optional `timeout` in seconds, with merged streaming stdout/stderr, exit status, and the same output limits as `bash`. On Windows, cancellation terminates the shell and bounds output draining, but does not guarantee termination of descendant processes.
306
307PowerShell is blocked while `/jail` is active because the existing shell checks do not parse PowerShell syntax. Packaged agents require `permissions.bash.mode: "ask"` (launch-time consent); `none`, `allowlist`, and unknown modes deny PowerShell execution. Bash allowlists are not applied to PowerShell.
308
309## Modes
310
311- **Interactive** (default): chat TUI with streaming output, spinner, cost meter, slash commands.
312- **Print**: `zot -p "prompt"` runs the agent to completion and writes only the final assistant text to stdout.
313- **Stream**: `zot --stream "prompt"` runs without the TUI and writes assistant text to stdout as it arrives. Tool activity goes to stderr.
314- **Piped input**: when no mode is specified, piped stdin selects print mode. Print, stream, and JSON modes prepend piped stdin to the positional prompt, separated by a newline. For example, `echo "list all go files" | zot` or `cat README.md | zot -p "summarize this text"`.
315- **JSON**: `zot --json "prompt"` emits one JSON object per agent event to stdout, newline-delimited. The schema is documented in [docs/rpc.md](docs/rpc.md).
316- **RPC**: `zot rpc` runs as a long-lived child process; commands in on stdin, events and responses out on stdout, both as NDJSON. Designed for embedding zot in third-party apps written in any language. See [docs/rpc.md](docs/rpc.md) for the wire schema and `examples/rpc/{python,node,shell,go}` for working clients.
317
318## zotfile agents
319
320A zotfile packages an agent's instructions, skills, requirements, and enforced tool permissions as a shareable agent. Run one from a local directory, a packed `.zot` artifact, a short name, or directly from a public GitHub repository:
321
322```bash
323zot run ./my-agent
324zot run ./my-agent.zot
325zot run frkr/zot-archify
326zot run acme/agents/code-reviewer --cwd /path/to/project
327```
328
329Single-part names resolve only to a matching local directory or `.zot` archive. Anyone can publish an agent as a public GitHub repository and run it as `owner/repository`, or publish a collection and run an agent directory as `owner/repository/agent`. zot has no built-in owner, official collection, configured registry, or allowlist. For GitHub sources, zot downloads the repository archive into a temporary directory, validates and runs the selected agent, then removes the downloaded source when the command exits. Agent data, consent receipts, and sessions still persist under `$ZOT_HOME`. See [docs/zotfiles.md](docs/zotfiles.md) for authoring, permissions, packaging, and current limitations.
330
331## Embedding
332
333Two ways to drive zot from another program:
334
335- **Go in-process**: import `github.com/patriceckhart/zot/packages/agent/sdk`. One `Runtime` per project; `Prompt(ctx, text, images)` returns a channel of `Event`. Small example in `examples/sdk/`.
336- **Any language, out-of-process**: spawn `zot rpc` as a subprocess and exchange newline-delimited JSON over its stdin/stdout. Wire format and event schema in [docs/rpc.md](docs/rpc.md). Reference clients live under `examples/rpc/`.
337
338Both interfaces share the same event schema, so transcripts captured by one can be replayed through the other.
339
340## Slash commands
341
342Slash command names are case-insensitive in the TUI and messaging backends; arguments keep their original case. Type `/` in the TUI to open the autocomplete popup. Available commands:
343
344| Command | Description |
345|---|---|
346| `/help` | Show key bindings and commands. |
347| `/login` | Log in via API key or subscription (opens a dialog). |
348| `/logout [provider]` | Clear credentials for any logged-in provider, or all when omitted. `/logout openai-codex` clears ChatGPT/Codex subscription auth while preserving a public OpenAI API key; `/logout kimi` also disables fallback to the official Kimi Code CLI token until you log in to Kimi through zot again. |
349| `/model` | Pick a model from a list (or `/model <id>` to set directly). |
350| `/reasoning` | Set the reasoning level for subsequent model calls. |
351| `/llama` | Connect to the configured llama.cpp router, load, unload, or remove cached models, and search/download GGUF models from Hugging Face with live progress. Shown after llama.cpp login is configured. |
352| `/sessions` | Resume a previous session for this directory. |
353| `/new` | Persist the current conversation and start a fresh session in the same directory. Unavailable with `--no-session`. |
354| `/session` | Five ops on the current session: inspect its `timeline`, `export` to a portable `.zotsession` file, `import` one back in, `fork` from a past user message, or view its branch `tree`. Opens a picker without an argument; direct forms include `/session timeline`, `/session export [path]`, `/session import <path>`, `/session fork`, and `/session tree`. Default export destination is `~/Downloads`. |
355| `/jump` | Scroll the chat to a previous turn (or `/jump <text>` to filter). |
356| `/btw` | Side chat with full context that doesn't add to the main thread. |
357| `/swarm` | Spawn, monitor, and chat with background subagents. Each runs in parallel with your main session and shares its working directory. |
358| `/skills` | List discovered skills (SKILL.md files) and preview their bodies. |
359| `/compact` | Summarize the transcript into one message to free up context. |
360| `/study` | Run the canned prompt "Read and understand everything in the current directory." so the agent has full project context before you start asking targeted questions. Pass a path — typed, drag-dropped, or selected via `@` — to target a specific file or directory instead: `/study [dir:packages/]`, `/study cmd/zot/main.go`. |
361| `/jail` | Confine tools to the current directory. |
362| `/unjail` | Allow tools to touch paths outside again. |
363| `/reload-ext` | Hot-reload all extensions (re-read manifests, respawn subprocesses, rebuild tool registry). |
364| `/telegram` | Connect, disconnect, or show status of the Telegram bridge (takes `connect` / `disconnect` / `status` as an optional argument; opens a picker without one). When connected, DMs from the paired user become prompts in the running session and the assistant's replies are mirrored back to Telegram. Alias: `/tg`. |
365| `/settings` | Change persistent settings, including inline images, auto-swarm, and the auto-compact threshold. Saved to `$ZOT_HOME/config.json`; takes effect immediately. |
366| `/clear` | Clear the chat transcript. |
367| `/exit` | Exit zot. |
368
369Extension-registered commands appear under a divider at the bottom of the popup, sorted by name.
370
371### Shell escape (`!command`)
372
373Type `!` followed by a command to run it directly without going through the model. Everything after the `!` is passed to the same shell the `bash` tool uses (`/bin/bash -c` when available on Unix, then `bash -c` from `PATH`, with POSIX `/bin/sh -c` as a fallback; `cmd /C` on Windows), runs in the session working directory, and honors the `/jail` sandbox. The command, merged output, and exit status are appended to the transcript as user context, so the model can use them on the next turn. Running the command does not itself start a model turn. A running `!command` shares the busy state with the agent: `esc` cancels it, and you cannot start one while a turn (or another shell escape) is in flight.
374
375After a shell command finishes, `up` / `down` input history recalls it with the leading `!`, without its output or exit status. This also works when resuming sessions saved with command metadata. Shell commands from older sessions without that metadata remain excluded from input history.
376
377### `/sessions`
378
379Shows previous sessions for the current working directory, newest first, with timestamp, model, message count, cost, and the first user prompt. Pick one with `up`/`down`, `enter` to resume, `r` to rename, `d` to delete, or `esc` to cancel. Deletion requires confirmation and permanently removes the session file; the currently active session cannot be deleted. zot swaps the current session file for the selected one and replays the full transcript (including tool calls) into the agent. Sessions remember the model they ended on, so resuming picks up on that exact model even if your global default changed.
380
381### `/session`
382
383Five ops on the current session. `/session` alone opens a picker; each is also runnable directly.
384
385- **`/session timeline`**. Replaces the chat area with a read-only timeline of the current system prompt, user and assistant messages, and tool calls. The header shows provider-reported context usage and an estimated split between system prompt, tool definitions, and messages. The split uses a simple byte estimate, so it is directional rather than tokenizer-exact. Use `up` / `down` or `pgup` / `pgdn` to select an event, `tab` / `shift+tab` to inspect its summary, payload, result, schema, and transcript-derived timing, and `/` to search. `ctrl+e` writes a JSON export to `~/Downloads` (falling back to the home directory) with `0600` permissions on platforms that support Unix file modes. Images are represented by MIME type and byte size instead of embedding their data. Press `esc` to return to chat.
386- **`/session export [path]`**. Writes the running transcript to a portable `.zotsession` file. Default destination is `~/Downloads/<timestamp>-<session-id>-<prompt-slug>.zotsession`. Pass a path to override; a directory is fine (a dated name is built inside), a bare name gets `.zotsession` appended. The meta's cwd is stripped on the way out so the recipient doesn't see your filesystem layout.
387
388 **What's included.** Only the main chat thread of the running session — messages, tool calls, tool results, compactions, and usage. **`/swarm` subagents are NOT included.** Their transcripts, unix-socket inboxes, and per-agent session files are all machine-local; a `.zotsession` is just a chat transcript and has no way to revive a unix socket on another box. If you want the conversation, copy it out of the dashboard manually.
389- **`/session import <path>`**. Copies a `.zotsession` file into `$ZOT_HOME/sessions/<cwd-hash>/` with a fresh id and the current cwd, then switches the running agent onto it. Imported sessions are first-class: they show up in `/sessions`, `/jump`, and the tree. Drag-drop paths in the editor are accepted (zot strips the surrounding quotes automatically).
390- **`/session fork`**. Opens a turn picker (same shape as `/jump`). Pick any past user message; zot copies every message up to and including that turn into a new session, records `parent` + `fork_point` in the new meta, and switches onto the branch. The parent session stays on disk. Use it to try a different question without polluting the original transcript, or to rewind after the agent went down the wrong path.
391- **`/session tree`**. Shows every session in the current cwd arranged by parent/child relationships, depth-first with indent per level. The current session is tagged `[current]`. Pick any entry to switch into it. Parentless sessions are roots; branches created via `/session fork` nest under whichever session they were forked from. Orphaned children (whose parent file was deleted) still show as roots so they stay discoverable.
392
393### `/jump`
394
395Opens a turn picker for the current session, one row per user prompt, each showing the turn number, how many tools that turn invoked, and the first line of the prompt. `up`/`down` to pick, `enter` to jump, `esc` to cancel. Any printable rune while the picker is open extends a filter; backspace narrows it back. `/jump <text>` pre-applies the filter; if exactly one turn matches, zot jumps straight there without showing the picker.
396
397Jumping is non-destructive. The transcript and future model context are untouched. Zot opens a temporary history view with the chosen turn at or near the top and a `viewing turn N of M` indicator. Use `pgup` and `pgdn` to browse. Reaching the bottom restores the live view, including output received while browsing. History browsing uses the terminal's alternate screen rather than trying to reposition native scrollback. The normal live view still uses native terminal scrollback. Use `/session fork` instead if you want to branch from an earlier turn.
398
399### `/btw`
400
401Opens a side-chat overlay with the full main session as frozen context, so you can ask quick clarifying questions ("does asyncio.gather() catch exceptions?", "btw the bundle budget is 10MB", "what's the default fetch timeout?") without bloating the main thread.
402
403Each question runs an isolated agent turn against `system + main transcript + side-chat history so far`. The side chat has the same tools and guards as the main chat, including per-tool confirmation when `--no-yolo` is active. Responses, tool calls, and tool results stay in the overlay. When you press `esc` to close, **nothing** has been added to the main session and subsequent main-thread turns don't re-read any of the side-chat exchanges, keeping the running context window lean.
404
405```
406/btw # open the overlay, type questions interactively
407/btw does PUT replace the whole resource?
408```
409
410Inside the overlay: `enter` sends, `esc` cancels an in-flight call (or closes the overlay if idle), `ctrl+c` closes immediately. Side-chat exchanges never touch the transcript and aren't persisted to the session file.
411
412### `/swarm`
413
414Background subagents that run alongside your main session. Each one is a separate `zot` subprocess with its own model loop, its own persistent session file, and its own chat in the dashboard — but they all run in **the same working directory as the host**, so they see and edit the same files you do. Spawn one for a side task (“draft the migration”, “investigate this stack trace”, “write the test harness for module X”), keep going in the main thread, check in on it whenever you want.
415
416> **Agents edit the same files you do.** They use the same `read` / `write` / `edit` / `bash` tools as the main agent against the host's working directory. There's no per-agent worktree or branch. If you need parallel edits on isolated checkouts, set that up yourself with `git worktree` outside zot.
417
418```
419/swarm # open the dashboard
420/swarm new <task> # spawn an agent
421/swarm new --model gpt-5 <task> # pin the new agent to a specific model
422/swarm logs <id> # jump straight into one agent's transcript
423/swarm send <id> <text> # send a follow-up without opening the dashboard
424/swarm resume # pick a stopped agent to bring back
425/swarm resume <id> # bring a specific agent back
426/swarm kill <id> # stop a running agent (its state stays)
427/swarm remove <id> # delete the agent's session and state
428/swarm list # alias for opening the dashboard
429```
430
431**Dashboard (`/swarm` with no arg)** — a list of every agent for the current session, with status, age, and current activity. Keys:
432
433| Key | Action |
434|---|---|
435| `↑` / `↓` | Move cursor between rows. |
436| `enter` | Open the highlighted agent's transcript view. |
437| `n` | Spawn a new agent (opens an inline task editor; inherits the host's current model). |
438| `p` | One-off prompt editor for the selected row (without entering the transcript). |
439| `R` | Resume a stopped agent in place. |
440| `k` | Kill the selected running agent. Its session and state stay so you can resume it later. |
441| `r` | Remove the selected agent entirely (session + meta gone). |
442| `esc` | Close the dashboard. |
443
444**Inside an agent's transcript** — a chat overlay with an always-on inline composer at the bottom. The conversation flows above it; type and `enter` to send a follow-up. The view auto-follows streaming output and shows an inline spinner with the agent's current activity (`thinking`, `tool: edit_file`, etc.) while it's busy. `esc` returns to the dashboard.
445
446**Switching the spawn model from inside the editor** — while composing a task in the `n`-prompt, type `/model` on its own line and `enter`. The standard `/model` picker pops up; pick a model, the picker closes, and the editor reopens with your typed task intact and the new model pinned for the spawn.
447
448**Session scoping** — each agent is stamped with the host session that spawned it and only shows up in that session's dashboard. Swap sessions with `/sessions` and the dashboard re-narrows accordingly. Agents from other sessions keep running in the background and reappear when you switch back.
449
450**Persistence across zot restarts** — every spawn writes a `meta.json` next to its event log and session file under `$ZOT_HOME/swarm/agents/<id>/`. On the next `zot` launch they show up in the dashboard as **detached**; press `R` (or `/swarm resume <id>`) to bring one back. Startup reads only the last 4 MiB of each large event log. Opening `/swarm` or resuming an agent loads its full log on demand, which can briefly delay that action for very large logs. The displayed transcript still retains at most 2000 lines. Resumed agents reattach to the same session and inbox socket, so the conversation continues from where it left off.
451
452**Where state lives** — everything per-agent (session file, events log, inbox socket, meta) lives under `$ZOT_HOME/swarm/agents/<id>/`. The agent's actual code edits land directly in your repo; track them with normal `git status` / `git diff`.
453
454**`/session export` does NOT bundle subagents.** A `.zotsession` is just the main chat transcript; per-agent state (session file, unix-socket inbox) is machine-local and doesn't round-trip through a JSONL file. To share what an agent said, copy it out of the transcript view manually.
455
456**Auto-swarm.** With `/settings` -> auto-swarm on, the main agent gets a built-in `swarm_spawn` tool and a system-prompt nudge to use it. It can then fork sub-agents on its own when a request naturally splits into independent parallel work ("implement A and B", "investigate three files"). Each spawn returns the sub-agent id immediately and the main turn keeps going. When every sub-agent the agent spawned in that batch finishes its initial task, zot injects one `[auto-swarm update]` message back into the main chat recapping each agent's status, task, and complete final response. If an agent produced no assistant response, zot includes a truncated transcript tail for diagnostics instead. The main agent then writes a short follow-up summary referencing the agents by id. Off by default; toggle from `/settings`.
457
458### `/settings`
459
460Opens a dialog with every persistent setting. `up`/`down` to navigate, `enter` or `space` to change the selected row, `esc` to close (rows that open a sub-view, like model shortcuts, use `esc` to go back one level first). Changes are written to `$ZOT_HOME/config.json` and take effect on the next turn (no restart needed). Current settings:
461
462- **render images when supported** — draw screenshots / `read`-returned images inline using the terminal's image protocol, or fall back to a text placeholder. Auto-detected from `TERM_PROGRAM`; the toggle overrides the detection. The row is greyed out and forced off on terminals that don't speak any image protocol.
463- **auto-swarm** — let the main agent spawn background sub-agents in parallel via a built-in `swarm_spawn` tool. Off by default. When on, the tool is registered with the running agent, the system prompt gains a short addendum telling the model to delegate independent sub-tasks proactively, and zot watches every sub-agent the main agent spawns. As soon as the last sub-agent in a batch finishes its initial task, an `[auto-swarm update]` message is injected back into the chat with each agent's status / task / transcript tail, so the main agent can summarise the collective outcome. Flipping off mid-session removes the tool from the live agent and strips the addendum on the next turn — the model stops trying to delegate. See `/swarm` for the dashboard that lets you monitor, message, kill, or remove the spawned agents.
464- **auto-compact threshold** — choose `off`, `70%`, `80%`, `85%` (default), or `90%` of the model's advertised context window. The selected percentage controls automatic compaction before and after interactive turns and persists as `auto_compact_threshold`. `off` disables percentage-based triggers but keeps manual `/compact` and automatic recovery from context-window and payload-too-large responses.
465- **jail new sessions by default**: start every new agent with tools confined to its working directory. Off by default. The setting applies to interactive, print, JSON, RPC, and background-agent runs, persists as `jail_by_default`, and immediately updates the current interactive session. `/jail` and `/unjail` remain session-scoped overrides and do not change this default.
466- **PowerShell tool** (Windows only): enable or disable native PowerShell alongside the default tools. Off by default; saved as `powershell_enabled` and applied without restarting. Explicit `--tools` or `--no-tools` takes precedence and disables this toggle for the run. Execution is still blocked while jailed and remains subject to packaged-agent permissions.
467- **OpenRouter Server Tools**: opt in to OpenRouter's beta web search, web fetch, advisor, subagent, fusion, image generation, datetime, and model-search tools. Off by default and available only with the OpenRouter provider. zot limits the remote loop to four server-tool calls per turn (adjustable via `openrouter_server_tool_call_limit` in `config.json`). `--no-tools` always disables them. Presets keep their own tool configuration and are not overridden by this setting. Server tools run remotely without zot's local tool confirmation dialog and may incur additional OpenRouter charges.
468
469 **config.json example:**
470
471 ```json
472 {
473 "openrouter_server_tools_enabled": true,
474 "openrouter_server_tool_call_limit": 4
475 }
476 ```
477
478- **fuzzy skill suggestions**: opt in to relevance-ranked, case-insensitive subsequence matching for `/skill:` names. For example, `/skill:review` or `/skill:crv` can find `code-review`. Matching characters appear in bold. Empty queries stay alphabetical, score ties use alphabetical order, and exact names retain priority. Off by default; missing or `false` keeps prefix matching. Changes apply immediately and persist as `fuzzy_skill_suggest` in `$ZOT_HOME/config.json`. Other slash commands and skill invocation lookup are unchanged. See [Skills](docs/skills.md).
479- **compact transcript rendering**: reduce visual chrome in the chat transcript. Tool calls render as a quiet header plus indented output instead of a bordered box, and sent messages render without padded background bubbles. Off by default. Changes apply immediately and persist to `config.json` as `compact_mode`.
480- **timestamps**: opens a sub-view with an **enabled** on/off toggle, an **interval** selector, and **date display** options. Show stored message times in muted text on existing separator rows, aligned with the tool box's right edge and leaving the same two-column right margin, without adding rows. Off by default, persisted as `chat_timestamps`. Times use the local timezone in `HH:MM` format, with a date on the first timestamp and when the day changes. User messages, completed assistant messages, and tool results are eligible. Missing timestamps and compaction summaries are skipped. Labels that do not fit in a narrow terminal are omitted. Timestamps appear after messages complete, not during streaming, and do not mark unread portions of a long response. Existing session timestamps are reused on resume.
481- **timestamps > date display**: choose **Start of each day** (default) to include `YYYY-MM-DD` on the first timestamp and when the local day changes, or **Every timestamp** to display `YYYY-MM-DD HH:MM` on every shown timestamp. Stored as `chat_timestamp_date` with values `day_start` or `every_message`. Missing or unknown values use `day_start`. This does not change which messages qualify under the interval setting.
482- **timestamps > interval**: minimum minutes since the last selected timestamp, default 0 (every eligible message). The first eligible message and day changes always qualify. Choose every message (0), 1, 5, 10, 15, 30, or 60 minutes in `/settings` > timestamps > interval, or set any nonnegative integer in `chat_timestamp_interval_minutes` in `config.json`. Missing or negative values use 0. Explicitly saved intervals are preserved. All timestamp settings apply immediately to the transcript. They affect only the interactive display, not model input or print/JSON/RPC output.
483- **collapse tool call rendering**: retain each tool-call header and the last non-empty rendered preview line, hiding the rest of the body. The preview may be an exit status, output line, or truncation summary; failed calls also retain an explicit error indicator. Boxed, flat, and compact layouts are preserved. `ctrl+o` expands the full output. Off by default. Changes apply immediately and persist to `config.json` as `collapse_tool_call`.
484- **show loaded resources at startup**: list the active Zotfile agent (for `zot run`), loaded `AGENTS.md` paths, extensions, and user-installed skills in compact sections above the transcript. Built-in skills are omitted. Off by default. Changes apply immediately and persist to `config.json` as `show_instructions_at_startup`.
485- **TUI settings**: opens a sub-view for input layout and status placement. **Input style** can be `plain` (default prompt line), `lines` (separator lines above and below the input), or `block` (a user-bubble-style input block). **Status position** places model, usage, and working-directory information above or below the input. **Working spinner position** places the busy spinner above or below the input. Changes apply immediately and persist to `config.json` as `tui_input_style`, `tui_status_position`, and `tui_working_position` (`above_input` or `below_input` for the position fields).
486- **reasoning level**: choose reasoning for supported models: off, minimum, low, medium, high, xhigh, or max. The `max` tier is opt-in and sent natively to GPT-5.6 and adaptive-thinking Claude models; unsupported backends clamp it to their highest accepted effort. The change is persisted to `config.json` and applied to the next model call. Use `/reasoning` to open this selector directly. The selector only shows distinct levels supported by the active model; models without reasoning support only offer `off`.
487- **color theme** — choose the built-in auto/dark/light theme or any JSON theme discovered under `$ZOT_HOME/themes` or a loaded extension. Theme files can override any subset of UI colors, syntax colors, and spinner frames/messages. Changes apply immediately; if a selected theme file is deleted, zot resets to auto. See [docs/themes.md](docs/themes.md).
488- **model shortcuts** — opens a sub-view with nine slots (`model 1` ... `model 9`). `enter` on a slot opens the same `/model` selector and binds the chosen provider/model to that slot; `backspace` clears a slot. Once assigned, press `Ctrl+1` ... `Ctrl+9` from the editor to switch the active model instantly (the same cross-provider swap `/model` performs, transcript and cost carried over). Assigning a shortcut does not change the current model. Shortcuts are skipped while a turn is running.
489
490### `/skills`
491
492Opens a picker listing every discovered SKILL.md file, built-ins hidden. Each row shows the skill name, source, and description. `enter` opens the body inline (scrollable with `up`/`down`/`pgup`/`pgdn`); `esc` goes back. Re-runs discovery each time it opens, so edits to a SKILL.md during a session are reflected immediately.
493
494Press `p` or `g` in the skills list to toggle project or global pins. Pinned
495instructions accompany your first message in a fresh conversation and after
496`/clear`, without starting a model turn on their own. A read-only notice above
497the input shows the pending skills. Resuming does not reload pins. See
498[pinning skills](docs/skills.md#pinning-skills) for scope, storage, and mode support.
499
500### `/compact`
501
502Sends the current transcript through the model with a structured summarization prompt. The returned summary replaces the transcript as one synthetic user message, with the last few exchanges kept verbatim for continuity. The status bar's context meter resets. Use it when the context meter creeps past ~80%. Press `ctrl+o` to expand the summary. Expanded text wraps to the current terminal width, including after resizing.
503
504zot also auto-compacts in the background: after any turn that reaches the configured context threshold, the agent kicks off a condense pass on its own. Choose `off`, `70%`, `80%`, `85%` (default), or `90%` under `/settings` → **auto-compact threshold**. You'll see `condensing history, esc to cancel` above the status bar and an `(auto)` tag next to the context percentage; `esc` aborts it without touching the transcript. Turning the percentage trigger off does not disable automatic compaction and retry after a context-window or payload-too-large response.
505
506### `/jail`
507
508Enforces a sandbox rooted at the cwd shown in the status bar. `read`, `write`, and `edit` resolve their target path (including through symlinks) and refuse anything outside the sandbox. `bash` refuses obvious escape patterns (`sudo`, `rm -rf /`, leading `cd /`, `cd ..`, `cd ~`, `chmod -R`, `dd of=/`, and similar) and rejects shell arguments or redirections that point outside the sandbox. The status bar shows `jailed, ~/your/cwd` while active. Enable **jail new sessions by default** in `/settings` to persist this behavior across launches; `/unjail` then unlocks only the current session.
509
510This is a guardrail against accidents, not a hard security boundary. If you need real isolation, run zot under docker or a proper sandbox.
511
512## Sessions
513
514Every interactive or print/json run (unless `--no-session`) writes a JSONL transcript under `$ZOT_HOME/sessions/<cwd-hash>/`. Resume any of them with `--continue`, `--resume`, `--session <path>`, or interactively via `/sessions` inside the TUI. In interactive mode, `/new` persists the current conversation and starts an empty session while preserving the working directory, model, tools, extensions, and runtime settings. It does not make a model call, and the previous conversation remains available through `/sessions`. `/new` is hidden from autocomplete and `/help` with `--no-session`, and invoking it directly reports that sessions are disabled. Empty sessions (the user exited without prompting) are deleted on close so the list stays tidy.
515
516Use `zot sessions prune` outside the TUI to find sessions whose recorded working directories no longer exist. The command groups sessions by directory, shows the number and human-readable total size of stored sessions, lets you select groups, and requires confirmation before permanently deleting files. It preserves sessions when a directory check fails with an error other than "not found" and rechecks each selected directory immediately before deletion. A deleted directory and a path hidden by some unmounted filesystems are indistinguishable, so review the selection and ensure remote filesystems are mounted before deleting missing-directory groups.
517
518Use `--older-than` to prune sessions by last activity instead, including sessions for directories that still exist. Supported age units are `m` (minutes), `h` (hours), `d` (24-hour days), `w` (7-day weeks), `mo` (calendar months), and `y` (calendar years). Add `--cwd PATH` to limit age pruning to one working directory. The session file is checked again immediately before deletion and preserved if it has become active recently.
519
520```sh
521zot sessions prune --dry-run # list missing-directory groups
522zot sessions prune --older-than 4h --dry-run # inspect sessions inactive for four hours
523zot sessions prune --older-than 1mo --cwd ~/project # select old sessions for one directory
524zot sessions prune --older-than 1y --all # select every match, then confirm
525zot sessions prune --older-than 30d --all --yes # delete every match without prompting
526```
527
528`--all` selects every match and still asks for confirmation. Adding `--yes` makes deletion non-interactive and requires `--all`. Malformed, unreadable, symlinked, and non-absolute session entries are reported and preserved during missing-directory pruning. The scan includes normal sessions and named-agent sessions below `$ZOT_HOME/sessions/`; explicit session files stored elsewhere and swarm-agent state are not included.
529
530## Providers
531
532zot's built-in provider catalog includes:
533
534- **Subscription-capable**: Anthropic Claude Pro/Max (`anthropic`), OpenAI Codex / ChatGPT Plus/Pro (`openai-codex`), Kimi Code (`kimi`), SuperGrok/X Premium (`xai`), GitHub Copilot (`github-copilot`).
535- **Direct API providers**: Anthropic, OpenAI Chat Completions, OpenAI Responses, DeepSeek, Google Gemini, Kimi/Moonshot, Moonshot CN, Groq, Cerebras, xAI, Together AI, Hugging Face Router, OpenRouter, Mistral, Z.AI, Xiaomi/MiMo token-plan regions, MiniMax global/CN, Fireworks, Vercel AI Gateway, OpenCode/OpenCode Go.
536- **Cloud/platform providers**: Amazon Bedrock, Google Vertex AI, Azure OpenAI, Cloudflare Workers AI, Cloudflare AI Gateway.
537- **Local/compatible**: Ollama, llama.cpp router mode, and OpenAI-compatible local endpoints via `--base-url`.
538
539Use `/login` to store API keys or subscription credentials. `/model` only shows models from providers that are currently available from env vars, `auth.json`, Kimi CLI fallback, local Ollama, or a configured llama.cpp router.
540
541## Models
542
543`--list-models` or the `/model` picker shows the full catalog across all built-in providers. Three sources:
544
545- **Catalog**: models baked into zot, covering Claude, GPT/Codex, Gemini/Gemma, Kimi/Moonshot, DeepSeek, Groq-hosted Llama/Gemma/Compound, OpenRouter-routed models, Bedrock model ids, Vertex model ids, Azure OpenAI deployments, Copilot models, and other provider-specific catalog entries.
546- **Live**: IDs discovered from the provider using your stored credentials — usually `GET /v1/models`, or provider-specific listing APIs for Amazon Bedrock (see [Amazon Bedrock](#amazon-bedrock)). Cached for 6h in `$ZOT_HOME/models-cache.json` and refreshed in the background on startup.
547- **Speculative**: IDs that appear in the upstream generator but aren't live on the public API yet. They'll 404 today and start working the moment the provider ships them.
548
549The context meter in the status line uses the model's advertised context window to show how much of it your last turn consumed.
550
551### Model fallback (rescue)
552
553When a turn fails because of a recoverable provider error — expired token (`401`), permission denied (`403`), rate limit (`429`), provider outage (`502`/`503`/`504`), or a transient network failure — zot opens a **rescue** picker over the chat instead of just painting a red banner.
554
555The picker is the same vertical list / fuzzy filter UI as `/model`, but it only shows models from providers you're currently logged in to (env vars, `auth.json`, Kimi CLI fallback, ollama). The failed model is excluded. Press `↑`/`↓` to choose, `enter` to retry the **same prompt** on the new model, `esc` to dismiss.
556
557Before the actual provider request fires, the OpenAI / Anthropic / Kimi / DeepSeek / Google / OpenAI-Codex clients also do up to two silent retries with short backoff (250ms, 750ms) on `502`/`503`/`504` and connection-reset / EOF-before-headers errors. Most edge-proxy blips disappear without you ever seeing the rescue picker.
558
559A rescue retry always **drops launch-time `--api-key` and `--base-url`** before rebuilding the agent. Those overrides are usually the reason the rescue triggered (bad key, typo'd base URL, corporate gateway only valid for the originally-picked provider), so the retry re-resolves credentials from env vars / `auth.json` / provider defaults instead. Use `/model` if you want overrides to stick.
560
561No configuration is required — the candidate list is built dynamically from your active credentials. Bad-request / context-length / serialization errors are NOT routed to the rescue picker, because switching models won't fix them; those still surface as a normal error.
562
563### Custom models
564
565Place a `models.json` in `$ZOT_HOME` (`$XDG_STATE_HOME/zot/` when set, otherwise the platform default above) to add models that aren't in the baked-in catalog or to override existing entries:
566
567```json
568{
569 "providers": {
570 "openai": {
571 "models": [
572 {
573 "id": "gpt-5.5",
574 "name": "GPT-5.5",
575 "reasoning": true,
576 "reasoningLevelMap": {"minimum": "low", "max": ""},
577 "contextWindow": 400000,
578 "maxTokens": 128000
579 }
580 ]
581 }
582 }
583}
584```
585
586Supported fields per model: `id` (required), `name`, `reasoning`, `reasoningLevelMap`, `contextWindow`, `maxTokens`, `baseUrl`, `priceInput`, `priceOutput`, `priceCacheRead`, `priceCacheWrite`.
587
588`reasoningLevelMap` is optional. Protocol defaults apply when it is omitted. Add only model-specific exceptions using `minimum`, `low`, `medium`, `high`, `xhigh`, or `max` as keys. Map a key to another level when both inputs are equivalent, use an identity mapping such as `"max": "max"` to enable a level beyond the protocol default, or map it to an empty string or `off` to remove it. The same effective mapping drives `/reasoning` and provider requests.
589
590Provider keys are normalized: `openai-codex` and `openai-responses` map to `openai`, `anthropic-messages` maps to `anthropic`, `moonshot`, `moonshot-ai`, and `kimi-code` map to `kimi`, and `deepseek-chat` and `deepseek-ai` map to `deepseek`. Built-in provider ids such as `groq`, `openrouter`, `github-copilot`, `amazon-bedrock`, `google-vertex`, `azure-openai-responses`, `fireworks`, `vercel-ai-gateway`, `mistral`, and `xai` can also be used directly.
591
592User-defined models show `source: user` in `--list-models` and take precedence over both the baked-in catalog and live-discovered models. Adding a `models.json` does not hide the built-in catalog; entries are merged on top of it. Missing or invalid files are silently ignored.
593
594#### Custom providers
595
596A top-level provider key that is not a built-in id defines a custom provider. Give it a provider-level `baseUrl` and an `api` wire format (`openai` for OpenAI-compatible Chat Completions, the default, `openai-responses` for the OpenAI Responses API, or `anthropic` for the Anthropic Messages API). A model-level `baseUrl` overrides the provider-level one for that model; an unknown `api` value falls back to `openai` with a warning.
597
598```json
599{
600 "providers": {
601 "my-company": {
602 "baseUrl": "https://llm.mycompany.com/v1",
603 "api": "openai",
604 "models": [
605 { "id": "company-llm-v2", "name": "Company LLM v2" }
606 ]
607 }
608 }
609}
610```
611
612Custom providers are first-class: they appear in `--list-models`, `/model`, and `/login`. `models.json` never stores secrets. Supply the key through `/login`, `--api-key`, or a derived environment variable in upper snake case (so `my-company` reads `MY_COMPANY_API_KEY`). Because many self-hosted gateways do not expose a model-list endpoint, custom provider keys are accepted and stored without a verification probe; an invalid key surfaces on the first model call.
613
614To retrieve this custom provider's key from a password manager, add a matching entry to `$ZOT_HOME/auth.json`:
615
616```json
617{
618 "additional_api_key_creds": {
619 "my-company": {
620 "api_key_command": {
621 "program": "op",
622 "args": ["read", "op://Work/OpenAI/credential"],
623 "timeout_ms": 120000
624 }
625 }
626 }
627}
628```
629
630The provider IDs in `models.json` and `auth.json` must match. Then select the custom model directly:
631
632```bash
633zot --provider my-company --model company-llm-v2
634```
635
636##### Live model discovery for custom providers
637
638Set `"discover": true` on a custom provider to list its models from the OpenAI-compatible `/models` endpoint instead of, or in addition to, a static `models` array. Discovery is off by default and requires a provider-level `baseUrl`.
639
640```json
641{
642 "providers": {
643 "m4": {
644 "baseUrl": "http://127.0.0.1:48000/v1",
645 "discover": true
646 }
647 }
648}
649```
650
651zot queries `<baseUrl>/models` (or `<baseUrl>/v1/models` when the base has no version segment) with a 3 second timeout: in the background at startup, when `/model` opens, and when launching with a discovered model that is not yet in the catalog. The stored or environment API key is sent as a bearer token when present; a missing key is not an error. Discovered IDs get a conservative 32K context and 4K output default, and IDs containing `embed` are skipped. Entries in the provider's `models` array override discovered metadata for the same ID. The list stays in memory only, failed refreshes keep the previous snapshot, and `/model` reports the error. Background refresh never executes `api_key_command`.
652
653### Kimi Code
654
655zot has built-in Kimi support through the Kimi Coding endpoint and Moonshot's OpenAI-compatible chat API.
656
657```bash
658zot --provider kimi
659```
660
661By default this uses:
662
663- model: `kimi-for-coding`
664- base URL: `https://api.kimi.com/coding/v1`
665
666Credential lookup order for Kimi:
667
6681. `--api-key`
6692. `KIMI_API_KEY`
6703. `MOONSHOT_API_KEY`
6714. `$ZOT_HOME/auth.json`
6725. the official Kimi Code CLI token at `~/.kimi/credentials/kimi-code.json`, unless disabled by `/logout kimi`
673
674Use `/login` for either API-key login or Kimi Code subscription login. The subscription flow uses Kimi Code's device-code OAuth flow: zot opens the verification URL, waits for browser approval, stores the token in `auth.json`, and refreshes it automatically.
675
676For direct Moonshot API keys or a custom compatible endpoint:
677
678```bash
679zot --provider kimi --model kimi-k2-0905-preview --base-url https://api.moonshot.ai/v1 --api-key "$KIMI_API_KEY"
680```
681
682Kimi K3 is built in as `kimi/k3`, `moonshotai/kimi-k3`, `moonshotai-cn/kimi-k3`, `opencode-go/kimi-k3`, `openrouter/moonshotai/kimi-k3`, and `vercel-ai-gateway/moonshotai/kimi-k3`. Its output limit is 131,072 tokens on every built-in route.
683
684You can add additional Kimi/Moonshot model IDs to `models.json` under the `kimi` provider.
685
686### xAI
687
688xAI supports either `XAI_API_KEY` or `/login` subscription authentication. The subscription option is labeled `Sign in with SuperGrok or X Premium`, opens a prefilled device-authorization URL, and refreshes the stored token automatically. The default xAI model is `grok-4.5`, sent through the Responses API.
689
690### DeepSeek
691
692zot has built-in DeepSeek support through DeepSeek's OpenAI-compatible chat API.
693
694```bash
695zot --provider deepseek
696```
697
698By default this uses:
699
700- model: `deepseek-v4-pro`
701- base URL: `https://api.deepseek.com/v1`
702
703Catalog ships with `deepseek-v4-pro` (reasoning) and `deepseek-v4-flash`. These are exactly the IDs returned by `GET https://api.deepseek.com/models` today. You can add additional model IDs to `models.json` under the `deepseek` provider. For direct DeepSeek reasoning models, `/reasoning` offers off, low, high, and max. DeepSeek maps medium and xhigh to high, while max sends the distinct `reasoning_effort: "max"` value. Other OpenAI-compatible providers retain their own effort limits.
704
705Credential lookup order for DeepSeek:
706
7071. `--api-key`
7082. `DEEPSEEK_API_KEY`
7093. `$ZOT_HOME/auth.json`
710
711Use `/login` and pick **api key** to paste a DeepSeek key. zot probes `/v1/models` once and stores the key under `deepseek` in `auth.json`.
712
713> **Auth model: API key only.** DeepSeek does not offer a subscription OAuth flow. The `/login subscription` step lists only Anthropic, OpenAI, and Kimi; DeepSeek shows up only under `/login → api key`.
714
715> **Text only at the wire level.** DeepSeek's chat-completions endpoint currently rejects the multimodal content schema (`unknown variant image_url, expected text`). When the active provider is `deepseek`, zot silently drops `ImageBlock` parts from outgoing user/tool messages and keeps only the text. Switching back to a vision-capable model (Claude, GPT-4o/5, Gemini) re-sends the image normally because the session file still stores it.
716
717For a custom-compatible endpoint (mirror, gateway, self-host):
718
719```bash
720zot --provider deepseek --base-url https://my-deepseek-mirror.example.com/v1 --api-key "$DEEPSEEK_API_KEY"
721```
722
723### Google Gemini
724
725zot has built-in Google Gemini support through the [AI Studio Generative Language API](https://aistudio.google.com/).
726
727```bash
728zot --provider google
729```
730
731By default this uses:
732
733- model: `gemini-2.5-pro`
734- base URL: `https://generativelanguage.googleapis.com`
735
736Catalog ships with `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`, `gemini-2.0-flash`, and `gemini-2.0-flash-lite`. Live discovery against `/v1beta/models` adds anything else your key can see.
737
738Credential lookup order for Google:
739
7401. `--api-key`
7412. `GEMINI_API_KEY`
7423. `GOOGLE_API_KEY`
7434. `$ZOT_HOME/auth.json`
744
745Use `/login` and pick **api key** to paste an AI Studio key. zot probes `/v1beta/models` once and stores the key under `google` in `auth.json`.
746
747> **Auth model: API key only (this provider).** Google does not issue OAuth tokens for consumer Gemini Advanced / Google One AI Premium subscriptions, so there is no "log in with your Google subscription" flow. The `/login subscription` step quietly downgrades to the api-key form when you pick Google so you don't end up in a dead end. If you need OAuth/service-account auth instead of an API key, use the `google-vertex` provider below.
748
749> **Free-tier rate limits.** AI Studio's free tier has tight per-minute and per-day caps that vary by model: `gemini-2.5-pro` is the strictest (a few requests per minute, ~50 per day), Flash and Flash-Lite are far more generous. If a Pro turn 429s with `"You exceeded your current quota"` while Flash on the same key still works, you've hit the Pro free-tier RPD. Either switch to Flash for agent loops, or [enable billing](https://aistudio.google.com/app/apikey) on your AI Studio project to flip the same key from free to pay-as-you-go pricing (`$1.25/M` input, `$10/M` output for Pro).
750
751Reasoning levels (`--reasoning off|minimum|low|medium|high|xhigh|max`, also configurable through `/reasoning` or in `/settings` as **reasoning level**) map differently per generation. `max` is a distinct opt-in tier above `xhigh`. GPT-5.6, GPT-6, and adaptive-thinking Claude models receive native `max`; unsupported providers clamp it to their highest accepted effort. Budget-based providers retain their provider/model caps. Gemini 3.x uses the `thinkingLevel` enum (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`), with Gemini-3-Pro pinned to `LOW` minimum and `HIGH` for any medium-or-higher request. `off` sends no reasoning config except on always-on thinking models. Gemini 2.0 models have no thinking config.
752
753Claude Opus 5.5 is available through Anthropic (`--provider anthropic --model claude-opus-5-5`) and GitHub Copilot (`--provider github-copilot --model claude-opus-5.5`, subject to account availability). Both entries support a 1,000,000-token context window and up to 128,000 output tokens. Adaptive thinking is always on, with `low`, `medium`, `high`, `xhigh`, and `max` effort levels. The default, `off`, and `minimum` requests use `low` instead. The reasoning selector omits unsupported levels. Catalog prices per million tokens are $4 input, $20 output, $0.20 cache reads, and $5 cache writes. Copilot billing remains governed by your plan.
754
755GPT-6 Sol and GPT-6 Luna are available through OpenAI API keys, OpenAI Codex subscriptions, and GitHub Copilot. Select `gpt-6-sol` or `gpt-6-luna` with `--provider openai`, `openai-responses`, `openai-codex`, or `github-copilot`. Both use the Responses API, support up to 128,000 output tokens, and expose `low` through `max` reasoning. OpenAI and Codex entries use a 272,000-token context window, while Copilot entries use 1,000,000 tokens. Access through subscription providers depends on account availability and organization policy.
756
757You can add additional Gemini model IDs to `models.json` under the `google` provider.
758
759### Gemini Enterprise Agent Platform (formerly Google Vertex AI)
760
761zot also has built-in support for [Gemini Enterprise Agent Platform](https://cloud.google.com/products/gemini-enterprise-agent-platform), Google's enterprise/GCP-hosted Gemini endpoint. Unlike the AI Studio `google` provider above, the `google-vertex` provider supports a Google Cloud API key plus `service_account` and `authorized_user` credential files. It does not support consumer Google login or the complete ADC credential chain, such as metadata-server and workload-identity credentials.
762
763```bash
764zot --provider google-vertex
765```
766
767Required configuration (env vars, read at construction time):
768
769- `GOOGLE_CLOUD_PROJECT` — required, your GCP project id.
770- `GOOGLE_CLOUD_LOCATION` — optional, defaults to `us-central1`.
771
772Credential lookup order for Vertex:
773
7741. `GOOGLE_CLOUD_API_KEY`: simplest option. An API key created in the GCP console is sent as `x-goog-api-key`, without a token exchange.
7752. `GOOGLE_APPLICATION_CREDENTIALS`: path to a supported credential JSON file.
7763. **Default ADC file**: if neither of the above is set, zot checks the file written by `gcloud auth application-default login`. This is `~/.config/gcloud/application_default_credentials.json` on Unix systems and `%APPDATA%\gcloud\application_default_credentials.json` on Windows.
777
778Two credential file shapes are supported:
779
780- `type: "service_account"`: zot signs a JWT with the private key and exchanges it for a short-lived access token.
781- `type: "authorized_user"`: zot exchanges the stored client ID, client secret, and refresh token for a short-lived access token.
782
783Access tokens are cached in memory and refreshed on demand.
784
785If none of these are available, zot errors with `vertex: no auth — set GOOGLE_CLOUD_API_KEY or GOOGLE_APPLICATION_CREDENTIALS`.
786
787### Amazon Bedrock
788
789zot talks to Bedrock's Converse streaming API and supports two credential styles:
790
791- **Bedrock API key** (bearer token) via `AWS_BEARER_TOKEN_BEDROCK`. This is the simplest setup: a single opaque string, region-scoped to wherever it was minted. It requires only the `bedrock:CallWithBearerToken` permission.
792- **SigV4** via `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` (plus optional `AWS_SESSION_TOKEN`), an `AWS_PROFILE`, or a default / SSO / assume-role / `credential_process` profile resolved through the AWS CLI. SigV4 (AWS Signature Version 4) is AWS's request-signing scheme: your secret key signs each request locally and only the signature travels over the wire. zot implements it directly so it does not depend on the AWS SDK.
793
794If both are present, the bearer token wins for inference, while live model discovery uses SigV4. The region defaults to `AWS_REGION` / `AWS_DEFAULT_REGION`, falling back to `us-east-1`. A `403` on a freshly-copied API key almost always means a region mismatch — set `AWS_REGION` to the region the key was created in, or pass `--base-url https://bedrock-runtime.<region>.amazonaws.com`.
795
796#### Catalog vs. live discovery
797
798Unlike most providers, Bedrock model **pricing, context windows, and capability flags are hand-maintained** in zot's baked-in catalog. AWS does not expose usable pricing programmatically for current models — its Price List API returns only stale, input-only rates for a few legacy models — so the catalog is authoritative for cost and metadata. This means:
799
800- **Bearer-token-only users** still get the full curated Bedrock model list with correct pricing from the catalog. No AWS API listing is attempted.
801- **Users with SigV4 credentials** additionally get **live model discovery**, even if inference uses a bearer token: on startup, zot lists the foundation models and cross-region inference profiles your account can actually use (via `ListFoundationModels` and `ListInferenceProfiles`) and merges them into the catalog. New Bedrock models therefore appear in `/model` without waiting for a zot release. Discovered IDs already known to the catalog keep their curated pricing; genuinely new IDs show up with placeholder pricing until the catalog is updated.
802
803Live discovery requires SigV4 because the listing APIs live on the Bedrock control plane, which rejects the runtime bearer token. It runs in the background at startup and is cached for 6h like other providers, so `/model` reflects the latest list without any manual refresh. If discovery fails (no SigV4 creds, expired SSO login, missing IAM permissions), zot silently falls back to the catalog. Discovery needs `bedrock:ListFoundationModels` and `bedrock:ListInferenceProfiles`.
804
805#### Cross-region pricing caveat
806
807Bedrock model IDs carry a routing prefix: `global.` (global cross-region inference), a geography such as `us.` / `eu.` / `apac.` / `au.` / `jp.` (geo cross-region inference), or none (in-region). zot's catalog prices **every** variant at the model's base (US) rate.
808
809AWS charges a surcharge for **geo cross-region inference** in some regions — for example, Anthropic models routed through the Asia Pacific (Melbourne) geo profile cost about 10% more than the base rate. zot does **not** yet capture these per-region uplifts, so if you use a geo-prefixed profile in a region that charges the surcharge, zot's reported cost may be **lower than your actual AWS bill**. Global cross-region and plain in-region usage match the base rate and are unaffected. The context/usage accounting is still correct; only the dollar figure can under-report in this edge case. If you need exact costs, check the per-region table on the [Amazon Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/).
810
811### Local models with ollama
812
813zot works with [ollama](https://ollama.com) out of the box. Ollama serves an OpenAI-compatible API locally, so any model you have pulled works with zot.
814
815Quick start:
816
817```bash
818ollama pull qwen3.5:4b
819zot --provider ollama --model qwen3.5:4b
820```
821
822No login or API key is needed for local models. zot defaults to `http://localhost:11434`. The model ID must match a model installed on that Ollama server, including its tag. The server must already be running.
823
824#### Save a local default
825
826To start with just `zot`, create `$ZOT_HOME/config.json` with the following fields, or merge them into your existing configuration:
827
828```json
829{
830 "provider": "ollama",
831 "model": "qwen3.5:4b"
832}
833```
834
835A `models.json` file is not required for this setup. If you want explicit model metadata, add the following to `$ZOT_HOME/models.json`. This registers a model but does not change the default selection in `config.json`:
836
837```json
838{
839 "providers": {
840 "ollama": {
841 "models": [
842 {
843 "id": "qwen3.5:4b",
844 "name": "Qwen 3.5 4B",
845 "contextWindow": 32768,
846 "maxTokens": 8192
847 }
848 ]
849 }
850 }
851}
852```
853
854Set `contextWindow` and `maxTokens` to limits your model and server configuration actually support. These fields inform zot's context and output budgets. They do not configure Ollama's context size or install the model.
855
856#### Use a remote Ollama server
857
858For a single run, pass the endpoint explicitly:
859
860```bash
861zot --provider ollama --model qwen3.5:4b --base-url http://ollama-server:11434/v1
862```
863
864For an endpoint behind authentication, use HTTPS and pass `--api-key` with the token. The built-in `ollama` provider uses that flag or a placeholder credential. It does not load a saved Ollama key from `auth.json`.
865
866To persist a remote endpoint, keep the `config.json` selection above and add `baseUrl` to the model in `$ZOT_HOME/models.json`:
867
868```json
869{
870 "providers": {
871 "ollama": {
872 "models": [
873 {
874 "id": "qwen3.5:4b",
875 "baseUrl": "http://ollama-server:11434/v1",
876 "contextWindow": 32768,
877 "maxTokens": 8192
878 }
879 ]
880 }
881 }
882}
883```
884
885`--base-url` overrides the model's endpoint for that run. `baseUrl` is a `models.json` field, not a `config.json` field. Only connect to trusted servers and do not expose an unauthenticated Ollama endpoint to the public internet.
886
887#### Use the same model on two hosts
888
889Models are identified by the pair `(provider, model ID)`. Two entries with the same ID under `ollama` cannot represent independently selectable hosts. A different display `name` does not change that identity.
890
891Instead, define a custom provider for each host in `$ZOT_HOME/models.json`, keeping the actual Ollama model ID unchanged:
892
893```json
894{
895 "providers": {
896 "ollama-home": {
897 "baseUrl": "http://localhost:11434/v1",
898 "api": "openai",
899 "discover": true,
900 "models": [
901 { "id": "qwen3.5:4b", "contextWindow": 32768, "maxTokens": 8192 }
902 ]
903 },
904 "ollama-server": {
905 "baseUrl": "http://ollama-server:11434/v1",
906 "api": "openai",
907 "discover": true,
908 "models": [
909 { "id": "qwen3.5:4b", "contextWindow": 32768, "maxTokens": 8192 }
910 ]
911 }
912 }
913}
914```
915
916Select either host explicitly:
917
918```bash
919zot --provider ollama-home --model qwen3.5:4b
920zot --provider ollama-server --model qwen3.5:4b
921```
922
923To make the second host your default, use:
924
925```json
926{
927 "provider": "ollama-server",
928 "model": "qwen3.5:4b"
929}
930```
931
932Save that selection in `$ZOT_HOME/config.json`. Custom providers support stored or environment credentials when required. Discovery-enabled custom providers also work without a key for unauthenticated local servers.
933
934`discover` lists models already installed on each server. It does not download models into Ollama. Static `models` entries are optional when discovery is enabled, but let you override the discovered context and output defaults. See [Live model discovery for custom providers](#live-model-discovery-for-custom-providers) for refresh behavior and limitations.
935
936The built-in `ollama` provider currently has no dedicated `/login` endpoint-setup flow or automatic model discovery. Use explicit model IDs, static entries, or discovery-enabled custom providers as above. The `ollama` provider uses the OpenAI chat completions protocol internally, so it also works with OpenAI-compatible servers such as vLLM, LM Studio, and LocalAI.
937
938### Local models with LM Studio
939
940Register an LM Studio server through `/login` -> API key -> `lmstudio`.
941Enter its server URL (for example `http://localhost:1234`) and an optional
942API key. The URL is required and is stored in `auth.json`. Zot does not
943probe localhost or discover LM Studio models until a server URL is registered.
944An API key alone does not enable discovery.
945
946After registration, zot refreshes the model list in the background on startup,
947after saving the connection, and when opening `/model`. Selecting or resuming
948an LM Studio model also refreshes its metadata. Choose a discovered model in
949`/model`, or use `zot --provider lmstudio --model <id>` after registration.
950Registration never changes your selected model or provider.
951
952Discovery prefers `/api/v1/models` and falls back to `/v1/models` if the native
953endpoint returns 404, 405, or 501. Native discovery excludes embedding models
954and uses loaded instance IDs and their configured context limits when present.
955Unloaded models use their model keys and advertised maximum context limits.
956The fallback only provides IDs, so embedding filtering there is best-effort.
957Quantization-specific keys are preserved, but zot does not expand the native
958API's separate variant list into additional model choices.
959
960Missing context metadata defaults to 32,768 tokens. Output defaults to at most
9614,096 tokens (or a quarter of the context window for smaller models).
962These are estimates, not server guarantees. Discovered models have zero API
963pricing, and reasoning controls are not enabled automatically from capability
964metadata. Token usage still comes from inference responses. Override metadata
965through `models.json` under `lmstudio` when needed, including the context limit
966for models that LM Studio will load with a smaller context than advertised.
967User overrides take precedence. Discovery does not change metadata for other
968providers or the coding-agent workflow.
969
970Listing models does not load or download them. Inference with an unloaded
971model depends on LM Studio's auto-loading settings. The discovered catalog is
972in-memory only, and a failed refresh retains the previous snapshot in that
973process. `/model` reports discovery failures without blocking other providers.
974Use `/logout lmstudio` to remove the connection and stop discovery.
975Background refresh never executes API-key commands.
976
977### Local models with llama.cpp router mode
978
979zot can connect to a recent [llama.cpp](https://github.com/ggml-org/llama.cpp) router, manage its GGUF files, and use loaded models through the router's OpenAI-compatible inference API. This is separate from Ollama. An Ollama server normally listens on port `11434` and should use zot's `ollama` provider instead.
980
981Install or update llama.cpp. On macOS with Homebrew:
982
983```bash
984brew install llama.cpp
985# or, when already installed
986brew upgrade llama.cpp
987```
988
989Create a directory for GGUF files and start `llama-server` without `--model`, `-m`, or `-hf`. Supplying one of those options starts a single model rather than the router API zot needs.
990
991```bash
992mkdir -p ~/llama-models
993
994llama-server \
995 --models-dir ~/llama-models \
996 --no-models-autoload \
997 --jinja \
998 --host 127.0.0.1 \
999 --port 8080 \
1000 -ngl 999 \
1001 -c 32768
1002```
1003
1004`--no-models-autoload` leaves load decisions to `/llama`. `--jinja` enables model chat templates and improves tool-call compatibility. `-ngl 999` requests maximum GPU offload, while `-c 32768` limits each loaded model to a 32K context. Adjust these values for your hardware.
1005
1006Confirm that router mode is active before configuring zot:
1007
1008```bash
1009curl http://127.0.0.1:8080/health
1010curl http://127.0.0.1:8080/models
1011```
1012
1013The models request must return JSON with a `data` array. A 404 usually means the server is outdated, running on a different port, or was started in single-model mode.
1014
1015In zot, run `/login`, choose **api key**, and select **llama.cpp**. Enter `http://127.0.0.1:8080` as the router URL and leave the API key empty for a local-only server. Do not enter `/v1`; zot derives the inference URL itself. The saved URL and optional key live in `$ZOT_HOME/auth.json`.
1016
1017You can configure the same connection through environment variables:
1018
1019```bash
1020export LLAMA_BASE_URL=http://127.0.0.1:8080
1021export LLAMA_API_KEY=optional-secret
1022```
1023
1024When using a key, launch the server with the matching `--api-key` value. Keep `--host 127.0.0.1` unless remote clients must reach the router.
1025
1026Run `/llama` to:
1027
1028- inspect the router's current model states
1029- search Hugging Face for GGUF repositories
1030- choose a quantization and download it with byte progress
1031- explicitly load or unload a model
1032- press `d` to ask the router to remove a downloaded cache model after confirmation
1033
1034Models discovered through `--models-dir` or a preset cannot be removed by zot. Delete those files from their configured source instead. The router removes the selected GGUF from its Hugging Face cache, but some llama.cpp versions retain shared repository artifacts such as `mmproj` files. Remove the repository's cache directory manually if those artifacts are no longer needed. Hugging Face search uses `HF_TOKEN` when available. Gated repositories require prior access approval, and the `llama-server` process also needs an authorized `HF_TOKEN` because the server performs the download.
1035
1036Opening `/model` refreshes the router and lists every loaded model under provider `llama.cpp`. Unloaded models are intentionally omitted because they cannot answer inference requests. Load them through `/llama` first, then select them through `/model`.
1037
1038A model installed through Ollama is kept in Ollama's internal storage and is not automatically available as a llama.cpp GGUF file. Download a GGUF copy through `/llama` or place GGUF files in `~/llama-models`, then restart the router so it discovers files added manually.
1039
1040## Child process environment
1041
1042Scripts and shell commands launched by zot inherit runtime metadata:
1043
1044| Variable | Meaning |
1045| --- | --- |
1046| `AI_AGENT` | Generic agent marker, set to `zot` |
1047| `ZOT_AGENT` | Zot-specific process marker, set to `1` |
1048| `ZOT_CHILD_SESSION` | Set to `1` for commands launched by a zot tool |
1049| `ZOT_SESSION_ID` | Current session ID, when session persistence is enabled |
1050| `ZOT_SESSION_FILE` | Absolute path to the current session file, when persisted |
1051| `ZOT_PROVIDER` | Current model provider |
1052| `ZOT_MODEL` | Current model ID |
1053| `ZOT_REASONING_LEVEL` | Current reasoning level, when configured |
1054
1055These values contain no credentials. Use `ZOT_AGENT` or `AI_AGENT` to distinguish execution inside zot from a standalone invocation; use the session and model values for diagnostics or attribution.
1056
1057Interactive shell commands receive model metadata even with `--no-session`; only session ID and file are omitted. Newly launched commands reflect session switches, working-directory changes, and reasoning-level changes. Turning reasoning off removes `ZOT_REASONING_LEVEL`. Already-running subprocesses retain the environment they inherited at launch.
1058
1059## Inline images
1060
1061When a tool returns an image (for example `read` on a PNG), zot renders it inline on terminals that support it: **Ghostty**, **Kitty**, **iTerm2**, **WezTerm**. On other terminals you see a text placeholder with MIME type, pixel dimensions, and byte size. Control with the `ZOT_INLINE_IMAGES` env var:
1062
1063| Value | Effect |
1064|---|---|
1065| unset (default) | Auto-detect based on `TERM_PROGRAM`; use the text placeholder inside VS Code and Herdr. |
1066| `iterm`, `iterm2` | Force the iTerm2 OSC 1337 protocol. |
1067| `kitty` | Force the Kitty graphics protocol. |
1068| `off`, `none` | Always use the text placeholder. |
1069
1070Herdr's Kitty graphics support is currently experimental, so zot does not enable inline images there automatically. After enabling `experimental.kitty_graphics` in Herdr, set `ZOT_INLINE_IMAGES=kitty` to opt in.
1071
1072Frames containing images are full-repainted (no differential diff) to prevent stale image pixels from lingering through scroll. That costs one terminal flash per image-containing frame; set `ZOT_INLINE_IMAGES=off` if that bothers you.
1073
1074## Tool rendering
1075
1076By default each tool call (bash, read, write, edit) renders inside a bordered panel — a `┌─ header ─┐`, `│`-prefixed body rows, and a `└─┘` footer. On a screen with many calls the borders can read as busy, so zot also offers a **flat** mode: a single quiet header line per call (`▌ bash …`) with indented, border-free output. Same information — tool name, arg summary, streamed output, the `... (N more lines, ctrl+o to expand)` truncation — just no frame.
1077
1078Set the `tool_render` key in `$ZOT_HOME/config.json`:
1079
1080```json
1081{
1082 "tool_render": "flat"
1083}
1084```
1085
1086| Value | Effect |
1087|---|---|
1088| unset / `"box"` (default) | Each tool call is wrapped in a bordered panel. |
1089| `"flat"` | Boxless: a quiet header line plus indented output. |
1090
1091The `ZOT_FLAT_TOOLS` env var overrides the config for a single run, which is handy for trying it without editing the file:
1092
1093| Value | Effect |
1094|---|---|
1095| `1`, `true`, `yes`, `on`, `flat` | Force flat rendering. |
1096| `0`, `false`, `no`, `off`, `box` | Force the bordered panel. |
1097| unset | Fall back to the `tool_render` config key. |
1098
1099```sh
1100ZOT_FLAT_TOOLS=1 zot # flat, just this run
1101ZOT_FLAT_TOOLS=0 zot # boxes, even if config.json says "flat"
1102```
1103
1104Either way, theme colors still drive the rendering (the header uses your accent/foreground, output uses the tool-output color) and `ctrl+o` still expands a truncated result.
1105
1106### Tool arg width
1107
1108The header line for a tool call shows the tool name plus a one-line summary of its primary argument — a `path`, a `command`, or a query. That summary is truncated to 60 cells by default (`web_answer What is the best architecture to implement resilience wit...`). On a wide terminal that can clip long queries more than you'd like, so set the `ZOT_TOOL_ARG_WIDTH` env var to raise (or lower) the limit:
1109
1110```sh
1111ZOT_TOOL_ARG_WIDTH=120 zot # allow up to 120 cells before truncating
1112```
1113
1114| Value | Effect |
1115|---|---|
1116| unset (default) | Truncate the arg summary at 60 cells. |
1117| integer in `[20, 500]` | Truncate at that many cells instead. |
1118| anything else | Ignored; falls back to the 60-cell default. |
1119
1120## Compact input
1121
1122By default a message you send renders as a padded, background-tinted bubble: a blank tinted row above and below the text, with a `▌` accent bar down the left. So even a one-line prompt occupies three rows. Set `compact_input` to collapse it to a single quiet `▌ your text` gutter line per wrapped row — no padding rows, no background tint.
1123
1124```json
1125{
1126 "compact_input": true
1127}
1128```
1129
1130| Value | Effect |
1131|---|---|
1132| unset / `false` (default) | Padded, background-tinted user bubble. |
1133| `true` | One quiet gutter line per wrapped row. |
1134
1135The `ZOT_COMPACT_INPUT` env var overrides the config for a single run (`1`/`true`/`on`/`compact` force compact; `0`/`false`/`off`/`bubble` force the bubble):
1136
1137```sh
1138ZOT_COMPACT_INPUT=1 zot # compact, just this run
1139```
1140
1141## Queued messages
1142
1143You can keep typing while the agent is working. Pressing `enter` during a turn queues the message instead of interrupting: it shows up above the status bar as `sliding in: <text>` and is delivered as the next user turn the moment the current one finishes. Queue as many as you want; they run in order. `esc` cancels the active turn and drops the queue so a runaway turn doesn't flood you with stale follow-ups; `ctrl+c` while busy arms the exit hint instead of interrupting, a second `ctrl+c` within two seconds exits zot.
1144
1145To recover the most recently queued message back into the editor (to tweak it before it runs), press `Option+↑`. In VS Code's integrated terminal that chord doesn't survive xterm.js's macOS key handling — use `Option+Shift+↑` there. zot's hint line under the sliding-in queue adapts automatically based on `$TERM_PROGRAM`.
1146
1147Slash commands also work while the agent is busy. Non-destructive ones (`/help`, `/jump`, `/btw`, `/sessions`, `/skills`, `/reasoning`, `/settings`, `/jail`, `/unjail`, `/exit`) take effect immediately. Destructive ones (`/new`, `/clear`, `/compact`, `/login`, `/logout`, `/model`, `/reload-ext`) cancel the active turn first and then run.
1148
1149
1150## Keys (interactive mode)
1151
1152### Input
1153
1154| Key | Action |
1155|---|---|
1156| `enter` | Submit (queued if the agent is busy). |
1157| `alt+enter` | Newline. |
1158| `tab` | Complete the selected slash command. |
1159| `esc` | Cancel the current turn (while busy); clear input (while idle). |
1160| `ctrl+c` | Clear the input and queue (while idle) or arm the exit hint (while busy). Press again within 2s to exit. Use `esc` to cancel a running turn. |
1161| `ctrl+d` | Exit on empty input. |
1162| `ctrl+l` | Redraw the screen. |
1163| `ctrl+v` | Paste clipboard text into the focused chat, side chat, dialog, filter, or credential input. In the main chat, image clipboard content is normalized to PNG and attached to the next prompt on macOS, Windows, Linux Wayland with `wl-paste`, and Linux X11 with `xclip`. Linux text paste also supports `xsel`; terminal-native bracketed text paste remains available without clipboard commands. |
1164| `ctrl+o` | Expand or collapse long tool results (read, write, edit, bash, glob outputs over ~12 lines). |
1165| `ctrl+1` ... `ctrl+9` | Switch to the model bound to that quick-model slot (configured in `/settings` -> model shortcuts). No-op while a turn is running. |
1166| `@` | Open the file picker. Browse files and directories in the working directory. |
1167
1168### File picker (`@`)
1169
1170| Key | Action |
1171|---|---|
1172| `@` | Open the file picker (type after a space or at the start of input). |
1173| `up`, `down` | Navigate the file list. |
1174| `right` | Open the selected directory. |
1175| `left` | Go back to the parent directory. |
1176| `enter` | Select the file or directory and insert it as a chip (`[file:name]` or `[dir:name/]`). |
1177| `esc` | Close the file picker. |
1178
1179Type `@` followed by a filter string to narrow the list (e.g. `@read` shows only entries containing "read"). Selected files are inserted as compact chips that expand to the full path on submit. Dragged-and-dropped files and directories also collapse to chips automatically.
1180
1181### Editor line navigation
1182
1183| Key | Action |
1184|---|---|
1185| `ctrl+a`, `ctrl+e` | Jump to start or end of line. |
1186| `alt+left`, `alt+right` | Jump one word back or forward. |
1187| `ctrl+u`, `ctrl+k` | Delete to start or end of line. |
1188| `ctrl+w`, `alt+backspace` | Delete the previous word. |
1189| `up`, `down` | Move within multi-line input. At the top edge, `up` recalls previous prompts and `down` moves forward through prompt history. |
1190
1191### Chat scroll
1192
1193| Key | Action |
1194|---|---|
1195| `pgup`, `pgdn` | Scroll one page up or down. |
1196| `up`, `down` (editor empty, not browsing prompt history) | Scroll three lines up or down. This is how the mouse wheel reaches the scroll logic on most terminals. |
1197
1198## Extensions
1199
1200zot can be extended in any language via a subprocess + JSON-RPC protocol. Extensions can register slash commands, expose tools to the model, intercept tool calls (block or rewrite args), gate whole turns before the model is called, and rewrite the assistant's visible text before it reaches the user. None are installed by default; opt in explicitly. Hot-reload any time with `/reload-ext`.
1201
1202### Install and manage
1203
1204```bash
1205zot ext install <path|git-url> # copy / clone into $ZOT_HOME/extensions/
1206zot ext list # show installed extensions
1207zot ext doctor # diagnose load, registration, and conflict issues
1208zot ext logs <name> [-f] # cat or tail the extension's stderr log
1209zot ext enable <name> # re-enable a disabled extension
1210zot ext disable <name> # disable without removing
1211zot ext remove <name> # delete an extension directory
1212```
1213
1214`zot ext doctor` keeps normal extension startup fail-soft, but gives
1215you an explicit troubleshooting view: manifest errors, disabled or
1216shadowed extensions, subprocess load errors, ready/auto-ready status,
1217registered commands/tools, registration conflicts, warnings, and the
1218stderr log path.
1219
1220For development, point `zot --ext <path>` at a working directory and skip the install step entirely. Repeatable; takes precedence over installed extensions of the same name.
1221
1222### Updating extensions
1223
1224`zot update` refreshes the zot binary **and** every installed extension that lives in a git checkout. Per-extension behaviour:
1225
1226- Disabled extensions are skipped.
1227- Extensions without a `.git/` directory (installed by `zot ext install ./local-path`) are skipped — there is no remote to pull from.
1228- For the rest, zot stashes any dirty worktree state (including untracked runtime files like `todos.json` or `config.json`), runs `git pull --ff-only`, and pops the stash. If the pop produces conflicts, the conflict markers are left in place and you'll see a warning.
1229- Diverged branches, offline pulls, or any other git failure are reported as `failed` and the next extension is processed. `zot update` itself never aborts because of an extension.
1230- zot does **not** run any build step (`go build`, `npm install`, `make`) after the pull. Extension authors are expected to commit the runnable artifact (binary, transpiled JS, etc.). If you need a build, rebuild manually and use `/reload-ext`.
1231
1232### Theme-only extensions
1233
1234An extension may ship only a theme: `extension.json` plus `theme.json` (or `themes/theme.json`) and no executable. zot loads it without spawning a subprocess and shows it in `/settings` with source information. See [docs/themes.md](docs/themes.md).
1235
1236### Reference
1237
1238`examples/extensions/` ships reference implementations in Go, TypeScript, Node, and shell. See [docs/extensions.md](docs/extensions.md) for the full protocol, the SDK API (`packages/agent/ext`), and the phase roadmap.
1239
1240## Skills
1241
1242A skill is a per-folder `SKILL.md` file with a YAML frontmatter header. zot discovers skills at startup, surfaces their names in the system prompt, and exposes a built-in `skill` tool the model uses to load the body on demand.
1243
1244By default zot loads built-in skills plus user-installed skills from:
1245
1246- `./.zot/skills/<name>/SKILL.md` (project)
1247- `$ZOT_HOME/skills/<name>/SKILL.md` (global)
1248- `./.claude/skills/<name>/SKILL.md`, `~/.claude/skills/<name>/SKILL.md` (Claude-compatible layout)
1249- `./.agents/skills/<name>/SKILL.md`, `~/.agents/skills/<name>/SKILL.md` (agent-compatible layout)
1250
1251See [docs/skills.md](docs/skills.md) for the frontmatter fields, authoring tips, and example skills under `examples/skills/`.
1252
1253## Telegram bot (bridge)
1254
1255zot can run as a telegram bot so you can DM it from your phone. Two ways to run it: **from inside the TUI** (the running session mirrors into Telegram) or **as a standalone background daemon** (a headless bot with its own independent agent).
1256
1257### From inside the TUI
1258
1259Type `/telegram` in the running TUI to open a picker with **connect**, **disconnect**, and **status**. When connected:
1260
1261- DMs from the paired user become prompts in the **same** session you're typing in, so you can continue a conversation from the terminal on your phone and back again.
1262- Messages you type in the TUI are mirrored into the Telegram thread prefixed `you: ...` and the assistant's replies come back prefixed `zot: ...`, so the Telegram chat stays a complete record of both sides of the conversation.
1263- Messages sent from Telegram show up as your own bubble in Telegram (no mirror) and the assistant's reply to them comes back bare (no prefix).
1264- The status bar shows a `- tg -` tag while the bridge is active.
1265- `/telegram connect` / `/telegram disconnect` / `/telegram status` (or `/tg`) also work as direct commands without the picker.
1266
1267The in-TUI bridge refuses to start while the standalone daemon (below) is running, since two concurrent long-poll consumers of the same bot race on every update and silently drop messages.
1268
1269### Standalone daemon
1270
1271For headless servers or long-running bots unattached to a TUI:
1272
1273```bash
1274zot telegram-bot setup # paste a BotFather token, verify, save
1275zot telegram-bot run # foreground: long-poll in this terminal (ctrl+c to stop)
1276zot telegram-bot start # background: detach and return immediately
1277zot telegram-bot stop # SIGTERM the background bot (SIGKILL after 5s)
1278zot telegram-bot logs -f # tail $ZOT_HOME/logs/bot.log (omit -f to just cat)
1279zot telegram-bot status # config (token masked) + running/stopped
1280zot telegram-bot reset # forget the token and paired user
1281# short alias: `zot tg ...` is accepted for every subcommand
1282```
1283
1284The background flavor writes the child's PID to `$ZOT_HOME/bot.pid` and redirects stdout and stderr to `$ZOT_HOME/logs/bot.log`. `zot telegram-bot stop` reads that PID, sends SIGTERM, waits up to five seconds, then escalates to SIGKILL if the child is still alive. Running two instances at once is refused at startup.
1285
1286> **Use the installed binary for `start`.** `go run ./cmd/zot telegram-bot start` won't work. `go run` builds a binary in a temp directory and deletes it when it exits, which kills the detached child. Run `make install` (or `go build`) first and invoke the installed binary.
1287
1288Setup flow:
1289
12901. Talk to [@BotFather](https://t.me/BotFather) on telegram, run `/newbot`, copy the token it gives you.
12912. Run `zot telegram-bot setup` and paste the token when prompted.
12923. Run `zot telegram-bot run` in the directory you want the agent to operate in.
12934. Open your bot on telegram, send `/start`. The first user to do this claims the bridge (stored as `allowed_user_id`); every other user is rejected.
1294
1295From then on, any DM you send is forwarded to the agent as a user prompt. Attached photos or `image/*` documents are downloaded and passed to vision-capable models. In-bot telegram commands are case-insensitive: `/help`, `/status`, `/stop` (cancel the current turn). Config lives in `$ZOT_HOME/bot.json` (mode 0600).
1296
1297Starting either Telegram bridge removes any webhook configured for that bot before long polling begins, while preserving pending updates. Telegram does not allow webhooks and `getUpdates` polling at the same time, so do not share the bot token with another service that expects to keep a webhook active.
1298
1299Bot mode respects the usual zot flags: `--provider`, `--model`, `--cwd`, `--reasoning`, `--continue`, `--no-session`, `--no-tools`, and so on. Run `zot tg run -c --model claude-opus-4-1` to resume the latest session on Opus, for example.
1300
1301### Architecture: protocol-agnostic bot core
1302
1303The messenger functionality is split in two layers. A generic, protocol-agnostic core lives in `packages/agent/modes/bot`: it owns the turn queue, agent prompting, built-in command dispatch (`/start`, `/help`, `/status`, `/stop`), status formatting, and per-turn credential refresh. Concrete transports implement the small `BotAdapter` interface (inbound polling, sending replies, a typing indicator, and optional protocol-specific status text); the Telegram support in `packages/agent/modes/telegram` is one such adapter.
1304
1305This means additional messaging backends (Discord, Slack, Signal, and similar) can be added by implementing `BotAdapter` in a new package and wiring up a subcommand. No changes to the runner, agent, or core are required. Channel IDs are opaque strings owned by the adapter, so the shared runner stays free of protocol-specific types.
1306
1307## Development
1308
1309```bash
1310make build # build ./bin/zot
1311make test # go test -race ./...
1312make lint # go vet + gofmt check
1313make fmt # gofmt -w .
1314make release # cross-compile linux/darwin/windows on amd64 and arm64
1315```
1316
1317Source layout (single Go module, four packages under `packages/`):
1318
1319```
1320cmd/zot/ main()
1321packages/provider/ LLM client surface, model catalog, streaming clients
1322packages/provider/auth/ credential store, api-key probe, oauth, login server
1323packages/core/ agent loop, sessions, cost tracking, compaction
1324packages/tui/ terminal raw-mode, input parser, editor, renderer, markdown, view
1325packages/agent/ cli wiring, arg parsing, system prompt, config
1326packages/agent/extensions/ extension subprocess manager
1327packages/agent/extproto/ extension wire-format types
1328packages/agent/modes/ interactive tui, print, json, dialogs
1329packages/agent/modes/bot/ protocol-agnostic bot runner (BotAdapter interface)
1330packages/agent/modes/telegram/ telegram adapter, api client, daemon
1331packages/agent/tools/ read, write, edit, bash, glob, sandbox
1332packages/agent/skills/ skill discovery, frontmatter parser, skill tool
1333packages/agent/swarm/ background subagent runtime
1334packages/agent/sdk/ public Go SDK for embedding zot in-process (package sdk)
1335packages/agent/ext/ public Go SDK for writing extensions (package ext)
1336```
1337
1338Downstream consumers can depend on individual packages:
1339`go get github.com/patriceckhart/zot/packages/core` pulls only `core` and its transitive deps (today: `provider`), no agent or TUI code.
1340
1341## License
1342
1343MIT