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).
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 gemini
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 --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, gemini, qwen, or aider. Switching agents clears the stored model and endpoint. |
--model <name> |
Model name. Optional for Codex, Claude and Gemini; needed for Qwen Code and Aider |
--api-key <key> |
API key. Optional for Codex and Claude, 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 and aider; 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 (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
npm install -g @google/gemini-cli # Gemini CLI
npm install -g @qwen-code/qwen-code # Qwen Code
curl -LsSf https://aider.chat/install.sh | sh # Aider
Zeltro passes a stored key only where it fits. Codex gets it as OPENAI_API_KEY only if it starts with sk-, and Claude gets it as ANTHROPIC_API_KEY only if it starts with sk-ant-. Anything else is ignored with a warning, and the CLI uses its own sign-in. Gemini never gets the key or the endpoint: it uses its own Google sign-in, or a GEMINI_API_KEY you export yourself.
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.
Aider#
Codex, Claude and Gemini 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.
Unattended mode#
By default an 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.
Zeltro does not turn those prompts off behind your back. 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" |
| Gemini CLI | ~/.gemini/settings.json |
"autoAccept": true |
| Qwen Code | ~/.qwen/settings.json |
"autoAccept": true |
| Aider | ~/.aider.conf.yml |
yes-always: true |
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. For throwaway containers and CI, ZELTRO_AI_AUTO_APPROVE=1
adds each agent's bypass flag to a single run instead.
With unattended mode on, the agent can change anything your user account can. Only use zeltro ai, zeltro create and zeltro resume in directories you're comfortable letting an AI modify freely. All three follow this setting; ZELTRO_AI_AUTO_APPROVE=1 overrides it for one run.
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, gemini, aider or qwen |
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, so it also holds on zeltro resume. Gemini CLI has no way to
add to its system prompt, so zeltro ai puts the instruction at the top of the prompt
and a resumed Gemini 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.
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 ask you things through the app's window instead of the chat: a multiple-choice question (zeltro gui ask), or an API key or password (zeltro gui secret), which goes straight into the project's .env so it never shows 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.