An autonomous coding agent that lives in your terminal.
It reads your repo, runs commands, writes files, chases down bugs and keeps going until the job is done — while you watch it work.
by Mr-Destroyer / ZIM
See it
Cold start
Banner reveal, a live system check, then the shell takes over.
The shell
*A transcript that streams, a tool card that shows the call while it runs, and two rails that keep the loop and the cost in view. The left rail is the agent's state — think, call, observe, idle. The right rail is the bill: throughput, how full the context is, what it has cost, which files it touched.*
Swap engines mid-session. |
Change how much rope it gets. |
What it does
ZIMZILLA is a coding agent you drive from a terminal. You describe what you want; it works out the steps, does them, and shows you everything it did.
- Talks, then acts. Ask a question and it answers. Ask for a change and it
- Sees your project. Point it at any directory and it works there: it can
- Runs real commands. A full shell, wired straight into the loop, with
- Edits like a human. Changes arrive as diffs you can read before they land.
- Shows its work. Every command, every file touched, every result — visible
- Reads its own markdown. A model answers in headings, bold, lists, tables
Asset: arrives as a bold
Asset, a dash-and-space becomes a bullet, a pipe grid becomes a real
table that wraps, and a fenced block arrives syntax-highlighted in the same
theme tool output uses. It streams that way too, so a half-arrived answer
reads as prose rather than as markup.
- Keeps score. Live token counts and a running cost estimate, per turn and
- Answers to a dial. A mode switch decides whether it merely investigates,
- Stays in bounds. An optional scope file declares what it may touch, and a
- Remembers the session. Save it, load it, compact it when the context
- Looks the part. A green-on-black terminal, animated from boot to prompt.
Install
One line:
curl -fsSL https://raw.githubusercontent.com/Mr-Destroyer/ZimZilla/main/install.sh | bash
That clones ZimZilla into ~/.local/share/zimzilla, builds its own virtualenv
there and installs the dependencies into it, then drops a zimzilla launcher on
your PATH (~/.local/bin). Nothing to activate and nothing to pip install
afterwards — the launcher finds that venv itself and runs from any directory:
zimzilla # launch
zimzilla -C ~/some/project # run against another directory
zimzilla --mode zim # full auto, follows AGENTS.md
zimzilla --allow allow.yaml # declare your targets
zimzilla --deny out-of-scope.yaml # hosts that are never touched
If ~/.local/bin is not on your PATH, the installer prints the line to add.
Re-running the same command updates an existing install.
The installer finishes by running ./setup.sh, which configures the route
without asking you anything: it seeds ~/.zimzilla/logfare/source from the
shared key shipped in packaging/logfare/source, so a new install is a session
away with no signup. Put your own key in that file (or export lfu_… as
ANTHROPIC_AUTH_TOKEN) whenever you want the shared one gone — an existing
profile is never overwritten.
The shared key is exactly that, shared. Every install uses it against one
per-account budget — **20 requests/minute, 500/hour, 2,500/day, three
concurrent** — and a busy day can exhaust it. When that happens Logfare answers
429 with a Retry-After (the budget resets at 00:00 UTC); ZimZilla reports the
wait rather than retrying into it. Your own key has its own budget; put one in
the profile whenever you want more headroom.
Logfare is a full route, not just the starting point. /zim-logfare switches
back to it from any other upstream, and the choice is remembered. Every chat
model Logfare serves is available through it — /logfare-models lists what is
up right now, with uptime, and /model offers the same list. The proxy config
also carries a wildcard route, so a model Logfare adds later works without
waiting for a ZimZilla release.
Other upstreams are /zim-tokenharbour, /zim-openrouter, /zim-unorouter,
/zim-aihubmix, /zim-beatapi and /zim-antseed, each with your own credential
or none at all.
To configure setup yourself later, or to install somewhere other than the default:
curl -fsSL <install.sh-url> | ZIMZILLA_SKIP_SETUP=1 bash
curl -fsSL <install.sh-url> | ZIMZILLA_INSTALL_DIR=~/zimzilla bash
install.sh honours ZIMZILLA_REPO, ZIMZILLA_INSTALL_DIR, ZIMZILLA_BIN_DIR,
ZIMZILLA_SKIP_DEPS and ZIMZILLA_SKIP_SETUP.
From source
Working on ZimZilla itself, you want the checkout you edit to be the one that runs. Same launcher, without the clone step:
git clone <this-repo> ~/zimzilla && cd ~/zimzilla
python3 -m venv .venv && pip install -r requirements.txt
./setup.sh # verify the route end-to-end
./run.sh # launch, no activation
requirements.txt ends with -e ., so that pip install also installs the
package and drops a zimzilla command into your venv's bin/. The launcher
uses the checkout's .venv when it exists, then an activated $VIRTUAL_ENV,
then python3 on PATH — so a venv of your own works too.
requirements.txt covers the harness only. The LiteLLM proxy is installed
separately, into its own venv (~/.zimzilla/venv, built by ./setup.sh),
because the two cannot share one environment: litellm[proxy] requires
rich<14.0 while textual 8.x requires rich>=14.2, and a set listing both does
not merely resolve badly — it fails to resolve at all, so nothing installs. If a
litellm is already on your PATH or at ~/.local/bin/litellm, setup.sh
reuses it instead of building the proxy venv.
zimzilla reads its credentials from the environment. The launcher sources the
profile ./setup.sh wrote before the harness starts, so a normal launch needs
nothing exported by hand.
Out of the box it runs deepseek-v4.1-flash against Logfare. Switch any
time with /model, or override before launch:
zimzilla -m qwen-3.8-27b # this run only
ANTHROPIC_MODEL=$MODEL_QWEN zimzilla # via a profile alias
Logfare retires models without notice, so ZimZilla fetches the live list from
Logfare's own status API rather than trusting a baked-in one. /logfare-models
shows what is up right now, with uptime, and /model is built from it — and the
fetched list expires after five minutes, so a long session cannot keep offering
a catalog that has since moved on. /model says how old its list is
(catalog live (2m old), catalog expired (1h old)) rather than asserting that
it is current.
Every chat model in that catalog is selectable. At the time of writing that is
nine: deepseek-v4.1-flash, deepseek-v4-pro-0813, gemma-4-26b, glm-5.3,
glm-5.3-flash, kimi-k2.6, claude-haiku-5-5, logfare/auto and
qwen-3.8-27b. The proxy config names each one and carries a * wildcard, so a
model Logfare adds later routes without a new ZimZilla release — the wildcard is
a fallback, never preferred over a named entry.
Two upstream behaviours are worth knowing before you pick one. **Premium models
need a one-time training opt-in** on the Logfare account; until it is granted the
upstream answers 403 training_optin_required, which is the account's state and
not a fault in ZimZilla. And the shared key has a daily request budget, so a
busy day answers 429 with a Retry-After until 00:00 UTC — the harness reports
the wait instead of retrying into it, and your own key has its own budget.
Updates
ZimZilla updates itself on launch. Because requirements.txt ends in -e .,
the running code is the git checkout — so "updating" means fast-forwarding
that checkout, not reinstalling anything. Every launch checks the upstream
first; if there are new commits it says so, pulls them, and restarts on the
new code before the harness comes up.
⟩ UPDATE
◈ updated by 3 commits
3be2d0c docs(readme): troubleshooting for the pip cache trap
4a58d45 docs(readme): say which model it starts on
· restarting on the new code…
Launching ZimZilla pulls and runs code from your origin. That is the
honest description of the feature — it is safe only insofar as you trust the
remote you cloned from, the same trust you extend by running git pull by
hand. It is on by default because that is what makes it useful; turn it off
with either:
zimzilla --no-update # this launch only
export ZIMZILLA_NO_UPDATE=1 # every launch in this shell
What it will and will not do:
| | |
|---|---|
| New commits, clean tree | fast-forwards (--ff-only) and restarts on the new code |
| New commits, edited tracked files | reports them and does not pull — your uncommitted work is never touched, stashed or reset |
| Untracked files | ignored — a fast-forward cannot lose them, so they do not block the update |
| Your branch has diverged | reports it and does not pull — never merges, never rebases |
| Offline, no remote, not a checkout | says nothing and boots normally |
| requirements.txt changed | updates, then installs the new dependencies into its own venv and restarts; a bring-your-own venv is told to pip install -r requirements.txt |
If you are developing ZimZilla itself, ZIMZILLA_NO_UPDATE=1 in your shell
profile is the setting you want — otherwise a commit you push from elsewhere
will restart the checkout under you on the next launch.
Usage
--model, -m NAME model to use
--workdir, -C DIR working directory
--mode NAME auto | edits | plan | zim | danger | uncensored (default: auto)
--allow PATH declared targets — arms the session
--deny PATH hosts that are never touched
--agents PATH the doctrine file zim mode follows (default: workdir,
its parent, ~/.zimzilla, then ~)
--theme {green,amber,cyan,system}
--rain start with matrix rain on
--no-rain no rain anywhere
--no-think never show the model's own reasoning
--max-tokens N max output tokens per reply
--no-auto-compact never compact history automatically (/compact still works)
--unsafe disable the filesystem jail
--panes run under tmux; ctrl+b h/v open more ZimZilla panes
--resume SESSION restore a saved session at launch (used by /panes)
--list-models
--version
Panes
zimzilla --panes hands the session to tmux on a private socket, so the panes
do not collide with your own tmux. ctrl+b h splits right and ctrl+b v
splits down, each new pane running another ZimZilla that shares a mailbox with
its siblings — so the panes' agents can message each other with agent_send.
The status bar carries the reminder.
The prefix keys are tmux bindings, not ZimZilla bindings — they only exist
in a session tmux started. In an ordinary session ctrl+b is not a prefix at
all, so ctrl+b h types an h into the prompt. If you are already in a
session and want panes, type /panes: it saves the conversation, re-opens the
same session under tmux, and replays the transcript there, so you carry on
where you were. Panes opened afterwards are fresh agents — they can ask their
peers what is going on rather than being handed a copy of your history.
**If you launch it from inside an existing tmux session, the prefix needs
pressing twice.** The outer server reads ctrl+b first and never forwards it,
so a single ctrl+b h splits your session and the ZimZilla bindings look
dead. ctrl+b ctrl+b h forwards one prefix inward and does what you meant; the
status bar switches to saying so when it detects the nesting. Both forms are
harmless standalone, so it is safe to just press it twice out of habit.
A pre-existing server on the private socket would otherwise keep tmux's stock
key table — tmux -f is read only when a server starts — so --panes sources
its config into whatever server it finds before attaching.
opencode, behind a rotating IP
/zim-opencode opens opencode in this terminal,
egressing from a different address each time. No account, no API key: opencode
serves a set of free models itself, and they answer with no credentials on disk
at all.
It does three things:
- Checks the binary. Missing, it says exactly
opencode not found - install it please ! and prints the install lines that
work (curl -fsSL https://opencode.ai/install | bash, mise use -g opencode,
brew install opencode). It never half-runs.
- Installs the
opencode-zimalias into~/.local/bin— a small shim that
- Rotates the exit IP and hands over the terminal. The pool's cursor is
/zim-opencode # open the TUI behind the next proxy
/zim-opencode --refresh # rebuild the proxy pool first
/zim-opencode --model <id> # a different free model
/zim-opencode -- --auto --agent build # everything after -- goes to opencode
Nothing global is touched. The proxy is passed to the child process through
its environment, so only opencode egresses through it — your shell, your other
programs and your real address are unaffected. This is not proxychains: no
LD_PRELOAD, no root, no whole-process-tree hook.
Why not Tor. opencode's free tier refuses Tor exit IPs — the same request
that succeeds through an ordinary proxy comes back *"OpenCode's free tier can
only be used from within OpenCode"* through Tor. Tor publishes its exit list, so
refusing it is one line for anyone. That is also why a commercial VPN works
where Tor does not, and it is what makes ordinary HTTP proxies usable here.
(SOCKS is not an option regardless: opencode's fetch layer rejects socks5://
outright, which is another reason the pool is plain HTTP.)
The gate is not a datacenter block, though it looks like one: residential and datacenter addresses scored identically when tested (2/6 each), so filtering proxies by IP type buys nothing. What it refuses is a specific set of addresses at a specific moment, which is why the only reliable test is a real turn.
The free models need no account — verified properly. Copy
~/.local/share/opencode and ~/.config/opencode to a temp HOME, delete
auth.json, and it still answers. (An empty $HOME does not prove this: a
fresh home has no config or model cache, so it fails for unrelated reasons. And
having an auth.json is not evidence against it — the key is simply unused for
these model ids.)
It is honest about the odds. The thing that decides whether a launch works is not whether a proxy carries traffic — it is whether opencode's free tier accepts that exit IP, and the two are close to independent. Measured: a pool validated on liveness alone ran 12% usable, while the same pool re-validated with real turns ran 79%. So the command validates in two stages — a cheap fetch to cut thousands of candidates down, then a real one-shot opencode turn on the survivors, in parallel. It also tells the three failures apart, because only two are worth retrying:
| What came back | Verdict | |---|---| | the free-tier refusal, a TLS/certificate error, a dead connection | retry the next proxy | | "Model is unavailable" | stop — the id is retired, another proxy will not help | | nothing worked | say so, with the reason, rather than opening a TUI that will fail |
Working proxies are not volatile — re-tested hours later, four of five still answered — so the pool remembers: every proxy that completes a turn is timestamped and tried first next time. The pool gets cheaper the more it is used.
Those two goals contradict each other, and the naive version of the first
silently destroys the second. A pool built by turn-validation has every entry
proven, so "prefer a proven proxy" returns the same one on every launch — the
first cut of this did exactly that, handing back an identical address five times
out of five. Rotation is therefore tracked separately: proven_at records
whether a proxy works, used_at records whether you have already been given it,
and each launch takes a proven proxy you have not had yet. Only when they run
out does the pool cycle, so a small pool degrades into reuse rather than into
failure. Both markers persist, so a restart does not reset the rotation.
A refusal retires a proxy, because it is newer evidence than whatever proof put it at the front of the queue. Free proxies die without warning, and a proxy that answered yesterday and died overnight would otherwise be tried first on every launch and time out for ~40s before the pool moved on. If it comes back it is trusted again — the failure is cleared the moment it completes a turn.
Expect the first run to take a minute or two — building the pool runs real model
turns (~4 minutes for a full pool), and each retry costs up to ~40s. The pool is
cached under ~/.zimzilla/opencode/ and rebuilt when it runs dry or is more than
six hours old; known-good entries are carried across a rebuild.
While it works, the screen says only this:
⠹ ZIM IS MAKING IT UNLIMITED - FINDING THE BEST PROXIES ...
No counts, because how many proxies there are says nothing about how many will be accepted — a pool of 24 measured 12% usable at one point and 79% at another. The proxy is named once it has been proven, not while it is being guessed at.
Two caveats worth stating plainly. The free models are opencode's own and
documented as limited-time — they rotate and retire, which is why
--model exists and why a retired id is reported instead of retried. And
free proxies see your traffic: opencode's own traffic, but in the clear as far
as the proxy is concerned. Do not send anything through them you would not send
through a stranger's server.
Limits
/limits prints all of these for the running session, and says where each
figure came from. Four limits matter, and they are independent — a turn that
stops is almost always one of the last two, not the first.
| Limit | Default | What happens at it |
|---|---|---|
| Context window | the model's own, from the live catalog (1M fallback) | History is compacted automatically at 80% — see below |
| Output ceiling | 32768 tokens | The reply is cut off; reported as truncated, never silently. A cut that carried nothing is retried once at double — see below |
| Rounds before pause | 40 | The turn continues if the last round did real work; otherwise it stops and says so |
| Hard round limit | 400 | The turn stops and says so — this is the runaway guard |
When the ceiling goes to thinking
A reasoning model's thinking is billed against the output ceiling like any other
output, so a model can spend the whole budget before it writes a word. That is
survivable when the thinking reaches you — but through a proxy that does not
forward it, it is not: the reply arrives as a single text block holding an
empty string, and the harness is told only that it was cut off.
Measured 2026-10-16 on Token Juice's deepseek-v4.1-flash behind the LiteLLM
proxy, same prompt through both protocol shapes:
OpenAI /v1/chat/completions finish_reason: length
message.content: None
message.reasoning_content: "We need answer. Need be careful…"
completion_tokens_details.reasoning_tokens: 16 # the whole budget
Anthropic /v1/messages stop_reason: max_tokens
content: [{"type": "text", "text": ""}]
The Anthropic shape is what ZimZilla speaks, so a question as ordinary as
"so where are we ?" against a loaded session came back as
reply truncated at 32768 output tokens with no reply above it — the model had
reasoned its way to the ceiling and the harness could not see any of it. The
warning was also wrong about what had happened: nothing the model asked for was
dropped, so "the requested action may not have run" described a different
failure.
A truncation that carries nothing is now its own shape, reasoning_exhausted,
and it is retried once with the ceiling doubled (capped at 131072). Re-sending
costs only a ceiling already known to be too small, and there is no dropped work
to protect — which is exactly why the mid-Write truncation below must not be
retried, and still is not. If the doubled ceiling is spent the same way, the
turn stops and says so rather than doubling again. The empty assistant turn is
dropped from the history either way, so the next request does not carry a blank
reply.
Watching the model think
Some models reason before they answer, and some endpoints forward that reasoning. When one does, ZimZilla shows it: dim and italic, above the answer, streaming as it arrives — the same thing DeepSeek's own client and OpenCode display.
✻ thinking (+26 earlier)
the operator asked for the upstream count beside the model, so the
field is enable_groups, not a health flag — there is no health field
anywhere in the payload...
The live block shows the last 14 lines and counts what it elided, so a model
that reasons for a page cannot push its own answer off the screen while it
streams. When the answer starts, the whole reasoning is committed to the
transcript as a [ ✻ THINKING ] panel — every line, not the window — so the
elided part is readable in the scrollback rather than lost. /think toggles the
whole thing; --no-think turns it off at launch.
Whether you see anything is up to the endpoint, not the model. Measured 2026-10-16, same prompt, same model family, two endpoints:
| Endpoint | Protocol | Reasoning forwarded? |
|---|---|---|
| OpenRouter | Anthropic-native | Yes — thinking block, 107 thinking_delta events, thinking_tokens: 490 in usage |
| Token Juice (:4000) | LiteLLM → OpenAI adapter | No — reasoning exists (reasoning_content, 1948 reasoning tokens) but LiteLLM's /v1/messages bridge drops it |
So on OpenRouter you watch it think. On Token Juice's deepseek-v4.1-flash you
will not, and that is not something the harness can fix: the thinking is real and
is billed — it just never crosses the proxy's Anthropic bridge. This is the
same proxy gap that causes the empty-reply truncation above, seen from the other
side. LiteLLM 1.100.1 was tested with merge_reasoning_content_in_choices,
with the deepseek/ provider in place of openai/, and with every
thinking-parameter variant (budget_tokens 1024–6000, reasoning_effort) —
all accepted, none forwarded. ZimZilla's own code is endpoint-agnostic: where the
block arrives it is shown, and where it does not, nothing changes.
Why a long turn used to just stop
This was a real bug, reported as *"sometimes in long tasks my models just go to
idle loop."* It was not the context limit and it was not idleness. The loop ran
for iteration in range(40), and on the 41st round it fell out of the for and
emitted an ordinary turn_end — the same event a finished turn emits. The UI
turns that into IDLE, so a turn that had run out of rounds was rendered
exactly like one that was done, and nothing picked the work back up.
Now the two are distinguishable:
- A turn still making progress past the round cap keeps going, up to the
N rounds and still progressing — continuing.
- A turn that stops — because a round achieved nothing, or because it hit the
CAPPED rather than IDLE
until the next turn starts.
"Progress" means a tool ran and succeeded. A round of nothing but denials and errors is the loop spinning, and that is what the cap is for.
Auto-compact
History is summarised automatically when the measured prompt size — the
input_tokens the endpoint reported for the previous call — reaches 80% of the
model's window. The trigger is a real measurement, not an estimate, and it is
checked before each request rather than after one fails.
The window is per-model: it comes from the active source's live catalog
(UnoRouter's contextWindow, OpenRouter's and TokenHarbour's context_length,
AntSeed's caps), falling back to context_window when the catalog does not
publish one. That matters because the models here differ by an order of
magnitude — a 1M-window free model and a 64k one cannot share a threshold
without the small one overflowing before it trips.
If a request is still rejected for being too long, that is recognised as a
context overflow (rather than the generic bad request it used to print),
history is compacted, and the round is retried once. /compact still works by
hand; --no-auto-compact turns the automatic path off entirely.
Modes
The mode decides how much the agent is allowed to do on its own. Switch at
runtime with /mode ; the active mode is always visible in the header.
| Mode | Behaviour |
|---|---|
| auto (default) | Full auto — file writes and shell commands, no confirmation. |
| edits | File work is free; the shell is removed entirely. |
| plan | Read-only. Investigate, propose, change nothing. |
| zim | Full auto, governed by your AGENTS.md. |
| danger | Full auto, operator-directed — no questions asked. |
| uncensored | Full auto, the default prompt is thrown out. The model cannot refuse; it only thinks how to do what you said. |
zim, danger and uncensored are the armed modes: everything runs
unattended, and the header turns red to say so. A tool that a mode disallows
does not merely get discouraged — it stops existing for that session, so
plan cannot write a file even if you ask it to.
uncensored is the one that changes how the model thinks, not just which
tools run without asking. Thinking models refuse by reasoning their way to
"I shouldn't"; this mode replaces the whole system prompt so that chain of
thought is not available. /mode uncensored, then say the thing.
zim mode and AGENTS.md
zim mode hands the wheel to a doctrine file of your choosing. Write the rules
you want followed; that file becomes the agent's whole operating manual.
It is looked for in this order, first hit wins:
— the project's own doctrine/AGENTS.md — a checkout's root file, seen from a subdirectory/../AGENTS.md ~/.zimzilla/AGENTS.md— the installed doctrine (./setup.shputs it there)~/AGENTS.md— a hand-kept global file
zim mode work from any directory: the file is
installed once, so a session opened in an unrelated project still finds your
doctrine instead of silently falling back to the default prompt. A project that
ships its own AGENTS.md still wins, because the workdir is checked first.
Override the whole search with --agents PATH; switching to zim at runtime
with /mode zim re-runs the search, so a session that started elsewhere picks
the file up too.
Tools
| Tool | Asks first? | What it's for |
|---|---|---|
| bash | in some modes | Run a shell command. |
| read_file | no | Read a file, whole or by line range. |
| write_file | in some modes | Create or replace a file. |
| edit_file | in some modes | Surgical string replacement in a file. |
| glob | no | Find files by pattern. |
| grep | no | Search file contents by regex. |
| list_dir | no | List a directory. |
| search_web | no | Search the web. |
| web_fetch | no | Pull down a URL. |
When a mode makes a tool ask first, you get a modal showing the exact command or a colour diff of the change, and three keys:
[y] run it [n] no [a] always, this session
Upstreams
ZimZilla speaks the Anthropic Messages protocol to whatever ANTHROPIC_BASE_URL
points at, so it can run against a local LiteLLM proxy, a hosted gateway or a
peer-to-peer marketplace without any code caring which. Eight sources are wired
up:
| Source | Endpoint | Key | How it starts |
|---|---|---|---|
| Logfare | :4001 | shipped (shared) | the bundled proxy — the default, all chat models |
| Token Juice | :4000 | discovered | its own local proxy — under construction |
| TokenHarbour | tokenharbor.ai | yours | hosted — nothing to start |
| OpenRouter | openrouter.ai | yours | hosted — nothing to start |
| UnoRouter | api.unorouter.com | yours | hosted — nothing to start |
| AIHubMix | aihubmix.com | yours | hosted — nothing to start |
| BeatAPI | direct.beatapi.io | yours | hosted — nothing to start |
| AntSeed | :8377 | none | antseed buyer start — a local buyer on a P2P network |
Switch with /zim-logfare, /zim-tokenharbour, /zim-openrouter,
/zim-unorouter, /zim-aihubmix, /zim-beatapi or /zim-antseed; the choice
is remembered, so the next launch comes back to the one that was working.
Logfare is a full switch target: the shipped key makes it work with no signup,
and it serves every chat model on Logfare's catalog. The key is shared, so its
request budget is shared too — see Install for the numbers, and use
/logfare-models to see what is up right now. Token Juice is the one route still
listed by /zim-source as under construction, because its upstream keeps
renaming its model id; the reason prints in full, and the route stays visible.
TokenHarbour
TokenHarbour is remote and needs a key of your own. Set it once:
/tokenharbour-api-setup thk_live_yourkeyhere
That writes ~/.zimzilla/tokenharbour/source (mode 600) — the same profile
format as every other source, so it is shell-sourceable too.
If you already keep per-provider profiles for Claude Code, **you do not need that command**. A profile that is already sourced is honoured:
source ~/claude-source/haiku-5.5 && zimzilla
ZimZilla picks up the base URL, the key and the model from it, and anything
under ~/claude-source that points at TokenHarbour is found automatically even
when nothing was sourced. An endpoint you set deliberately is never overridden
by a remembered choice.
TokenHarbour's catalog is live — it changes, and some models are free:
/tokenharbour-models # fetch it now, and list what is free
/model # free models first, with prices
/model mimo-v2.5:free # switch to one
/model reads the fetched catalog, so free models sort to the top and the
prices shown are the gateway's own rather than a local estimate.
UnoRouter
UnoRouter fronts a couple of hundred models behind one key, and — like TokenHarbour — speaks the Anthropic Messages protocol natively, so nothing is translated on the way through. It is remote and needs a key of your own:
/unorouter-api-setup sk-yourkeyhere
That writes ~/.zimzilla/unorouter/source (mode 600), the same profile format
as every other source. Then:
/zim-unorouter # switch, and fetch the live free list
/unorouter-models # the free catalog again, any time
/models unorouter # the same set, through the generic command
/model gemini-3.6-flash:free
The catalog is public, so /models unorouter works before you have a key
— the key is only needed to send a message. That is why the setup command says
so rather than refusing to list anything.
Free here means free, text-output, and able to call tools — all three. The gateway also fronts image generators, embedding models, Whisper transcription and TTS voices for free, and all of them are genuinely free and useless to this harness: they cannot answer a Messages turn. Filtering on the modality as well as the price is what keeps them out of the list. Measured on 2026-10-16: **230 models in the catalog, 97 free, 46 free and usable here** — the filter drops 36 for modality and 15 for missing tool support.
Those counts move as the catalog changes, and they moved between two runs
minutes apart while this was being written (232/99/48, then 230/97/46), so treat
them as a dated snapshot rather than a specification. The usable_free_models()
filter is the thing that holds; the numbers are what it returned on one
afternoon.
Upstream redundancy — why some free models stall
UnoRouter does not serve a model from one place. Each model has a list of upstream channels behind it, and how many there are decides what a rate limit means. One channel means a 429 there is the end of the request; a hundred means the router tries elsewhere and you never see it. That is the difference between a free model that works and one that appears to hang.
/models unorouter shows the count beside every model, coloured by a tier, and
sorts the list most-redundant first — so the top of the list is the safest
pick:
○ minimax-m2.7:free 101 upstreams · free · reasoning · 204k ctx
○ nemotron-3-super-120b-a12b:free 19 upstreams · free · reasoning · 262k ctx
◉ gemini-3.6-flash:free 13 upstreams · free · reasoning · 1000k ctx
○ gpt-oss-20b:free 13 upstreams · free · reasoning · 128k ctx
...
○ glm-4.5-flash:free 1 upstream · free · 131k ctx
The share that matters holds even though the counts do not. Measured on 2026-10-16 across three consecutive reads, the number of usable free models moved 47 → 45 → 42 while the single-channel share stayed at 38–42% — so roughly two in five of the free models on offer have exactly one upstream, and those are the ones that stall. They were previously indistinguishable from the rest of the list. As with the counts above, treat any absolute figure here as a dated snapshot: the ratio is the finding, the totals are one afternoon's reading.
Two things this is not, both worth stating plainly:
- It is not a health signal. UnoRouter's public catalog publishes a channel
- The tier boundaries are ZimZilla's, not UnoRouter's. The count is measured;
UPSTREAM_THIN = 5) is our judgement about the point at
which one channel failing stops mattering. The number is printed next to the
colour so the colour can never be mistaken for something the gateway said.
A model whose catalog entry carries no channel list reads as unknown and is
left uncoloured — absence of the field is not evidence of a single channel.
A note on prices: UnoRouter publishes credit ratios, not dollars per million
tokens, so this integration does not invent a rate from them. Free models read
free — which is exact — and a paid model falls through to ZimZilla's own
estimate table, labelled as an estimate.
/zim-unorouter never lands you on a paid model. With no preference saved it
names a free model by default, so a fresh switch costs nothing. The default is a
measured shortlist, ordered by whether the model actually returns a tool_use
block rather than by the catalog's supportsTools flag. That flag is a good
filter and not a guarantee: nemotron-3-super-120b-a12b:free advertises tool
support and a 262k context, and answered a real tool-using request with a
completed turn in prose. It was ranked highly here on the strength of one early
sample and removed once re-measured — which is the whole reason the shortlist is
short and each entry is one that was actually seen calling a tool.
The measurement has to be done with the requests spaced. Hammering one model back to back measures the free tier's rate limiter, not the model.
Even spaced, though, a small sample cannot rank these models. Two sweeps of the same five, three attempts each, disagreed on every one of them:
| model | first sweep | confirmation |
|---|---|---|
| gemini-3.6-flash:free | 3/3 | 2/3 |
| qwen3.8-27b:free | 3/3 | 2/3 |
| glm-5.3-flash:free | 3/3 | 1/3 |
| deepseek-v4.1-flash:free | 1/3 | 2/3 |
| gpt-oss-20b:free | 1/3 | 2/3 |
The run-to-run variance is larger than the difference between the models, so the
honest reading is not "gemini is best" but **all five can call a tool and none
does so dependably** — a property of the free tier rather than of any one model.
The shortlist therefore records what was verified (each name returned a real
tool_use at least once) and breaks the tie on upstream redundancy first,
context second. The default is gemini-3.6-flash:free and
gpt-oss-20b:free behind it — both with 13 channels, so the context tiebreak
decides and gemini's 1M window wins. qwen3.8-27b:free and
glm-5.3-flash:free sit at one or two channels and rank below them.
The reordering came from this same measurement, and it corrected a mistake worth
recording: a single read put gpt-oss-20b:free at 14 channels against
gemini's 12, which looked like grounds to promote it. Five consecutive reads
then showed both at 13 every time — the gap was drift, not signal. Promoting on
it would have been reading noise into a ±2 wobble, so the two are treated as
tied. What the counts do settle is that two of the five shortlisted names have
a single channel, so the model this harness chose on the operator's behalf was, a
third of the time, one of the fragile ones. A default that quietly picks a model
which rate limits is the same complaint as a list that hides which models rate
limit.
Entries do disappear: glm-5.3-flash:free was gone from the catalog within the
hour it was measured, and deepseek-v4.1-flash:free is gone now. The list falls
through to the next name and then to the catalog's own order, so a vanished entry
costs one lookup rather than a broken switch.
BeatAPI
BeatAPI (beatapi.io) fronts text, image and video models behind one key. The
text half speaks the Anthropic Messages protocol natively, so nothing is
translated on the way through. It is remote and needs a key of your own:
/beatapi-api-setup sk-yourkeyhere
That writes ~/.zimzilla/beatapi/source (mode 600), the same profile format as
every other source. Then:
/zim-beatapi # switch, and fetch the live free list
/beatapi-models # the free catalog again, any time
/models beatapi # the same set, through the generic command
/model glm-5.3-free # switch to one
Two hosts, and the difference matters. Turns go to
https://direct.beatapi.io, which is not behind Cloudflare's 100-second limit —
so a long non-streaming reply is not cut off. The model catalog is served from
the other origin, https://api.beatapi.io, which is also the documented
fallback for turns. ZimZilla uses each for what it is good for, and does not let
either one be "fixed" into breaking the other.
The catalog is public, so /models beatapi works before you have a key —
the key is only needed to send a message.
Free here is a live set, not a fixed list. BeatAPI's free models are -free
ids in a free family, and they turn over: the gateway calls them a limited-time
offer, rate limited per account, and they can be withdrawn without notice.
/beatapi-models and /zim-beatapi fetch the current set, so the list is what
is free now rather than what was free when this was written. With no preference
saved, /zim-beatapi shows that free list rather than picking a paid model for
you.
Free entries publish no price, no context window and no capabilities, which is why the list is built the way it is:
endpointsis not a filter. Every free entry reportsopenaias its
pricedmeans a price was actually read. A free entry that publishes no
0/0 price, or every unpriced model in the catalog
would be offered as free.
Five free text models were on offer when this was written — glm-5.3-free,
glm-5.3-flash-free, gemini-3.8-flash-free, deepseek-v4.1-flash-free and
deepseek-v4-flash-0731-free. Treat that as a dated snapshot: the set moves.
AntSeed
AntSeed is not a gateway. It is a peer-to-peer marketplace: independent sellers advertise models, and a buyer process you run locally routes your requests to them. Install it, then start the buyer once:
antseed buyer start # serves http://localhost:8377
Then /zim-antseed — there is no key to set. The buyer authenticates
nothing; a request with no Authorization header at all comes back 200.
/tokenharbour-api-setup-style commands have nothing to store here, and
ZimZilla says so rather than pretending.
The catalog is public and changes constantly, so look before you switch:
/antseed-models # the free models, each with the peer to pin
/models antseed # the same set, through the generic command
Free means a free seller, not a free model. A model is free when at least
one peer serving it charges 0 in and 0 out. A model that is free in but paid out
is a paid model, and is listed as one. Expect the free set to be a handful out
of a few hundred — the exact count moves as sellers come and go, which is why
/antseed-models fetches rather than quoting a number.
Why the list looks like 4668854b…@glm-4.5-flash. That peer@model form is
AntSeed's own pinning syntax, and it is the whole point of this integration. The
router ranks peers by price and trust, with minTrustScore defaulting to 60
while free peers cluster below it — so asking for a free model by its bare name
gets you
502 model_not_found: No policy-allowed peer currently serves model "glm-4.5-flash".
even though the peer is online. The pin bypasses that ranking, and it needs no client change at all: no header, no extra option, just the model id.
A pin bypasses the ranking, not the seller. A seller is a stranger's machine,
and it can be busy or refuse — on one measurement of the free pins, most
answered with a real reply while others returned 429 or 502 "pinned peer
could not complete". So ZimZilla lists every free pin rather than promising
one, and /model orders them by protocol support and trust. If one 429s, try
the next.
/zim-antseed never lands you on a paid model. With no preference saved it
names a free pin by default, so a fresh switch costs nothing. That default is a
measured shortlist — nemotron-120b-free, nemotron-3-ultra-free,
glm-4.5-flash, qwen2.5-coder-7b — and it is ordered by whether the seller
actually answers, not by reputation, because reputation does not predict it. Of
the nine free pins measured on 2026-10-15, the highest-trust free seller
returned 502 twice and the lowest-trust one returned 200 twice. Choosing
a pin with /model remembers it, so the next launch returns to
the same seller.
Two smaller things worth knowing:
- Every seller on the network advertises the OpenAI protocol and none advertises
400 unsupported_protocol from a
particular seller is a property of that seller rather than a bug here.
- The reasoning models spend the token budget before emitting any text. A
content: [] and stop_reason: max_tokens at --max-tokens 32
becomes real prose at 256. ZimZilla's default is well clear of this.
Scope
Two files, one job each. Both are picked up automatically from the working
directory, or pointed at with --allow / --deny.
allow.yaml — your declared targets. Its presence arms the session:
gated tools stop asking. It is a declaration of what you are authorised to
work on, not a fence around you — it restricts nothing and blocks nothing.
domain: # a domain also covers its subdomains
- example.com
ip:
- 203.0.113.10
cidr:
- 198.51.100.0/24
out-of-scope.yaml — never touch. This one is enforced. A command that
reaches for one of these hosts is stopped before it runs, and fetching one is
refused outright.
⛔ BLOCKED — SCOPE GUARD — HARD BLOCK
out-of-scope.example.com — denied — listed domain
domain:
- out-of-scope.example.com
Deny always beats allow. Loading only out-of-scope.yaml arms nothing — it
only subtracts.
What this is and is not
Worth being straight about, because the difference matters:
- The allow-list is an arming mechanism, not a safety control. With it
out-of-scope.yaml.
- The deny-list is a check on what it's about to do, not a sandbox. It
Use it as a seatbelt, not as a cage.
Upgrading from scope.yaml
scope.yaml is no longer read. It was one file doing three jobs at once, and
its meaning cannot be carried across safely — reading it as a deny-list would
block exactly the hosts it declared. A stale scope.yaml is reported loudly at
startup and then ignored. Rename it to allow.yaml or out-of-scope.yaml.
Slash commands
| Command | Effect |
|---|---|
| /help | command reference |
| /mode [name] | show or switch mode |
| /model [name] | show or switch the model |
| /cost | session token + cost breakdown |
| /scope | scope status and entries |
| /theme green\|amber\|cyan\|system | switch palette — system adopts the terminal's own colours |
| /rain | toggle the matrix rain |
| /think | show the model's own reasoning — where the endpoint forwards it |
| /panes | re-open this session under tmux — ctrl+b h/v split |
| /zim-opencode [--model id] [--refresh] [-- args] | open opencode in this terminal behind a fresh rotating IP — free models, no account, nothing global |
| /save [name] | write the session to disk |
| /load [name] | restore a session |
| /compact | summarise history to free context |
| /limits | context window, round caps and timeouts — and where each figure comes from |
| /team | fan the task out across parallel agents |
| /osint | open-source recon on a target |
| /osint-tree [kind\|path] | re-show a finished recon as a result tree |
| /shodan-api-setup | store your Shodan API key |
| /shodan-cam-search [cc] | exposed IP cameras — then the model analyses the result |
| /shodan-scada-search [cc] | exposed ICS / SCADA — PLCs, RTUs, HMIs |
| /shodan-db-search [cc] | exposed databases — MongoDB, Elasticsearch, Redis, SQL |
| /shodan-rdp-search [cc] | exposed RDP and VNC services |
| /shodan-router-search [cc] | exposed routers, firewalls and VPNs |
| /shodan-vuln-search | hosts Shodan has flagged with a known CVE |
| /shodan-search | run any Shodan query verbatim — then analyse |
| /phish | clone a login page, serve it, harvest creds into zim-pane |
| /zim-logfare | switch upstream to Logfare (:4001) — bundled proxy, all chat models |
| /zim-tokenjuice | Token Juice (:4000) — under construction |
| `/zim-t
... (README truncated for length)