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.

Prefer to click through it?

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

KeyWhereWhat it does
nameprojectNamespaces worktrees, slices and the default domain.
domainprojectDefaults to <name>.localhost.
port_rangeproject[low, high] Binky allocates from.
max_parallelprojectAdmission cap. Defaults to something derived from your RAM.
httpsprojectFront the proxy with Caddy for TLS via a local CA.
cmdserviceThe process to run. Tokens are resolved before launch.
setupserviceRun once when the worktree is created (pnpm install).
urlservicetrue, a subdomain string, or {label = sub}.
portsserviceExtra named ports beyond the primary one.
healthserviceAn http URL, or a table with http/tcp/cmd.
depends_onserviceStart order, and what lands in this service's env.
env, env_fileserviceExtra environment, tokens resolved.
watchserviceGlobs that trigger a restart.
restartserviceon-failure (default), always, never.
sharedserviceOne instance across every worktree, instead of one each.
adapterresourceWhich adapter carves the slice.
admin_urlresourceThe server to carve it from. Use ${env.NAME}.
migrate, seedresourceCommands run against a fresh slice.
clone_fromresourceBuild a golden template once, copy it per worktree.
optionsresourceFree-form per-adapter settings, passed through verbatim.
The one rule

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

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

Read this one

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.

Sharpest edge in the feature

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

CommandWhat 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 dashboardLive TUI over every worktree.
binky doctorDiagnose the install and daemon. Non-zero when something is wrong.
binky metricsPoint-in-time daemon metrics as JSON.
binky gcSweep orphaned adapter slices a crash left half-provisioned.
binky configShow 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 mcpRun the MCP server.
binky daemon start|stop|statusManage 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 sliceAdapters
a databasepostgres mysql mariadb percona mongodb clickhouse
a key prefixredis valkey
a bucket / prefixs3 minio
a vhost / topic prefixrabbitmq kafka redpanda
a collectionqdrant
an index prefixelasticsearch opensearch

Writing your own

Two tiers, one contract. Both implement the same four verbs:

VerbDoes
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"].

One daemon, either way

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 eachRAM: Binky time: a container eachtime: Binky
1150 MiB174 MiB6s4s
2298 MiB182 MiB11s6s
4596 MiB228 MiB20s10s
81197 MiB295 MiB40s16s

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.

Open an issue