confit ports: collision-free dev ports across git worktrees
confit v0.5.0 adds a [ports] section that assigns each project a fixed port band and gives every active git worktree its own slot — no more port collisions when running multiple branches concurrently.
I use git worktrees constantly. Three or four branches active at once, each in its own directory, each running its own dev server. It's a great workflow until you try to run two of them at the same time and both reach for port 3000.
The usual fix is manual: hardcode different ports per branch, or add a port offset to your shell profile and remember to set it. Both are fine until you forget, and then you're chasing a connection-refused error that has nothing to do with your code.
confit v0.5.0 ships a [ports] section that solves this at the config layer. Each project gets a fixed 100-port band. Shared infra (postgres, redis, whatever) gets fixed offsets within that band. Each active git worktree gets its own per-branch slot for HTTP services. No two concurrently checked-out branches can ever collide — structurally, not probabilistically.
What it looks like
Here's a minimal confit.toml with [ports]:
[ports] band = 4300 [ports.infra] postgres = 0 redis = 1 [ports.services] app = 50 site = 70
band = 4300 anchors the whole project at port 4300. Infra ports are fixed offsets within the band — postgres is always 4300, redis is always 4301, regardless of which branch you're on. That's intentional: shared infra should be shared.
Service ports are where the per-worktree slot comes in. On main, slot 0 is assigned. On your feature branch, slot 1. The app service with an offset of 50 resolves to 4300 + 50 + (slot * 10) — so main gets 4350, the feature branch gets 4360. Same logic for site: 4370 and 4380.
All of these resolve into normal confit values. You reference them the same way you'd reference anything else:
[db]
url = "postgres://localhost:{ports.infra.postgres}/app_{ports.branch_slug}"
[server]
port = "{ports.services.app}"No new commands, no new concepts. confit run server -- ./target/debug/myapp injects the right port as an env var. confit resolve ports.services.app prints it. confit show ports --yaml dumps the whole resolved section. Everything that already worked with confit still works the same way.
How slot assignment works
The first cut of this used a hash of the branch name to assign slots: 0 for main/master, hash(branch) % 9 + 1 for everything else. That works fine until you have enough active branches and two hash to the same slot. With 9 buckets that's birthday-paradox territory — not unlikely at all once you have a few long-lived branches running concurrently.
v0.5.0 replaces that with a stateful allocator. There's a small ledger file at <git-common-dir>/confit/ports.toml — inside .git, per-clone, shared across all worktrees of the repo, never committed. On every resolve:
List every currently checked-out worktree via
git worktree listand drop any ledger entry whose branch isn't in that list — proactive pruning, so a removed or merged worktree's slot is freed immediately.If the current branch already has a slot in the ledger, reuse it.
Otherwise, take the lowest free slot, record it, persist.
This packs slots tightly. Three active worktrees get slots 1, 2, 3. Remove the middle one, add a new branch — it gets slot 2, not slot 4. Same-slot collisions between concurrently-active branches are impossible, not just unlikely. If you somehow have ten branches checked out simultaneously, the eleventh fails with a clear "no free port slot" error instead of silently doubling up.
Concurrent confit invocations across worktrees are coordinated with an fd-lock flock on the ledger file. No custom lockfile logic, no stale-file heuristics — flock is released by the OS when the process dies, so there's nothing to get stuck.
validate --host, now automatic
There's also a host-checking pass that runs automatically whenever [ports] is in scope. confit validate now checks:
Port collisions within the band (two infra entries with the same offset)
Privileged ports (below 1024)
Ports inside the OS ephemeral range (platform-aware — macOS and Linux have different ranges)
Service ports already bound on the host
Ledger corruption
This used to require a --host flag. I dropped the flag because the checks are cheap and read-only — gating them behind a flag just meant people wouldn't run them. Now they're on by default when ports are in scope. Validating an unrelated section (e.g. confit validate vars) still skips the port checks.
Lazy resolution
One thing I was careful about: port resolution is lazy. Config::build validates the shape of [ports] eagerly — serde catches malformed tables immediately — but actually reading the git branch, acquiring the ledger lock, and computing slot values are deferred until something actually touches ports.*.
This matters in practice. Commands that don't need port values don't spawn git, don't take the ledger lock, and don't fail if you're not inside a git repo or if the ledger is temporarily unavailable. A misconfigured [ports] section shouldn't break confit resolve db.password.
A note on the internals
The PR also centralized all git shell-outs behind a new Git client in confit-core/src/git.rs. Before this, confit export's gitignore guard and the new ports code each had their own inline Command::new("git") calls. That's the kind of thing that accretes until you have five slightly-different git invocations scattered across the codebase. One client, four methods (current_branch, check_ignore, common_dir, worktrees), called from wherever needs it.
The v0.5.0 release also covers a bunch of internal correctness fixes that landed as part of the branch audit before merge: the provider-double-invoke bug (every confit resolve through a provider was running the provider command twice), improved source cache sharing so a source's load command doesn't re-run on repeated calls, and stricter [vars] validation at build time that catches unknown variable names at config-build rather than silently ignoring them.
Try it
If you're already running confit, update and add a [ports] section to your confit.toml. The docs at confit.krondor.org have a full breakdown of band arithmetic, slot mechanics, and the primary-branch override for setting a fixed slot on main.
curl -fsSL https://raw.githubusercontent.com/krondor-corp/confit/main/install.sh | bash
If you're not running confit yet — it's a single Rust binary, no dependencies, drops a commented confit.toml on confit init. The repo is at github.com/krondor-corp/confit.