Every worktree, one URL.

Which tab was :3001 again? Run your dev command through lhp and each git worktree keeps a named URL that never changes.

then two minutes to set up
zsh — auth
~/work/my-repo/auth
$ pnpm dev
> my-app@ dev
> lhp -- next dev
┌ localhost-proxy
worktree feature-auth.my-repo (feature/auth)
proxied http://feature-auth.my-repo.test
upstream http://localhost:52341
running next dev
▲ Next.js 16.2.4 (Turbopack)
✓ Ready in 243ms
~/work/my-repo/main
$ lhp list
feature-auth.my-repo http://feature-auth.my-repo.test -> :52341
main.my-repo http://main.my-repo.test -> :52007
$

Two minutes, once.

A real sequence — do it in order, then forget it exists.

1Point macOS at the proxy

One sudo command writes /etc/resolver/test, so *.test resolves to the proxy's built-in loopback DNS. Delete the file to undo. Prefer zero setup? lhp config domain localhost skips this step entirely.

$ lhp setup
Writing /etc/resolver/test …
Done. *.test now resolves to 127.0.0.1
whenever the daemon is running.

2Wrap your dev command

lhp allocates a free port, exports it as PORT, and passes everything through. Servers that ignore PORT take a {port} placeholder.

// package.json
"dev": "lhp -- next dev"
// vite ignores PORT
"dev": "lhp -- vite --port {port}"

3Run dev anywhere

The branch and repo names come from git — and in a monorepo each package gets its own app label, so turbo run dev registers the whole stack. Nothing to configure, no collisions. The dashboard at http://test shows everything running, grouped by repo.

$ pnpm dev
│ proxied http://feature-auth.my-repo.test

A daemon you never think about.

lhp wraps; a tiny proxy routes. The first lhp run starts a daemon on port 80 (no root needed on modern macOS — that's why the URLs have no port). It routes by hostname, pipes WebSockets straight through so HMR works, and registers name → port when your dev command starts.

Routes clean themselves up. Stop the dev server and its route is gone; crash it and the daemon prunes the dead registration. Restart the daemon and routes reload from disk. Visit a worktree that isn't running and you get a page listing everything that is.

Loopback only. Connections from other machines are dropped at the socket, the control API answers only to local hostnames, and cross-origin form posts are rejected. Your dev servers stay yours.

Apps learn their own URL. Auth flows break behind proxies when the app builds absolute URLs on localhost:<port> — so lhp exports AUTH_URL, NEXTAUTH_URL, and LHP_URL to the wrapped process, and any other framework can be fed through an "lhp" key in package.json with {url} templates. Your shell env always wins.

Reference

Defaults: proxy on :80, domain .test, names branch.app.repo. Change any of it, then lhp stop — the daemon restarts with new settings on the next run. Full detail in the docs.

lhp -- <cmd…>Wrap a dev command — register, run, unregister on exit. The -- is optional.
lhp listRegistered worktrees and their URLs
lhp status · lhp stopDaemon status; stop the daemon
lhp config port 7777Move the proxy off port 80 — URLs gain :7777
lhp config domain localhostZero-setup mode: *.localhost resolves natively, no sudo step
lhp config scheme <tokens>URL labels: a dot-list of branch, app, worktree, repo
lhp config https onServe https:// URLs via a local CA (name-constrained to your dev domains) — enables OAuth wildcard redirect URIs like https://*.my-repo.test/…
lhp setupOne-time /etc/resolver entry for any non-localhost domain
--name · --portPer-run overrides for the worktree name and upstream port

Domains that can't work are refused with the reason: .dev and .app are HSTS-preloaded (browsers force https), .local belongs to Bonjour. .test is IETF-reserved; anything made up works. For OAuth providers, whitelist each worktree's callback URL — e.g. http://feature-auth.my-repo.test/api/auth/callback/okta.