branch-localhost
v0.1.0
Published
Per-branch <branch>.localhost:<port> for local dev — deterministic port per git branch, free-port probing, env injection.
Downloads
194
Maintainers
Readme
branch-localhost
Run your local dev server at a stable, per-git-branch URL like my-feature.localhost:4123. Deterministic port per branch, free-port probing, env injection — one command, no config files, no global state.
branch-localhost --base-port 4000 -- next dev
# - Branch URL: http://my-feature.localhost:4123
# (next dev runs with PORT=4123 and DEV_HOST_URL/DEV_HOST_HOST set)Why?
- Stable URL per branch. Same branch → same port, every time. No more "which tab was the auth feature on?".
- Isolated cookies & storage.
*.localhostis a separate origin in modern browsers — branches don't stomp each other's session. - No collisions across branches/apps. Different branches in the same app
get different ports (hash of branch). Different apps get disjoint ranges
(each picks its own
--base-port). - Free-port aware. If the seeded port is busy, probes the next ones in the range.
- Works great with git worktrees. Each worktree is on its own branch, so each gets its own deterministic URL — run several dev servers in parallel with zero coordination. See Git worktree workflow.
Installation
npm install -D branch-localhost
# or pnpm add -D branch-localhost / yarn add -D branch-localhostRequires Node 18+ and a git working tree (uses git rev-parse --abbrev-ref HEAD).
Usage
branch-localhost [options] [-- <command> [args...]]The bit after -- is the command to spawn. It inherits stdio and receives
the computed env vars. Exit code and signals are forwarded.
CLI
| Option | Type | Required | Default | Description |
| ------ | ---- | -------- | ------- | ----------- |
| --base-port <n> | number | no | 3000 | First port of the range |
| --range <n> | number | no | 1000 | Range size (ports [base, base+range)) |
| --probe-limit <n> | number | no | 50 | Max attempts to find a free port |
| --env-url <NAME> | string | no | — | Set NAME=http://<host>:<port>. Repeatable. |
| --env-url-slash <NAME> | string | no | — | Set NAME=http://<host>:<port>/. Repeatable. |
| --env-port <NAME> | string | no | — | Set NAME=<port>. Repeatable. |
| --env-host <NAME> | string | no | — | Set NAME=<host>. Repeatable. |
| --show | boolean | no | false | Print URL to stdout and exit. Skips port probing. Implies --quiet. |
| --quiet | boolean | no | false | Suppress the "Branch URL" stderr line. |
| -h, --help | boolean | no | — | Show help |
| -v, --version | boolean | no | — | Show version |
Always-set env vars (wrapper mode): DEV_HOST_URL, DEV_HOST_PORT, DEV_HOST_HOST.
Examples
Next.js — Next reads PORT, so just point it there:
{
"scripts": {
"dev": "branch-localhost --base-port 4000 --env-port PORT -- next dev"
}
}Next.js with public site URL injection:
{
"scripts": {
"dev": "branch-localhost --base-port 4000 --env-port PORT --env-url-slash NEXT_PUBLIC_SITE_URL --env-url BASE_URL -- next dev"
}
}Vite — Vite reads PORT too, or pass via --port:
branch-localhost --base-port 5173 --env-port PORT -- viteMonorepo with two apps on disjoint ranges:
// apps/web/package.json
{ "scripts": { "dev": "branch-localhost --base-port 4000 --env-port PORT -- next dev" } }
// apps/admin/package.json
{ "scripts": { "dev": "branch-localhost --base-port 5000 --env-port PORT -- next dev" } }Print the URL without running anything:
branch-localhost --base-port 4000 --show
# → http://my-feature.localhost:4123
open "$(branch-localhost --base-port 4000 --show)" # open it in the browserGit worktree workflow
This is where the tool shines. With git worktrees, each checkout lives in its own directory on its own branch. Spin up a few in parallel and each gets a unique, stable URL — no port collisions, no "which terminal had which feature?".
# Layout:
# ~/proj/main (master)
# ~/proj/auth (worktree, branch: feat/auth)
# ~/proj/checkout (worktree, branch: feat/checkout)
cd ~/proj/auth && pnpm dev # → http://feat-auth.localhost:4291
cd ~/proj/checkout && pnpm dev # → http://feat-checkout.localhost:4807
cd ~/proj/main && pnpm dev # → http://master.localhost:4145All three run side-by-side. Cookies and localStorage are isolated per subdomain. The port is the same every time you start that worktree.
To grab a URL from a script (e.g., to open in browser or paste in Slack):
# from inside any worktree:
branch-localhost --base-port 4000 --showTo open every running worktree at once (Bash):
for wt in $(git worktree list --porcelain | awk '/^worktree /{print $2}'); do
open "$(cd "$wt" && branch-localhost --base-port 4000 --show)"
doneDetached HEAD (e.g. git checkout <sha>) falls back to sha-<short> as
the branch label, so each checked-out commit still gets a stable URL.
How it works
- Read current git branch via
git rev-parse --abbrev-ref HEAD. If detached (returnsHEAD), fall back tosha-<short>. - Sanitize → lowercase
[a-z0-9-], trim dashes, cap at 63 chars (DNS label limit). Host becomes<sanitized>.localhost, or justlocalhostif the branch sanitizes to empty. - Hash the sanitized branch (deterministic) → pick a port inside
[base-port, base-port + range). - If that port is busy, probe successive ports (wrapping inside the range)
up to
--probe-limittimes. (--showskips this — it returns the deterministic seed.) - Spawn
<command>with stdio inherited and the chosen env vars set.
FAQ
Does *.localhost actually resolve?
Yes — Chrome, Firefox, Safari, and Edge all resolve *.localhost to
127.0.0.1 by default (per RFC 6761).
Most modern OS resolvers (macOS, Linux with systemd-resolved) also do this.
If your environment doesn't, add an /etc/hosts entry or pick another
suffix and patch this script.
What about Windows?
The wrapper uses shell: true on Windows to resolve .cmd shims. Should
work but is less battle-tested than Mac/Linux.
Does the port survive a branch rename? No — different branch name → different hash → different port. Same branch name is deterministic across machines.
License
MIT
