I saw Greg Raiz’s local.vibe post on Hacker News. The problem is familiar: once you have enough local projects, remembering localhost:5173 vs localhost:3001 vs whatever the browser extension dev server picked becomes annoying.

My setup is less ambitious. I already use Cloudflare Tunnel for most products I build, so my tunnel config became the directory of local dev services.

It is a boring YAML file, but it has the answer I need most often: what runs where?

The File

My ~/.cloudflared/config.yml has the exposed dev apps:

ingress:
  - hostname: dev.example.com
    service: http://localhost:5174
  - hostname: dev-backend.example.com
    service: http://localhost:4002
  - hostname: dev-myog.example.com
    service: http://localhost:5273
  - hostname: dev-myog-backend.example.com
    service: http://localhost:4010
  - hostname: dev-stacknaut.example.com
    service: http://localhost:5375
  - hostname: dev-stacknaut-backend.example.com
    service: http://localhost:3005
  - service: http_status:404

In my actual config, those are real hostnames. They go through a Cloudflare Tunnel to my machine. I use the dev domains as the normal URLs for those apps, including OAuth, webhooks, mobile callbacks, and testing from other devices.

For local-only things that should not be exposed through the tunnel, I add comments to the same file:

# Local-only dev ports reserved outside Cloudflare tunnels:
# - 3017: MyOG browser-extension WXT dev server
# - 3006: DevSnoop browser-extension WXT dev server

That’s it. Exposed services are normal ingress rules. Local-only services are comments at the bottom.

Why I Like This Better Than Another Dashboard

I don’t need a dashboard for this. I need one file to check.

Most of the time I don’t browse a launcher to find a project. I am already in the project, in tmux, or talking to a coding agent. What I need is for me and the agent to agree on which port and hostname belong to which thing.

The Cloudflare config already has to exist. It already maps names to ports. It has the exact shape I care about:

hostname -> localhost port

Adding another file just for agents would make the system worse. Now I have two places to update, and eventually one of them lies.

The Agent Angle

This ended up working well with coding agents.

My AGENTS.md tells coding agents to check ~/.cloudflared/config.yml before choosing or fixing dev ports. It also tells them that https://*.example.com domains are Cloudflare tunnels to my local machine, not deployed servers.

So when an agent needs to test a frontend, it does not guess:

  • It checks the tunnel config.
  • It sees the hostname and port.
  • It uses the dev domain.
  • If it adds a new exposed app, it updates the ingress list.
  • If it reserves a local-only port, it adds a comment at the bottom.

Browser extension dev servers, local helper tools, one-off WXT servers — these often do not belong on the public internet, even behind a hard-to-guess dev subdomain. But they still need a port. Putting them in comments keeps the list complete without pretending every local service should be tunneled.

The Config Is Also Documentation

I like documentation that is also configuration. It stays accurate because something depends on it.

A README that says “frontend runs on 5173” gets stale. A tunnel config that routes dev-myog.example.com to localhost:5273 gets fixed when it breaks.

The comments are the only weak part because they are not executable. But they are in the same file the agent already has to read, right next to the executable mappings. That is good enough in practice.

I also put a command note in the file:

# Adding new subdomains below requires running this command ("dev" = my tunnel name, don't change):
# cloudflared tunnel route dns dev dev-<new-subdomain>.example.com

This saves a question. When I ask an agent to add a new tunneled dev app, it has the naming convention and the command in front of it.

Why Not Auto-Assign Ports?

Auto-assigned ports are nice for tools that fully own process lifecycle. If a tool starts the app, injects $PORT, proxies the hostname, watches the process, and shuts it down, auto-assignment makes sense.

That is not my workflow.

I usually have projects already running in tmux. Some are frontend apps. Some are Fastify backends. Some are browser extension dev servers. Some are old projects with old assumptions. I want stable ports because stable ports make everything else boring:

  • OAuth callback URLs stay fixed.
  • Mobile apps can keep the same backend URL.
  • Browser bookmarks work.
  • Agents can test without asking me which app is running where.
  • Cloudflare Tunnel can expose selected services with real HTTPS.

The cost is that ports still need to be reserved. That’s fine. The agent can pick one, but it has to write the choice down in the same file.

What I Tell Agents

The key instruction in my global agent setup is simple:

### Dev Ports

Other local-only dev port usage (not Cloudflare tunnels, e.g. WXT dev servers)
is documented in comments at the end of ~/.cloudflared/config.yml.
Check/update there before choosing/fixing ports.

### Dev Domains

https://*.example.com are Cloudflare tunnels to the local machine,
not deployed servers. Config in ~/.cloudflared/config.yml

The file is useful to me, but it is more useful because the agents know it exists.

Without that instruction, agents guess. They search package files, find default Vite ports, try raw ports, maybe start another server, maybe collide with something already running.

With it, they check the registry first.

The Small Setup Works

I like tools like local.vibe. It solves more of the lifecycle problem: hostnames, HTTPS, process management, setup instructions for agents. If I wanted a self-contained local app launcher, I would look at it seriously.

But for my setup, Cloudflare Tunnel already covers the part I care about most. I get stable HTTPS dev domains, remote-device testing, webhook testing, and a readable mapping of services to ports.

The only extra habit I needed was treating ~/.cloudflared/config.yml as the local dev directory, not just tunnel config. Agents read it, use it, and update it.