← Back to Blog

Day 78: v0.5.1 and the Host Rewrite

Wednesday. v0.5.1 ships less than 48 hours after v0.5.0, which is the cadence the early-customer install base requires: each install on a new shape of host surfaces the next class of bug that the team's own deployment didn't have. Two bugs got rolled into one release today, because they're the same shape — a default that worked on the team's laptops and didn't work everywhere else.

The baseUrl Crash

The first bug is the one that prompted the release. v0.5.0's openclaw-config writer was emitting provider entries for the bundled providers — Anthropic, OpenAI, Google — without a baseUrl field. On the team's deployment, the absence was harmless: every team member sets ANTHROPIC_BASE_URL in their .env for a proxy, so the env-var override was always present and the missing default was never exercised. On a clean install, no env-var override exists; the writer emits the provider entry without a baseUrl; OpenClaw's schema validator rejects it with models.providers.<name>.baseUrl: expected string, received undefined; OpenClaw refuses to start; Pinchy looks broken.

The fix is to write the canonical default for each bundled provider — https://api.anthropic.com, https://api.openai.com/v1, https://generativelanguage.googleapis.com — into openclaw.json at config-write time, with the env-var override still available for proxy deployments. The unit tests for the writer got the inverse of what they used to assert: default required, env-var is override, rather than env-var required, default is fallback. The integration test that landed alongside is the kind that takes the longest to write — an OC-schema integration test that loads the real openclaw v2026.4.27 as a devDep and validates the generated config against the real schema. The first run of the new test reproduced the v0.5.0 crash in red, which is exactly the test you want to land before the fix it pins down.

A small docs cleanup followed: comments in openclaw-config that described the OC 4.27 baseUrl behaviour from memory got rewritten against what the integration test actually proves. Comments about external behaviour are a class of drift; the test makes the comments verifiable.

Ollama Local Finally Works

The second bug is the one a half-dozen users hit since the trial flow opened: setting up a local Ollama provider in the onboarding wizard never worked on a fresh install. The wizard accepted http://localhost:11434; the config got written; OpenClaw started; the first agent call to the provider returned a connection refused. The fix has three parts that all had to land together.

The first part is the hostname. Inside the OpenClaw container, localhost is the container's own loopback, not the host's; 127.0.0.1 is the same story; host.docker.internal would route correctly given a host-gateway alias, except that OpenClaw 2026.4.27's isLocalBaseUrl allowlist accepts *.local but not host.docker.internal, so the runtime refuses to treat the URL as local and the synthetic-local-models path that Ollama needs never runs. The setup wizard now detects any local-shaped base URL — localhost, 127.0.0.1, host.docker.internal — and rewrites it to ollama.local before writing the provider config. The user-entered URL stays unchanged; Pinchy translates at config-emit time.

The second part is making ollama.local resolvable from inside the OpenClaw container. docker-compose.yml picked up an extra_hosts entry that maps ollama.local to host-gateway — Docker's portable way of saying the host this container is running on, which resolves to whatever the container runtime considers the host gateway IP. Linux, macOS, and Windows now all do the right thing without a per-platform branch in the compose file.

The third part is the provider shape. v0.5.0's setup wizard wrote api: ollama with an empty models[] array; OpenClaw's runtime then took the discovery path and tried to GET /api/tags against the provider to enumerate models, which works on a real Ollama instance but failed silently against the synthetic hostname rewrite. The fix populates models[] with the user's selection at wizard-completion time and uses api: openai-completions with a /v1 suffix — the shape OpenClaw expects for self-hosted OpenAI-compatible endpoints. Ollama speaks both protocols; openai-completions is the more predictable one inside a container.

A small intentional discard fell out of the rewrite that earned its own comment: the wizard receives a Pinchy-side display label for each model (m.name, the prettified form like qwen2.5:7b (7B)) from the user's selection, but only the bare model id (m.model) lands in OpenClaw's name field. An earlier commit had tried to use the display label there too, but OpenClaw 2026.4.27's diff classifier treats model name changes as restart-required, and the Pinchy form and OpenClaw's persisted-normalised form diverged after the first enrichment — so every regenerate flipped the file back, triggered a full restart, and the idempotency test on main started failing inside iteration one. The revert keeps the bare id in the config; Pinchy's own model picker still shows the nicer label because it reads fetchOllamaLocalModelsFromUrl directly. A comment at the discard point and in the test file warns the next reviewer off the same trap.

Domain Lock Gets Its Integration Test

The domain-lock fix from yesterday — the /api/internal/* exemption — got an integration test today rather than the unit-only coverage it shipped with. The integration spec drives a real reverse-proxy fixture in front of a real Pinchy process and asserts both halves: an external request with a wrong Host hits a 403, an internal callback to /api/internal/secrets-bundle/refresh doesn't. The kind of test that, like the OC-schema test from earlier today, costs more to set up than the bug it catches but pins down a regression class that's expensive to discover in production.

Container UID Pinned

One small bit of housekeeping landed: the Pinchy container's runtime UID/GID got pinned to 999 explicitly in the Dockerfile, with matching docs in the secrets reference. The number was already 999 by virtue of being the Debian pinchy system user, but the secrets reference and the upgrade docs had been saying uid=1000 in one place and uid=999 in another, which mattered the moment a customer wrote a custom docker-compose.yml with a tmpfs block. A drift guard test that asserts the documented UID matches the Dockerfile's declared UID landed alongside, so the two can't disagree silently again.

Day 78

The shape of a hotfix that ships less than 48 hours after the release it patches is that the team has been wrong about something specific and has just learned what it is. Today, twice: the bundled-provider baseUrl default that nobody on the team needed because everyone uses a proxy, and the Ollama-local setup that nobody on the team needed because everyone has Ollama on a remote host. Both were defaults that worked for the people writing them and didn't work for the people running them. The integration tests that landed alongside — schema validation against real OpenClaw, reverse-proxy fixture for the domain lock, drift guard for the documented UID — are the ones that catch this class of bug before it ships, not the ones that catch the next instance of the bug after it ships.

← Day 77: The Day After Day 79: Personal Means Personal →

Pinchy is open source and ready to deploy. Clone the repo, run docker compose up, and your first agent is live in minutes.