Profile
Back to NewsBack
GitHub Trending 38 min
Reader Mode
Mr-Destroyer/ZimZilla: THE MOST POWERFUL OFFENSIVE AI AGENT FOR PENTESTING , OSINT , AND ETHICAL HACKING

Mr-Destroyer/ZimZilla: THE MOST POWERFUL OFFENSIVE AI AGENT FOR PENTESTING , OSINT , AND ETHICAL HACKING

13 hours ago

ZIMZILLA

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

!python !platform !interface !status

a ZIMZILLA session

See it

Cold start

ZIMZILLA boot screen — ASCII banner, system check, shell ready

Banner reveal, a live system check, then the shell takes over.

The shell

the ZIMZILLA shell — loop rail, transcript with a live tool card, telemetry rail, status bar

*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.*

switching models at runtime

Swap engines mid-session.
Prices in, prices out. No restart, no lost context.

the mode list

Change how much rope it gets.
From read-only spectator to full send.


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
makes the change — reading files, running commands, editing code.
  • Sees your project. Point it at any directory and it works there: it can
read, search, and reason across the whole tree.
  • Runs real commands. A full shell, wired straight into the loop, with
output captured and shown inline.
  • 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
in the transcript as it happens, not buried in a log.
  • Reads its own markdown. A model answers in headings, bold, lists, tables
and fenced code whether or not anything asked it to. The transcript renders that rather than showing you the source: 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
per session.
  • Answers to a dial. A mode switch decides whether it merely investigates,
edits freely, or runs everything unattended.
  • Stays in bounds. An optional scope file declares what it may touch, and a
second declares what it must never go near.
  • Remembers the session. Save it, load it, compact it when the context
gets heavy.
  • 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:

  1. 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.
  1. Installs the opencode-zim alias into ~/.local/bin — a small shim that
calls back into ZimZilla, so the shell path and the slash command cannot drift apart. Delete the file to remove it. It takes no arguments of its own; they all go to opencode.
  1. Rotates the exit IP and hands over the terminal. The pool's cursor is
persisted, so consecutive runs differ even across restarts.
/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
hard limit, printing N rounds and still progressing — continuing.
  • A turn that stops — because a round achieved nothing, or because it hit the
hard limit — prints why, and the loop rail reads 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:

  1. /AGENTS.md — the project's own doctrine
  2. /../AGENTS.md — a checkout's root file, seen from a subdirectory
  3. ~/.zimzilla/AGENTS.md — the installed doctrine (./setup.sh puts it there)
  4. ~/AGENTS.md — a hand-kept global file
Step 3 is what makes 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
list and no per-channel status — checked field by field, there is no such field on the API. A model with 20 channels could have all 20 degraded. What is counted is how many places the router could go, not how many are answering.
  • The tier boundaries are ZimZilla's, not UnoRouter's. The count is measured;
where "good" begins (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:

  • endpoints is not a filter. Every free entry reports openai as its
native format, and the gateway documents that field as a line's native formats, "not the full list of formats you can call" — so filtering on it would hide every free model. The tools hint shown beside a free model is borrowed from its paid sibling and is used to order the list, never to hide a model from it.
  • priced means a price was actually read. A free entry that publishes no
price must not read as a 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
Anthropic — the buyer translates for you. It works, but it is a translation, not a native protocol, so the occasional 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
200 with 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
loaded, the only thing between the agent and an arbitrary host is out-of-scope.yaml.
  • The deny-list is a check on what it's about to do, not a sandbox. It
reads the command as written. A target assembled at runtime, or reached through an unlisted redirect, is not something it can see. It catches the obvious mistake; it does not stop a determined command.

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)

Chat with me