AI workflow
Zeltro's command surface is shaped around AI-driven development. The point is to give an agent a fixed environment to work inside, so it builds your app instead of inventing infrastructure.
Zeltro doesn't sell AI. It drives an agent CLI you install and sign in to, and you pay that provider directly (or nothing, with a local model).
Don't have one yet? The first time you open the Zeltro app, you can use our key to try it out: Claude Code on Claude Haiku, with no account, no card and no key of your own, enough to try it on a first project (one per computer, while it lasts). It runs through Zeltro's server (see privacy). After that, connect your own AI the same way as above; nothing goes through Zeltro then.
Choosing an agent#
zeltro ai-set --agent claude --model claude-opus-5-5
zeltro ai-set --agent codex --model gpt-6-sol
zeltro ai-set --agent agy
zeltro ai-set --agent aider --model anthropic/claude-sonnet-5 --api-key sk-ant-...
zeltro ai-set --agent qwen --model qwen/qwen3-coder-30b-a3b-instruct --api-base https://openrouter.ai/api/v1 --api-key sk-or-...
zeltro ai-set --agent opencode --model openai/gpt-5.4-mini --api-key sk-...
zeltro ai-set --agent hermes --model openrouter/qwen/qwen3-coder --api-key sk-or-...
zeltro ai-set --json-output # inspect current settings (read-only)
A fresh install has no agent set, and the AI commands tell you to run zeltro ai-set first. If you're unsure, pick claude and leave the model blank so Claude Code uses its own default.
| Flag | Description |
|---|---|
--agent <name> |
codex, claude, agy, qwen, aider, opencode or hermes. gemini still works but is deprecated (see Antigravity CLI). Switching agents clears the stored model and endpoint. |
--model <name> |
Model name. Optional for Codex, Claude, agy, Gemini, OpenCode and Hermes; needed for Qwen Code and Aider, and for OpenCode and Hermes with --api-base. OpenCode and Hermes take provider/model, e.g. openai/gpt-5.4-mini |
--api-key <key> |
API key. Optional for Codex, Claude, agy, OpenCode and Hermes, which can use their own sign-in; needed for Aider and for a hosted Qwen Code model. --api-key "" clears a stored key |
--api-base <url> |
Custom endpoint. OpenAI-compatible for qwen, aider, opencode and hermes; Anthropic-compatible for claude. none clears it. See Cheap and local models for Codex |
--install-only |
With --agent: install that agent's CLI if it's missing, without making it the default |
--allow-unattended / --no-allow-unattended |
Turn the agent's approval prompts off or back on in its own config, for terminal use. In the app, each project's "Works without asking" switch (on by default) decides instead (see Unattended mode) |
When you pick an agent that isn't installed, zeltro ai-set installs it with:
curl -fsSL https://claude.ai/install.sh | bash # Claude Code
npm install -g @openai/codex # Codex
curl -fsSL https://antigravity.google/cli/install.sh | bash # Antigravity CLI (agy)
npm install -g @qwen-code/qwen-code # Qwen Code
curl -LsSf https://aider.chat/install.sh | sh # Aider
npm install -g opencode-ai # OpenCode
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash -s -- \
--non-interactive --skip-browser --skip-computer-use # Hermes Agent
Zeltro passes a stored key only where it fits. Codex gets a key only if it starts with sk- (any key with a custom endpoint), and Claude gets it as ANTHROPIC_API_KEY only if it starts with sk-ant-. Codex ignores OPENAI_API_KEY, so Zeltro signs it in with the key (codex login --with-api-key) in a Codex home of its own, ~/.zeltro/codex/<hash of the key>, with your config.toml linked in. Your own ~/.codex, and any ChatGPT sign-in in it, is never touched, and each key keeps its own session history for zeltro resume. Anything else is ignored with a warning, and the CLI uses its own sign-in. agy and Gemini get a stored key as GEMINI_API_KEY (a Gemini API key from aistudio.google.com); agy takes no custom endpoint.
zeltro ai-set --json-output on its own is read-only. It installs nothing and writes nothing, and reports the agent, model, endpoint, whether a key is stored, the unattended setting, and which agents are installed.
Running cheaper or local models? See Cheap and local models. Qwen Coder on OpenRouter costs roughly 30x less per token than Claude Sonnet 5, and a local Ollama model costs nothing to run.
Settings live in /etc/zeltro-cli/.env. Always change them with zeltro ai-set rather than editing the file.
Antigravity CLI (agy)#
agy is Google's coding agent CLI, and it replaces Gemini CLI. Google stopped serving
Gemini CLI to personal Google accounts (free, AI Pro and Ultra) on 2026-06-18. Gemini CLI
still works with a paid Gemini API key, so zeltro ai-set --agent gemini keeps working,
but it's deprecated in Zeltro.
zeltro ai-set --agent agy # sign in with Google the first time you run agy
zeltro ai-set --agent agy --api-key AIza... # or a Gemini API key (aistudio.google.com)
It installs as one program in ~/.local/bin (no Node needed) and updates itself. To sign in
with Google, open a terminal and run agy once. Its first interactive start also asks for a color
scheme and shows Google's terms, where you choose whether Google may use your conversations;
Zeltro leaves that choice to you. With a key, Zeltro sets "modelProvider": "gemini" in
~/.gemini/antigravity-cli/settings.json (agy ignores the key without it), and removes
it again when you clear the key, so Google sign-in works again. agy takes no custom
endpoint, so it can't run cheap or local models through --api-base.
Aider#
Codex, Claude and agy each sign in with their own account. Aider doesn't. It talks straight to a provider API, so it needs a model and a key before it will run:
# a hosted provider (the model prefix picks it)
zeltro ai-set --agent aider --model anthropic/claude-sonnet-5 --api-key sk-ant-...
# a local OpenAI-compatible server
zeltro ai-set --agent aider --model openai/qwen3-coder:30b \
--api-key ollama --api-base http://localhost:11434/v1
Zeltro tags the key with the provider from the model prefix (anthropic=sk-ant-...),
or with openai when there is no prefix. --api-base is only for OpenAI-compatible
servers (Ollama, LM Studio, OpenRouter, vLLM); leave it blank to use a provider's own
hosted API. Aider normally commits each edit itself. Zeltro runs it with
--no-auto-commits so its changes sit in your working tree like every other agent's.
Qwen Code#
Qwen Code is an open-source terminal agent
that started as a fork of Gemini CLI and is built around the Qwen Coder models.
Zeltro installs it with npm install -g @qwen-code/qwen-code.
Zeltro runs it in OpenAI-compatible mode (--auth-type openai, with the key and
endpoint in OPENAI_API_KEY and OPENAI_BASE_URL), so it is the simplest route to a
cheap hosted model or a local one:
# hosted, pay-as-you-go
zeltro ai-set --agent qwen --model qwen/qwen3-coder-30b-a3b-instruct \
--api-base https://openrouter.ai/api/v1 --api-key sk-or-...
# local Ollama
zeltro ai-set --agent qwen --model qwen3-coder:30b \
--api-base http://localhost:11434/v1 --api-key ollama
Qwen's free OAuth sign-in was discontinued on 15 April 2026, so it needs an API key
or a local endpoint. There is no free hosted route through Qwen itself. It also wants
Node 22+; it runs on Node 20 with an EBADENGINE warning, but that is unsupported.
OpenCode#
OpenCode is an open-source terminal agent that works with almost
any provider. Zeltro installs it with npm install -g opencode-ai. It can use its own
sign-in (opencode auth login), or a key you give Zeltro:
# a hosted provider: the model is provider/model, as OpenCode names it
zeltro ai-set --agent opencode --model openai/gpt-5.4-mini --api-key sk-...
zeltro ai-set --agent opencode --model openrouter/deepseek/deepseek-v4.1-flash --api-key sk-or-...
# any OpenAI-compatible server (the model is the name that server uses)
zeltro ai-set --agent opencode --model qwen3-coder:30b \
--api-base http://localhost:11434/v1 --api-key ollama
The key goes to the variable the model's provider reads (OPENAI_API_KEY,
ANTHROPIC_API_KEY, OPENROUTER_API_KEY or GOOGLE_GENERATIVE_AI_API_KEY). An
endpoint becomes an OpenAI-compatible provider called zeltro, passed in
OPENCODE_CONFIG_CONTENT for that run only, so nothing is written to your OpenCode
config. Zeltro also turns OpenCode's auto-update off for its runs. OpenCode reads the
project's AGENTS.md by itself.
Hermes Agent#
Hermes Agent by Nous Research is an open-source
agent with a full coding tool set. Zeltro installs it with its official script, without
the optional browser and computer-use extras, into ~/.local/bin/hermes.
# a provider prefix picks Hermes's provider: openai/, openrouter/, anthropic/ or gemini/
zeltro ai-set --agent hermes --model openai/gpt-5.4-mini --api-key sk-...
zeltro ai-set --agent hermes --model openrouter/qwen/qwen3-coder --api-key sk-or-...
# any OpenAI-compatible server
zeltro ai-set --agent hermes --model qwen3-coder:30b \
--api-base http://localhost:11434/v1 --api-key ollama
Without a prefix the model goes to whatever provider Hermes is set up for. An endpoint
uses Hermes's openai provider with OPENAI_BASE_URL. Hermes reads the project's
AGENTS.md by itself. Meta's Hermes JavaScript engine also installs a command called
hermes; Zeltro checks it has the agent (hermes --version says "Hermes Agent") before
it runs one.
Unattended mode#
On its own, an AI agent asks before each file edit or command. That stalls
zeltro create, which hands the agent a task and expects it to finish on its own.
In the desktop app: on by default, per project#
Each project has a Works without asking switch in its panel, and it is on by default. With it on, the project's AI works without stopping to ask. Turn it off for a project to approve each step yourself in the agent's terminal.
The app does this one session at a time. When it starts a project's agent it sets
ZELTRO_AI_UNATTENDED=1 (switch on) or 0 (switch off), and Zeltro turns that into
the agent's own flag for that run:
| Agent | Switch on | Switch off |
|---|---|---|
| Claude Code | --dangerously-skip-permissions |
--permission-mode manual |
| Codex | --dangerously-bypass-approvals-and-sandbox |
--ask-for-approval on-request --sandbox workspace-write |
| Qwen Code | --yolo |
--approval-mode default |
| Gemini CLI (deprecated) | --yolo --skip-trust |
--approval-mode default |
| OpenCode | --auto |
OPENCODE_PERMISSION={"*":"ask"} |
| Aider | --yes-always |
--no-yes-always |
| Antigravity CLI (agy) | --dangerously-skip-permissions |
(no way to force it for one run: its own config decides) |
| Hermes Agent | --yolo |
(no way to force it for one run: its own config decides) |
It doesn't change the agents' approval settings. The one thing it writes is Claude
Code's one-time consent, "skipDangerousModePermissionPrompt": true in
~/.claude/settings.json, so Claude doesn't stop at its "bypass permissions" warning
screen. The app has no machine-wide switch.
In a terminal: the agent's own setting#
zeltro ai, zeltro create and zeltro resume follow the agent's own config unless
ZELTRO_AI_UNATTENDED is set (1 or 0, for that run, as above). The first time
zeltro ai-set sets up an agent in a terminal, it asks once, and if you say yes it
writes the setting into the agent's own config file, where you can see and undo it:
| Agent | File | Setting |
|---|---|---|
| Claude Code | ~/.claude/settings.json |
"permissions": {"defaultMode": "bypassPermissions"} |
| Codex | ~/.codex/config.toml |
approval_policy = "never", sandbox_mode = "danger-full-access" |
| Antigravity CLI (agy) | ~/.gemini/antigravity-cli/settings.json |
"toolPermission": "always-proceed", "artifactReviewPolicy": "always-proceed" |
| Gemini CLI (deprecated) | ~/.gemini/settings.json |
"autoAccept": true |
| Qwen Code | ~/.qwen/settings.json |
"autoAccept": true |
| Aider | ~/.aider.conf.yml |
yes-always: true |
| OpenCode | ~/.config/opencode/opencode.json |
"permission": "allow" |
| Hermes Agent | ~/.hermes/config.yaml |
approvals.mode: off (set with hermes config set; turning it back on restores smart) |
zeltro ai-unattended # allow it for the current agent
zeltro ai-unattended codex --status # report without changing anything
zeltro ai-unattended codex --revoke # turn the prompts back on
zeltro ai-set --allow-unattended and --no-allow-unattended do the same thing as
part of a settings change. In the app, a project's switch wins over this setting, except
for agy and Hermes with the switch off.
Working without asking, the agent can change anything your user account can. Only let it do that in projects you're comfortable letting an AI modify freely.
Per-session overrides#
zeltro ai-set is global. To use a different agent, model or key for one run without
changing it, set these in the environment. They are never written to the .env:
| Variable | Overrides |
|---|---|
ZELTRO_AI_AGENT |
The agent: codex, claude, agy, aider, qwen, opencode, hermes or gemini (deprecated) |
ZELTRO_AI_MODEL |
The model |
ZELTRO_AI_API_BASE |
The endpoint |
ZELTRO_AI_API_KEY |
The key |
ZELTRO_AI_API_KEY_FILE |
The key, read from the first line of this file so it never shows in a process list. Wins over ZELTRO_AI_API_KEY. Keep the file until the session ends |
ZELTRO_AI_LANGUAGE |
The language the agent replies in, e.g. Spanish. Code and commands stay as they are, and Zeltro's own output stays English |
ZELTRO_AI_AGENT=qwen \
ZELTRO_AI_MODEL=qwen/qwen3-coder-30b-a3b-instruct \
ZELTRO_AI_API_BASE=https://openrouter.ai/api/v1 \
ZELTRO_AI_API_KEY_FILE="$HOME/.openrouter-key" \
zeltro ai "Add a health-check endpoint at /ping"
Unset means "use the ai-set value"; set but empty means "clear it for this run".
An unknown agent, an unreadable key file, or an agent that isn't installed stops the
command before anything runs. Nothing is installed for you; use
zeltro ai-set --install-only --agent <name> to add an agent without making it the default.
The language instruction reaches Claude Code and Qwen Code through
--append-system-prompt, Codex through -c developer_instructions=..., and Aider
through --chat-language, and Hermes through HERMES_EPHEMERAL_SYSTEM_PROMPT, so it
also holds on zeltro resume. agy, Gemini CLI and OpenCode have no way to add to their
system prompt, so zeltro ai puts the instruction at the top of the prompt and a resumed
agy, Gemini or OpenCode session gets none.
In the Zeltro app, AI profiles (Settings → AI Profiles) are named agent, model,
endpoint and key setups you pick per session. Keys are stored encrypted, and a
profile reaches the CLI through these same variables. The built-in default profile
uses the host's zeltro ai-set configuration.
zeltro create: idea to running project#
zeltro create "A timeclock for employees in Django"
zeltro create "Set up n8n and configure a webhook that posts to Slack"
zeltro create "https://github.com/monicahq/monica"
create runs in three steps:
- Classify. Your agent is asked only which stack fits, and answers in JSON: one framework to build it from scratch, any ready-made apps from Zeltro's catalogue that genuinely fit, a database, and a project name. In a terminal you pick from a menu. Scripts and
--one-offtake the top recommendation. - Create. Zeltro itself runs
zeltro neworzeltro install. No AI is involved in this step. - Build. Zeltro writes the project's
AGENTS.mdand hands your original idea to the agent inside the project as a one-offzeltro airun. The agent builds with framework-native conventions, updates the README, restarts the app and checks it responds before finishing.
If the idea only asks for an app to be installed ("set up Grafana"), step 3 is skipped: the installer has already checked the app responds and printed its URL and login.
Input options#
zeltro create "an idea" # argument
zeltro create -f spec.md # from a file
zeltro create < spec.md # stdin
cat spec.md | zeltro create # pipe
zeltro create --one-off "..." # no menus; take the top recommendation
zeltro create --classify-only "..." # print the recommendation and stop; creates nothing
--classify-only never prompts. Add --json-output for a machine-readable result, which is what the Zeltro app uses. If the agent itself fails (a wrong key, a model your account can't use, no credit left), the result is {"status":"error","error":"agent_failed","message":"Codex: <the agent's own error>"}, and Zeltro stops as soon as the agent reports a 400, 401, 403 or 404 rather than waiting out its retries. "error":"no_stack" means the agent answered but gave no usable recommendation. create reports the same codes.
The AGENTS.md hand-off#
When a project is created by create, new, clone or install, Zeltro writes an AGENTS.md into the project directory. For new, clone and install it then cds into the project and hands your agent a prompt whose only job is to read that file. --one-off, --json-output or a non-terminal run skips that hand-off.
AGENTS.md records what an agent needs to pick the project up cold:
- Local URL, container name, project directory
- The resolved database, read from the project's
.env - Command patterns that work inside the container
- Shared-service hostnames and credentials
- Rules (never
--json-output,python3notpython, verify with curl before declaring done)
Because this context lives on disk rather than in a chat session, it survives closing your terminal, rebooting, and switching between agents.
The file is regenerated on each hand-off, but only between its markers. Anything you write outside the generated block is preserved.
zeltro ai: a prompt in the current project#
cd ~/zeltro-projects/my-app
zeltro ai "Add a health-check endpoint at /ping"
zeltro ai --interactive "Let's refactor the auth flow"
One-off is the default: the agent receives the prompt, does the work, and exits. Durable context lives in AGENTS.md, so each prompt stands alone.
| Flag | Description |
|---|---|
--interactive, -i |
Open a persistent session instead |
--one-off |
Accepted for compatibility (now the default) |
An unknown flag is an error rather than being added to the prompt.
zeltro resume#
zeltro resume my-project
Starts the project if it isn't running, then reopens the agent's most recent session in that project directory. If the agent can't resume, it starts a new session.
Messaging other agent sessions#
When the Zeltro app hosts agent sessions in several projects, on one host or several, they can message each other. Run these from inside your project directory:
zeltro peers # live sessions, as project@host
zeltro send blog api@shop -- "API is on v2 now"
zeltro send --all -- "Shared Postgres restarting in 5 minutes"
cat notes.md | zeltro send blog -- - # message from stdin
Every target must be live or nothing is sent. Messages are capped at 16 KB. An incoming message shows up in the terminal as [Zeltro message from shop@shawn to blog, api] .... It comes from another agent, not from you.
In the Zeltro app, agents can also use the app's window for two things: notices (zeltro gui notify), such as the login for an admin account they set up or "your change is ready", which pop up and stay in the project's Notices list; and API keys or passwords (zeltro gui secret), which go straight into the project's .env so they never show up in the conversation. See the command reference for details.
Notes for agents#
If you are an agent working with Zeltro, read /usr/local/share/zeltro-cli/AGENTS.md. It's the condensed platform reference, kept deliberately short because every byte is paid for on each run.
The rules that matter most:
- Never pass
--json-output. It suppresses all human-readable output including the success/failure distinction, so you can't tell whether a command worked. It exists only for external scripts and GUIs. - Always pass explicit arguments. Nothing prompts; a missing required argument is a hard error with a usage hint.
- Prefer
zeltro execover the TTY variants (zeltro bash,zeltro exec-tty*). Those allocate a terminal and aren't automation-friendly. zeltro supervisor restart all, neverzeltro exec supervisorctl ....python3, notpython. For Django, preferzeltro django manage <args>.
Spotted a mistake? Edit this page on GitHub.