Stable named .localhost URLs for local dev servers.
For humans and agents, on the BEAM — no Node, no npm, no root CA.
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.