Real hostnames for the dev servers already running on your Mac.
http://localhost:3000http://myapp.test
Every alias gets its own loopback address and one line in /etc/hosts. A small root process then splices port 80 on that address to the port your dev server is already listening on. One admin prompt, nothing permanently installed, and https:// when you want it.
There is no signed or notarized build to download. Build it from source — nothing on your system changes until you launch it and accept the prompt. Apple Silicon only.
build from source
git clone https://github.com/rioukkevin/localhost-aliases cd localhost-aliases bun install make bundle # builds dist/LocalhostAliases.app make install # copies it into /Applications
Needs Bun 1.2.5+ and the Xcode command line tools (swiftc). make uninstall reverses every change, including the /etc/hosts block.
The download page has the version, the size, the sha256 and what Gatekeeper will actually say. The FAQ answers the sceptical questions: what runs as root and for how long, why there is no https, and what is left behind when you uninstall.
Patchbay
3 aliases · 2 livePort- myapp.test127.0.0.2:80 → 127.0.0.1:3000:3000
- api.myapp.test127.0.0.3:80 → 127.0.0.1:3001:3001
- docs.test127.0.0.4:80 → 127.0.0.1:4321:4321
the problem
localhost:3000 tells you nothing
A port number is not a name. Everything your browser keys on the origin gets keyed on a number that means nothing and moves the next time something else grabs it first.
Without aliases
- localhost:3000
- localhost:5173
- localhost:8080
- localhost:4321
- localhost:3001
Five ports, no names. Cookies ignore the port, so all five share one localhost cookie jar too.
With aliases
- shop.test
- api.test
- docs.test
- admin.test
- blog.test
A separate origin each, so history, autofill and cookies finally key on something that means the project.
- The tab barFive projects open, and the tabs read :3000 :3001 :5173 :4321 :8080. You find the right one by clicking until it looks familiar.
- History and autofillBoth key on the origin. Search your history for the project and you get nothing, because the project was never called anything.
- CookiesCookies ignore the port, so everything served from localhost shares one cookie jar. Two apps that both set session overwrite each other.
- Sharing a URLA link in a README or a ticket only works if the reader happens to have started the same server on the same port.
the mechanism
A name, an address, and a splice
DNS maps a name to an IP, never to a port. So each alias gets its own loopback IP, and a raw TCP forwarder carries port 80 on that address to the port you already use.
myapp.test
what you type
1 · resolve
/etc/hosts
127.0.0.2:80
an lo0 alias, loopback only — the root agent listens here
2 · forward
raw TCP
127.0.0.1:3000
your dev server, exactly as you started it
- 1 — a loopback addressThe lowest free address in 127.0.0.2 … 127.0.0.254 is assigned when the alias is created and stays with it for life. 127.0.0.1 is never used. That is a ceiling of 253 aliases, and the app says so if you reach it.
- 2 — one line in /etc/hostsAll entries live inside a single managed block between markers. Everything outside those markers is preserved byte for byte, so whatever else you keep in /etc/hosts is left exactly as it was.
- 3 — raw byte forwardingA small root process binds :80 on that address and copies bytes to 127.0.0.1:<your port>. It parses nothing — no Host header, no header rewriting — so WebSockets, HMR and non-HTTP protocols pass straight through with no configuration.
what changes here
Two markers in /etc/hosts, an address on lo0, one prompt
Everything privileged is one idempotent apply. This is the whole of it, so you can decide before you type a password rather than after.
Above and below the markers: preserved byte for byte.
lo0 alias
ifconfig lo0 alias 127.0.0.2
One loopback address per alias, taken from 127.0.0.2 … 127.0.0.254 and kept for the alias’s life. 127.0.0.1 is never used, and an address the agent did not allocate is never removed.
one admin prompt
Once, when the app launches. That prompt starts the root agent, which is also the forwarder. Adding, removing, renaming and re-porting aliases after it never ask for a password. A reboot clears lo0, so the next launch prompts again.
nothing installed
- /Library/LaunchDaemons/*.plist
- SMAppService privileged helper
- sudo install script
The root agent exits by itself when the app stops touching its liveness file, so quitting leaves nothing running as root — and needs no second prompt.
what quitting leaves
The /etc/hosts block and the lo0 addresses, so your names still resolve. make uninstall, the tray’s Uninstall item and the dashboard all run the same script and remove both behind one prompt.
What runs as root
four operations, nothing else- Loopback addresses. ifconfig lo0 alias|-alias 127.0.0.x — only inside the pool, and only for addresses it allocated itself.
- The managed block. It rewrites /etc/hosts between the two markers, atomically, and refuses to write at all if a byte outside them would change.
- The DNS cache. dscacheutil -flushcache and killall -HUP mDNSResponder, so the new name answers immediately.
- Bytes between sockets. It binds :80 on each of those addresses and copies bytes to 127.0.0.1:<your port>.
That is the entire list. The agent re-validates every hostname, address and port it reads before acting on any of them, and nothing named in that file is ever executed. One bad entry rejects the whole file and leaves the previous state in place.
optional, off by default
https:// with a real padlock
Raw byte-forwarding and TLS are not mutually exclusive here, because the address already identifies the alias.
Turn it on in Settings and every alias also answers on https://, with http:// still bound so nothing you have bookmarked breaks. It works because each alias owns its own loopback address: a listener on 127.0.0.3:443 already knows which alias it is, so it presents that certificate without reading a byte. No Host header is parsed and no SNI is inspected — after the handshake it is the same raw splice, which is why WebSockets and HMR are unaffected.
One step is deliberately not automatic
The certificate is issued for you and renews itself. Telling your Mac to trust the authority that signs it is not automatic: macOS asks for your keychain password, and an app that silently installs a trusted root would be indistinguishable from malware. Settings gives you the exact command, and Uninstall removes it again by fingerprint.
Firefox keeps its own list of trusted authorities and will still warn until you add it there too. Leaving https off is a perfectly good answer — everything works over http://, with the caveat that a .test name is not a browser “secure context”, so service workers and getUserMedia need https or http://localhost.
the other real limitation
A root process acts on a file your user account can write
This is what one prompt per launch actually costs, so it is stated here rather than discovered later.
While the app runs, anything running as you can reach root through it
The prompt at launch starts the root agent. It watches ~/.config/localhost-aliases/desired-state.json — a file your user account can write — and reconciles the machine to it with no further password. So any process running as you can ask root to edit /etc/hosts and add loopback addresses, silently, for as long as the app is open. That is a real local privilege escalation, and it is the price of not being asked for your password on every edit. Quit the app and nothing runs as root.
It is bounded by validation the agent does itself, never trusting the file: hostnames re-checked on every read (no whitespace, no #, no newlines, no localhost); only 127.0.0.2–127.0.0.254 ever added to or removed from lo0, never 127.0.0.1 and never an address it did not allocate; /etc/hosts edits confined to the managed block and refused if one byte outside it would change; a port at or below 1024 bound only on a pool address. One bad entry rejects the whole file and leaves the previous state untouched. Nothing named in that file is ever executed.
scope
What it does not do
A tool that edits /etc/hosts as root should be explicit about its edges.
- Terminate TLS for aliaseshttp:// only, for the reason above. Only the dashboard can be served over https://.
- Run or supervise your serversYour dev server on :3000 is not started, restarted, wrapped or touched. If nothing is listening, the alias simply reports no server.
- Expose anythingEvery address it creates is loopback. Nothing becomes reachable from your network or the internet — this is not a tunnel and not a sharing tool.
- Inspect or rewrite trafficNo headers are touched and no requests are logged. It cannot do either: it never parses the bytes it forwards.
- Leave a background serviceNothing survives quitting the app except the /etc/hosts block and the lo0 addresses, and make uninstall removes both.
- Run anywhere but macOSIt is built on /etc/hosts, lo0 aliases and a Swift menu-bar app. There is no Linux or Windows build.
- Ship signed or notarizedNo signed, notarized build has been published yet. Today you build it from source on your own machine.
for coding agents
An MCP server, so your agent knows which URL is which
A stdio MCP server ships with the app and talks to the local dashboard API. Setup installs it into Claude Code or Codex in one click, with a copy-paste snippet as the fallback.
Tools
6 exposed- list_aliases
- list_projects
- create_alias
- delete_alias
- link_project
- get_usage_instructions
What that buys you
- The agent can read the aliases and projects on this machine, so it writes http://myapp.test in your docs and tests instead of guessing a port.
- It can register an alias for the repo it is working in, and link a folder to a URL.
- While the root agent is running, an agent’s alias changes land without a further password prompt — the same as your own. If that is not what you want, quit the app: nothing runs as root once it is closed.
before you start
Requirements
Short list, plus the one thing worth knowing about the TLD before you name anything.
- macOS 13 or later, Apple SiliconThe menu-bar app is built with a 13.0 (Ventura) deployment target and an arm64-only Swift target, so it runs on Apple Silicon. There is no Intel or universal build today — building on an Intel Mac produces an arm64 binary that will not launch there.
- An admin accountYou are asked for your password once per app launch, to start the root agent. Alias changes after that — adding, removing, re-porting — never prompt again.
- To build itBun 1.2.5 or later and the Xcode command line tools (swiftc). Then make bundle && make install.
- Names end in .test.test is reserved for development by RFC 6761: never delegated, never publicly resolvable, and claimed by nothing on macOS, so the /etc/hosts entry answers immediately. .local is not offered — mDNSResponder owns that suffix and every lookup waits out a multicast query, about five seconds per name, measured. HSTS-preloaded TLDs like .dev and .app are refused too: browsers force them to https://, which aliases cannot serve.