How it works
Three things stand between typing api.test in a browser and your dev server answering. LocalRun
owns all three, and none of them require you to edit a file or type a password.
*.test ──► DNS responder ──► 127.0.0.1
│
browser ──► Caddy :80/:443 ──► your project on 127.0.0.1:<port>
▲
daemon (DNS + Caddy + processes + IPC) ◄── Electron tray UINothing is installed into your project. LocalRun stores a name, a folder and a port, and generates its own Caddyfile. Remove the project and your folder is exactly as it was.
The name resolves
macOS lets you route a whole TLD to a resolver by dropping a file in /etc/resolver/. LocalRun
writes /etc/resolver/test, and a bundled DNS responder answers every *.test query with
127.0.0.1.
The responder listens on an unprivileged high port, 15353 by default, and the resolver file
names that port explicitly:
nameserver 127.0.0.1
port 15353That port is not a detail you can skip. macOS lets an unprivileged process bind a privileged port
on the wildcard address but not on a specific one: 0.0.0.0:53 is allowed, 127.0.0.1:53 returns
EACCES. The responder binds loopback deliberately, so it stays off your LAN, which means it
cannot use port 53, which means the resolver file has to say where it actually is.
If the daemon binds a different port than the resolver file names, every .test name stops
resolving while everything else still looks healthy. Settings reports this as stale, and
re-running setup rewrites the file.
Two smaller details matter more than they look. AAAA queries are answered NOERROR with zero
answers rather than ignored; silence makes getaddrinfo wait out a timeout and every page load
stalls about a second. And after the resolver file is written the DNS cache is flushed, because
macOS caches negative lookups and your first request would otherwise still fail.
The certificate is real
Caddy listens on :80 and :443 and reverse-proxies each host to the right local port. For
projects with TLS on, it issues a certificate from its own internal CA, and LocalRun trusts that CA
in your login keychain.
Browsers see an ordinary valid certificate. There is no warning page and no exception to click through, because nothing is self-signed as far as the browser is concerned; the issuing CA is genuinely trusted on your machine.
If you already run Caddy yourself, LocalRun does not compete for the port. It writes a fragment and
adds a single import line to your Caddyfile, then reloads through the admin API.
Your process is supervised
The daemon starts your run command, captures its output, watches whether it is alive, and can restart it. Everything the tray shows (the status dot, the log tail, the health check) comes from that supervision rather than from guessing.
It also notices servers it did not start. If something is already listening on a port, LocalRun can proxy to it without restarting anything, and it will say plainly that it does not own the process.
One seam
The daemon holds all the state and all the privilege. The tray app holds none: every button is a
JSON-RPC call over a unix socket at ~/.localrun/daemon.sock.
That split is why the UI cannot mislead you about what it can do: the daemon classifies each
service and returns what is actually possible, and the tray renders exactly that. It is also why
.test names keep working while the tray app is closed, as long as the daemon is running.
Next: Service kinds explains what LocalRun can and cannot do to a given process, and why.