Retiring Named Pipes: A Simpler Fix for Dev Port Conflicts

5 min read

Update, 05-10-2026: The first version of this post said the port lived in git config --worktree. That turned out to have a catch: git worktree add copies the current worktree’s config into the new one, so a fresh worktree could report someone else’s port. Since v0.4.0 the port lives in a small file in the worktree’s own git folder instead, and you read it with git wdp-port. I’ve updated the post below to match, and added a section on what went wrong.

Back in February I wrote about solving dev-server port conflicts with HTTP over named pipes. The short version: I run a lot of parallel Claude Code sessions, each in its own git worktree, and each one needs to spin up a demo site. Fixed ports don’t work when you’ve got several instances running at once, and dynamic ports create a discovery problem — how does a script or an AI agent find out which port a given worktree’s site landed on?

Named pipes were a genuinely fun rabbit hole, and they worked. But a few months of actually living with that solution taught me it had more moving parts than the problem deserved.

What started to bug me

Three things, mainly.

First, named pipes and Unix sockets are two different transports depending on whether you’re on Windows or macOS/Linux, so the Kestrel configuration had to branch on platform. Not the end of the world, but it’s exactly the kind of code that only gets exercised on someone else’s machine.

Second, the “which worktree am I in” logic — walk up from .git, check if the path contains worktrees, fall back to the branch name — had to be duplicated in C# for the server and JavaScript for the client generator. Every time I touched one, I had to remember to touch the other.

Third, and the thing that actually pushed me to fix it: I wanted the same trick in Umbraco.Automate, a completely separate repo with the exact same problem. Copy-pasting a named pipe listener, a custom /site-address discovery endpoint, and two copies of identifier-detection logic into a second codebase was the point where “clever solution” started to feel like “thing I now maintain in two places.”

The simpler idea: let git remember the port

The thing I’d been missing is that git already gives every worktree somewhere to keep its own private state: its git folder. The main checkout has .git/, and every linked worktree gets its own .git/worktrees/<name>/. Nothing in there is committed, nothing in there shows up in your project, and — this is the part I like most — the whole folder is deleted automatically when you run git worktree remove. No cleanup step to forget.

So instead of exposing a whole separate HTTP transport just so tools could avoid knowing the port, I flipped the problem around: what if the port itself was just… normal, and the only thing worth solving was remembering it?

The demo site picks a free port once, on first run in a given worktree, and writes it to a wdp-port file in that worktree’s git folder. Every later run in that worktree reads the same value back. The package also adds a small git alias to the repo, so any other tool — a client generator, a teammate, an AI agent — gets the same answer with one command:

git wdp-port

(On a fresh clone where the site hasn’t run yet, the alias won’t exist. The same lookup by hand is cat "$(git rev-parse --git-path wdp-port)".)

No pipes, no sockets, no platform branching, no custom discovery endpoint. Just a real TCP port that any HTTP client already knows how to talk to.

I pulled the whole thing out into a standalone, open-source package — Umbraco.Community.WorktreeDevPort — precisely so I wouldn’t be tempted to copy-paste it into Umbraco.Automate too.

How to use it

For the .NET side:

dotnet add package Umbraco.Community.WorktreeDevPort

That’s it. It’s picked up automatically as an Umbraco composer and only activates in Development. No configuration required, though WorktreeDevPort:BasePort and WorktreeDevPort:RangeSize are there in appsettings.Development.json if you want to change the defaults.

For tooling — client generators, scripts, CI — there’s a matching npm package:

npm install worktree-dev-port
import { getPort } from "worktree-dev-port";

const port = getPort();
const spec = await fetch(`https://127.0.0.1:${port}/umbraco/swagger/<package>/swagger.json`);

Compare that to last time’s socket-path juggling. It’s just… fetch.

A couple of refinements that fell out along the way

Two things came up while actually using this day to day that I ended up handling in the package rather than working around each time:

The main checkout gets a familiar port. If you’re just working normally — no worktrees, one checkout — you don’t want the port to be some arbitrary number from a pool. So the package reserves a fixed port (44355 by default) specifically for the main checkout, and only hands it out when nothing else is using it. Linked worktrees skip straight past it into the auto-assigned pool, so it stays free for the checkout that actually wants it.

An explicit :0 port gets ignored, not honored. If a launch profile still has "applicationUrl": "https://127.0.0.1:0" lying around — the old “ask the OS for a random port” trick, which is exactly what this package exists to replace — the package now detects that and assigns its own stable port instead of quietly handing control back to a random one. Small thing, but it’s the kind of leftover config that’s easy to forget you have.

The catch I missed first time round

The first version of this package didn’t use a file. It used per-worktree git config: git config --worktree wdp.port <port>. On paper it was perfect. A real git feature, scoped to one checkout, cleaned up on git worktree remove.

The problem showed up once I was actually spinning worktrees up day to day: git worktree add copies the current worktree’s config into the new one. So a brand-new worktree would start life already “knowing” a port — the port of the worktree I’d created it from. A tool asking that new worktree for its port would get a confident, wrong answer, and quietly talk to a different site.

That’s a nasty kind of bug, because nothing errors. So v0.4.0 moved the port into a plain file in the worktree’s git folder, which git never copies. If you were on 0.3.0 or earlier, each worktree just picks its port again the first time it starts after upgrading, and anything that read wdp.port directly should switch to git wdp-port.

The bit I keep coming back to

In the original post I asked what other parts of our tooling still quietly assume a single developer, a single terminal, a single running instance. Named pipes were my answer at the time, and they weren’t wrong — they worked, and they taught me something. But they were also a custom-built answer to a problem git had already solved, sitting right there in the git folder I’d never had a reason to look inside before. (Even then, I reached for the wrong part of git first. The config looked like the obvious fit, and it took real use to find out it wasn’t.)

I don’t think the lesson is “named pipes were a bad idea.” I think it’s that the sign a solution has more moving parts than it needs isn’t that it doesn’t work — it’s that you find yourself explaining the same workaround to a second codebase. That’s usually when it’s worth asking whether the tool you’re reaching for has already solved the actual problem, one layer down.

If you’re wrangling the same multi-worktree, multi-agent port chaos, give it a try and let me know how it goes.

Until next time 👋