The cherrypicker logo: a cherry driving a cherry picker

Stable named .localhost URLs for local dev servers.
For humans and agents, on the BEAM — no Node, no npm, no root CA.

Hex versionHex docsCI statusLicence: MIT or Apache-2.0

Website · API reference · Design · Changelog


Port numbers wander between runs and read like noise (localhost:5173 means nothing). Names do not. cherrypicker is a tiny loopback reverse proxy: tell it docs lives on port 8080 and http://docs.localhost works in every browser.

$ cherrypicker start
proxy up — routes serve at http://<name>.localhost (Ctrl-C to stop)
$ cherrypicker route docs 8080
http://docs.localhost

*.localhost already resolves to loopback on Windows, macOS, and systemd Linux — no DNS setup, no hosts-file edits, no root certificate, no Node.

Install

From Hex (0.1.0 ships shortly):

mix escript.install hex cherrypicker

From source, today:

mix escript.install github holsee/cherrypicker

Both install the cherrypicker binary into ~/.mix/escripts — make sure that directory is on your PATH. To use only the Elixir client library, skip the escript and add the dependency (see From Elixir).

Agent skill

The repo ships an agent skill (skills/cherrypicker/SKILL.md) that teaches coding agents the daemon lifecycle, the register model, and the --json envelopes:

gh skill install holsee/cherrypicker

The daemon

Everything routes through one long-running process:

cherrypicker start

That binds port 80, which is what makes the URLs bare. Windows and modern macOS let a normal user bind 80; on Linux it needs cap_net_bind_service or a different port. When 80 is taken or refused:

$ cherrypicker start --port 7777
proxy up — routes serve at http://<name>.localhost:7777 (Ctrl-C to stop)

URLs then carry the :7777 suffix, once and permanently — the names stay stable either way. --port 0 binds any free port.

On start the daemon writes its bound port to ~/.cherrypicker/daemon.json (override the directory with CHERRYPICKER_HOME); every client finds it through that file, so there is nothing to configure. The file is removed on clean shutdown. The daemon runs in the foreground — stop it with Ctrl-C. Routes live in memory and are gone when it stops.

The daemon binds loopback only: nothing is reachable from other machines.

Working with routes

cherrypicker never wraps or spawns your app — the register model. Start your dev server however you normally do, then say where it is:

$ cherrypicker route docs 8080
http://docs.localhost
$ cherrypicker ls
cherry.localhost → 127.0.0.1:4000
docs.localhost → 127.0.0.1:8080
$ cherrypicker unroute docs
docs unrouted

Registering an existing name replaces its port — exactly what a dev server restarting on a new port wants. Names are lowercase DNS labels (dots allowed: api.myapp); cherrypicker is reserved for the control API.

Every verb takes --json for a machine-readable envelope, and exit codes are 0 success / 1 failure / 2 usage — built for agents as much as humans:

$ cherrypicker ls --json
{"command":"ls","ok":true,"routes":[{"name":"cherry","port":4000}]}

Requests to a named URL stream through chunk by chunk in both directions, so SSE and live-reload connections stay live. See the API reference for the complete CLI, proxy, and HTTP control-API contract.

From Elixir

Add the client to any BEAM app (0.1.0 on Hex shortly; the GitHub dep works today):

{:cherrypicker, "~> 0.1"}

The client is deliberately zero-cost: it uses only the standard library, starts no processes, and never opens a port as a side effect of being in a deps list. Register your app's server at startup:

case Cherrypicker.register("mysite", port) do
{:ok, url} -> IO.puts("also at " <> url)
{:error, :no_daemon} -> :ok # fall back to the port URL
end

{:error, :no_daemon} is the designed quiet path — no daemon running costs one failed connect and your app prints its port URL as ever. Also available: Cherrypicker.unregister/1, Cherrypicker.routes/0, and Cherrypicker.daemon_port/0.

A host app can also embed the daemon itself in its supervision tree:

children = [
{Cherrypicker.Daemon, port: 8080}
]

Cherry sites get all of this built in as cherry serve --name mysite.

Why not portless?

portless proved the idea. It is also a global npm install that self-elevates and installs a root CA — a maximal supply-chain surface — and it wraps your dev server as a child process, injecting per-framework port flags. cherrypicker is the BEAM answer: two runtime dependencies (Bandit, Finch), no Node, no certificate authority until TLS ships as an explicit opt-in, and no process wrapping — apps register themselves.

Status

HTTP proxying with full streaming (SSE and live-reload safe) works and is tested on Linux and Windows. TLS via an opt-in local CA, WebSocket passthrough, and background daemonization are designed but not built — see DESIGN.md.

Licence

MIT or Apache-2.0, at your option.