Install
Binky needs Python 3.11+ and git. It's supported on Linux and Windows — the two platforms it's tested on for real. macOS isn't a supported target yet; a well-tested port would be a welcome contribution.
The guided walkthrough on the home page steps from install to a running worktree at your own pace — the short version of this page.
uv tool install binky # or: pipx install binky
binky --version
Or from a clone, to run the unreleased main or to hack on it:
git clone https://github.com/andreisilva1/binky && cd binky
uv tool install . # or: pipx install .
Adapters that need a database driver ship as extras — installing Binky for Redis
shouldn't drag in boto3:
uv tool install "binky[postgres]" # or [redis] [mysql] [mongodb] [s3] [kafka]
uv tool install "binky[all-adapters]" # the whole matrix
rabbitmq, qdrant, clickhouse and
elasticsearch need no extra at all — they drive plain HTTP.
Binky does not run your databases. It carves an isolated slice out of a server you already run, wherever that is: a local install, a container, a shared dev host.
Your first worktree
If the repo already has a docker-compose.yml, start from it rather than from
a blank file:
binky init # writes a binky.toml you can read and edit
binky check # validate it, and flag footguns
init reads docker-compose.yml and the key names in
.env — never their values — and maps images onto adapters. It has two hard
rules: what it writes always passes binky check, and it never copies a
secret into the file. Where compose says how to run something in a container and
Binky would need the command to run it on the host, it writes the block
commented with a TODO instead of guessing.
Then bring worktrees up, as many as you like, at the same time:
binky up <your-branch-name>
binky up <another-branch-name>
binky status # every worktree on the machine, grouped by project
binky env <your-branch-name> # its live coordinates, for you or an agent
binky down <your-branch-name> # pause: keeps files and ports; `up` again is instant
binky remove <your-branch-name> # destroy it (refuses if there are uncommitted changes)
Each up creates or reuses a git worktree for that branch, allocates a port
block, provisions an isolated database, renders the environment, starts the services in
dependency order, and registers the URLs. Nothing is hardcoded and nothing is discovered
at runtime.
binky.toml
One file at the root of the repo, committed. Unknown keys are rejected, so a typo fails loudly instead of being ignored.
[project]
name = "acme"
https = true # optional: TLS via Caddy, when Caddy is on PATH
[services.db]
adapter = "postgres"
admin_url = "${env.PG_ADMIN_URL}" # never a literal credential
migrate = "pnpm prisma migrate deploy"
seed = "pnpm seed"
clone_from = "golden" # built once → copied per worktree
[services.api]
cmd = "uv run api --port ${self.port}"
url = "api" # asking for a URL is what asks for a port
depends_on = ["db"]
env = { DATABASE_URL = "${db.url}" }
health = "http://localhost:${self.port}/healthz"
[services.web]
cmd = "pnpm dev --port ${self.port}"
url = "web" # → web.<worktree>.acme.localhost
watch = ["web/**"]
[agents]
expose = ["web", "api", "db"] # service names, not fields
verify = ["pnpm test", "pnpm lint"]
A service is a process or a resource, never both
Exactly one of cmd or adapter. A cmd service is a
process Binky starts and supervises. An adapter service is a slice Binky
asks some server to carve out — and keys like migrate, seed and
clone_from are only valid there.
The keys
| Key | Where | What it does |
|---|---|---|
name | project | Namespaces worktrees, slices and the default domain. |
domain | project | Defaults to <name>.localhost. |
port_range | project | [low, high] Binky allocates from. |
max_parallel | project | Admission cap. Defaults to something derived from your RAM. |
https | project | Front the proxy with Caddy for TLS via a local CA. |
cmd | service | The process to run. Tokens are resolved before launch. |
setup | service | Run once when the worktree is created (pnpm install). |
url | service | true, a subdomain string, or {label = sub}. |
ports | service | Extra named ports beyond the primary one. |
health | service | An http URL, or a table with http/tcp/cmd. |
depends_on | service | Start order, and what lands in this service's env. |
env, env_file | service | Extra environment, tokens resolved. |
watch | service | Globs that trigger a restart. |
restart | service | on-failure (default), always, never. |
shared | service | One instance across every worktree, instead of one each. |
adapter | resource | Which adapter carves the slice. |
admin_url | resource | The server to carve it from. Use ${env.NAME}. |
migrate, seed | resource | Commands run against a fresh slice. |
clone_from | resource | Build a golden template once, copy it per worktree. |
options | resource | Free-form per-adapter settings, passed through verbatim. |
Never put a credential inline in a committed binky.toml. Use
${env.NAME} or an env_file. binky check warns
when you do, and binky init never writes one.
Ports & URLs
Ports are the thing that breaks first when two branches run at once, so Binky does not leave them to chance. A machine-global allocator reserves a block for the worktree before any process starts, confirms nothing else holds it, and then injects the whole map into every service — its own ports and its dependencies'. Nobody discovers a port at runtime, because there is nothing left to discover.
Which services get one
Asking is explicit: a service is allocated a port if it declares ports, or a
url (which needs one to point at). A service with neither gets none — which
is right for a worker or a one-shot job, and a trap if you then write
${self.port} anywhere. Unrecognised tokens are left alone, so that
cmd reaches the shell with a literal ${self.port} in it and the
process dies on its own argument parsing. binky check warns when a service
does this; if it needs a port, say so.
Tokens
| Token | Resolves to |
|---|---|
${self.port} | This service's primary port. |
${self.<label>} | One of its named extra ports. |
${api.port} | Another service's primary port. |
${api.url} | http://localhost:<its port>. |
${db.url} | An adapter service's connection string. |
${worktree} | The worktree name. |
${env.NAME} | A variable from the daemon's environment. |
Every service also gets $PORT in its environment, for frameworks that read
it. Tokens Binky doesn't recognise are left alone rather than blanked, so a
${...} your own tooling owns survives untouched.
Binky can hand a service a port; it cannot make a framework listen on it. If your dev
server has its own hardcoded default, pass the port explicitly —
cmd = "pnpm dev --port ${self.port}", not "pnpm dev". This is
the single most common cause of "two worktrees collide even with Binky".
URLs
A service with url set gets a stable hostname through Binky's embedded
reverse proxy: <service>.<worktree>.<project>.localhost.
WebSockets and HMR are proxied too, so a dev server behaves normally behind it. The
hostname is stable across restarts even though the port behind it may not be.
Set https = true and Binky fronts the proxy with Caddy for
TLS from a local CA — when Caddy is on PATH. When it isn't, Binky
logs it and http keeps working. A missing optional binary degrades; it does not break
up.
Databases & golden templates
The naive way to give N branches N databases is to run N database servers. That costs N times one server, and it is the cost Binky exists to remove. An adapter carves an isolated slice out of one server you already run: a database, a key prefix, a bucket, a vhost, an index prefix.
[services.db]
adapter = "postgres"
admin_url = "${env.PG_ADMIN_URL}"
On up, that becomes a database named after the project and worktree, and
${db.url} resolves to its connection string. On remove, it is
dropped.
clone_from: pay for migrate+seed once
Migrations and seeds are usually the slowest part of standing a world up, and running
them per worktree means paying the same cost over and over for the same result. With
clone_from, the first binky up builds a golden
template — provision, migrate, seed — and every worktree after that is a
server-side copy of it.
The template is keyed by a hash of your migrate and seed
commands, so changing either rebuilds it automatically. Supported where the server can
copy a slice itself: postgres, mysql, mariadb,
percona, mongodb.
Where it isn't supported, or where a clone fails on something environmental — a client
binary too old to authenticate, a missing grant — Binky falls back to
provision+migrate+seed for that worktree and logs clone_fallback.
Correctness never depends on cloning. Only speed does.
The hash covers the migrate and seed commands, not the files they call.
Editing migrations/007_x.sql does not change "make migrate",
so nothing rebuilds and your new worktrees get the old schema. Delete the marker under
~/.binky/golden/<adapter>/<project>_golden_<clone_from>
to force a rebuild — or put something command-visible in the hook, like a version or a
checksum, so the hash moves when your schema does.
Supervision & health
Binky starts services in depends_on order and waits for each one to be
healthy before starting what depends on it. A service with no health is
considered up once its process is up; give it a probe and Binky waits for the probe.
health = "http://localhost:${self.port}/healthz" # shorthand
[services.api.health] # or the table form
tcp = "localhost:${self.port}"
timeout = "30s"
Exits are reaped as they happen, not polled for. Restarts follow the
restart policy with exponential backoff and a give-up cap, so a service that
is genuinely broken stops instead of spinning. Everything a service writes to stdout and
stderr is captured per worktree and per service — binky logs <worktree>
— and a worktree's logs are deleted when you remove it. Each service's log rolls at 5 MiB,
keeping three generations, at the moment it (re)starts; a service that runs a long
time without restarting still grows its current log until then, and
binky doctor flags a log directory past half a gigabyte.
The daemon that does all this is machine-global and always alive. It survives your terminal closing, and it reconciles on the way back up: when it restarts it re-adopts the processes that are still running rather than orphaning or duplicating them.
New worktrees are admitted through a FIFO queue bounded by max_parallel,
real free RAM (a percentage kept in reserve) and free disk (a fixed GB headroom).
Point twenty agents at a laptop and they queue, instead of taking the machine into swap.
If a running swarm later crosses the RAM reserve, the daemon suspends the
newest worktree to relieve pressure — a suspended status — and resumes it in
fairness order once room returns. Tune the reserves with binky reserve.
Agents: env & verify
An agent working in a worktree needs two things from Binky: where its world is, and
whether its work is any good. That's the whole [agents] section.
[agents]
expose = ["web", "api", "db"]
verify = ["pnpm test", "pnpm lint"]
expose lists service names — not fields. A resource yields
its address, a url service yields its proxied URL, anything else yields
http://localhost:<port>. That curated set is what
binky env prints, and nothing else leaks:
binky env <your-branch-name>
{"web": "http://web.<your-branch-name>.acme.localhost",
"api": "http://localhost:41003",
"db": "postgresql://…/acme_<your-branch-name>"}
# or straight into a shell
eval "$(binky env <your-branch-name> --dotenv)"
verify is the gate. binky verify <worktree> runs each
check inside the worktree with the exposed coordinates in its environment, records
pass/fail against the worktree, and exits non-zero on failure — so an agent can call it
at its "done" moment and find out it isn't done.
Agents that speak MCP don't need the CLI at all. See the MCP server.
CLI
| Command | What it does |
|---|---|
binky init [dir] [--force] | Write a starting binky.toml from docker-compose.yml + .env. |
binky check [path] | Validate a binky.toml and flag footguns. |
binky up <name> [--group g] | Create/use the worktree, allocate ports, provision resources, start services. |
binky reload <name> [--group g] | Restart in place — stop, then bring back up on the same ports and data. |
binky down <name> | Stop the processes. Keeps files, ports and data. |
binky remove <name> [--force] | Destroy it — stop, drop its slices, remove its files. |
binky status [.] | List worktrees, grouped by project. . = this project only. |
binky env <name> [--dotenv] | Print a worktree's exposed coordinates. |
binky curl <name> <service> [path] | Call a service through the proxy with the Host header filled in (-X/-d/-H/-i/-f). |
binky verify <name> | Run the project's checks. Non-zero on failure. |
binky logs <name> | Print the worktree's captured service logs. |
binky dashboard | Live TUI over every worktree. |
binky doctor | Diagnose the install and daemon. Non-zero when something is wrong. |
binky metrics | Point-in-time daemon metrics as JSON. |
binky gc | Sweep orphaned adapter slices a crash left half-provisioned. |
binky config | Show the machine-global settings — RAM/disk reserves and proxy ports. |
binky reserve [--ram %] [--disk gb] | Set how much RAM/disk Binky permanently keeps free. |
binky port [--proxy p] [--https p] | Set the proxy and HTTPS ports. |
binky mcp | Run the MCP server. |
binky daemon start|stop|status | Manage the background daemon. |
State lives in ~/.binky/ — a SQLite registry migrated in place across
upgrades, a JSON-lines event log, per-worktree logs, and the golden template markers.
Nothing runs in the cloud and Binky never phones home.
Adapters
16 names, 10 implementations, every one exercised against a real server
in CI. Protocol-compatible names share one implementation rather than a copy of it:
valkey is the Redis adapter, percona is the MySQL one,
opensearch is the Elasticsearch one.
| The slice | Adapters |
|---|---|
| a database | postgres mysql mariadb percona mongodb clickhouse |
| a key prefix | redis valkey |
| a bucket / prefix | s3 minio |
| a vhost / topic prefix | rabbitmq kafka redpanda |
| a collection | qdrant |
| an index prefix | elasticsearch opensearch |
Writing your own
Two tiers, one contract. Both implement the same four verbs:
| Verb | Does |
|---|---|
provision(slice) | Create the slice — CREATE DATABASE … |
resolve(slice) | Return its address → ${db.url} |
teardown(slice) | Destroy it. |
capabilities() | What it supports, e.g. {"clone": true} |
In-process adapters are Python classes in the repo, discovered by name.
External adapters are executables in any language, discovered as
binky-adapter-<name> on PATH and driven over a one-shot
subprocess + JSON protocol:
$ binky-adapter-filestore provision <<< '{"slice": "acme_<your-branch-name>", "config": {}}'
{"ok": true}
$ binky-adapter-filestore resolve <<< '{"slice": "acme_<your-branch-name>", "config": {}}'
{"ok": true, "address": "file:///tmp/binky-filestore/acme_<your-branch-name>"}
Python authors get the protocol for free: binky.adapter_sdk.run() bridges a
normal adapter class to it. There is a complete, dependency-free example in
examples/adapters/filestore.py
— about thirty lines.
clickhouse and the search adapters report clone: false on
purpose. There's no server-side copy for them, so "cloning" would mean
INSERT … SELECT per table — O(rows), which is exactly the cost
clone_from exists to avoid.
MCP server
binky mcp speaks the Model Context Protocol over stdio, exposing five
tools: up, down, status, env and
verify. They are the same verbs the CLI calls, against the same daemon — an
agent that can call tools doesn't need to shell out or parse output.
In Claude Code, one line wires it up:
claude mcp add binky -- binky mcp
Any other client that speaks the protocol takes the same stdio server as a config block — identical everywhere, only where the block lives changes:
{
"mcpServers": {
"binky": { "command": "binky", "args": ["mcp"] }
}
}
To run it with nothing installed at all, hand the job to uvx instead —
claude mcp add binky -- uvx --from "binky[all-adapters]" binky mcp, or as a
block, "command": "uvx", "args": ["--from", "binky[all-adapters]",
"binky", "mcp"].
However you launch it, the MCP server is a thin client of the same machine-global
daemon your CLI talks to. A worktree an agent brings up over MCP is the same worktree
binky status shows you in a terminal.
The useful loop is small: the orchestrator calls up for a branch, hands the
agent what env returns, lets it work, and calls verify before
accepting the result.
What N worlds cost
The claim is that N isolated worlds shouldn't cost N times one world. Measured, not
asserted — benchmarks/worlds.py is in the repo and reproduces this
(postgres:16, 200,000 rows per world, Docker Desktop on Windows 11):
| worlds | RAM: a container each | RAM: Binky | time: a container each | time: Binky |
|---|---|---|---|---|
| 1 | 150 MiB | 174 MiB | 6s | 4s |
| 2 | 298 MiB | 182 MiB | 11s | 6s |
| 4 | 596 MiB | 228 MiB | 20s | 10s |
| 8 | 1197 MiB | 295 MiB | 40s | 16s |
The 8th world costs +150 MiB and +4.8s as its own container, or +17 MiB and +1.7s under Binky. One arm grows with N; the other mostly doesn't.
Read the first row too. At one world Binky is behind — it pays
for a server plus a golden template nobody is using yet, and only earns that back on the
second. And disk is not a win:
CREATE DATABASE … TEMPLATE is a file copy, not copy-on-write, so every world
is still a full copy of the rows. What Binky removes is the server per world,
not the bytes per world. It also doesn't make your own app processes cheaper —
one dev server per worktree costs the same either way.
python benchmarks/worlds.py --sweep 1,2,4,8 --rows 200000
Security
Binky runs your commands on your machine as
you. It is a developer tool for a single-user workstation, not a
multi-tenant sandbox. A binky.toml is as trusted as a Makefile,
and binky up on a repo you don't trust is make on a repo you
don't trust.
That's a deliberate scope, not an oversight. The threat model and what is explicitly out of it are written down in SECURITY.md.
The one rule worth repeating: never put a credential inline in a committed
binky.toml. Use ${env.NAME} or an env_file.
Troubleshooting
Start here. It exits non-zero when something is actually wrong:
binky doctor
binky up fails on the adapter
Binky doesn't run your Postgres — it carves a slice of one you already run.
admin_url has to point at a server that's up, with an account that can create
and drop databases. Check it directly first:
psql "$PG_ADMIN_URL" -c "SELECT 1"
If that works and Binky still can't connect, the usual cause is
${env.NAME} resolving to nothing: Binky reads it from the environment of the
daemon, not your shell. Export it, then restart the daemon —
binky daemon stop && binky daemon start.
Something outside Binky holds the port
Binky allocates from its own range and never guesses, but it can't stop another program
from sitting on a port first. Move the range: port_range = [42000, 43000]
under [project].
web.<your-branch-name>.acme.localhost doesn't resolve
*.localhost resolves to loopback on macOS, most Linux setups and Windows 10+.
If yours doesn't, add the specific name to your hosts file — or skip the proxy entirely
and hit the port directly: binky status lists every service's port, and
binky env prints the addresses.
It cloned, but my schema is stale
The golden template hash covers the migrate and seed commands, not the files they run. Delete the marker to force a rebuild:
# ~/.binky/golden/<adapter>/<project>_golden_<clone_from>
rm -f ~/.binky/golden/postgres/acme_golden_golden
It silently stopped cloning
It didn't do it silently — look for clone_fallback in the event log:
grep clone_fallback ~/.binky/events.log
The world you get is identical, just built the slow way. The most common cause by far is a
mysqldump older than the MySQL server it's dumping
(caching_sha2_password could not be loaded) — upgrade the client, or accept
the slow path.
Most of what binky init wrote is commented out
Expected, and it's the honest answer. Compose says how to run something in a
container; Binky runs it on the host, and those are rarely the same command.
Rather than guessing a cmd that looks right and doesn't work, init
writes the block commented with a TODO.
Still stuck
binky doctor output plus the relevant part of
~/.binky/events.log is what makes an issue actionable. Binky logs event
names, worktrees, services and adapter names — not your config values. One thing to skim
before pasting: failure events carry the underlying error text, and an error raised by a
database driver can quote the connection it was attempting.
The long-form version of all of this, kept next to the code, is TROUBLESHOOTING.md.