Preventing port conflicts in git worktrees using directory hashing
Coding agents are making git worktree popular because worktrees let
multiple agents work on the same repository at the same time without interfering with
each other. Each branch is checked out in its own directory: if your
main branch lives in main/, a feature-1 branch can
sit right next to it in feature-1/.
However, directory isolation doesn't isolate network ports. When a server in one
worktree runs on port 8080, starting the server in another worktree fails
because the address is already in use.
At first, I passed ports manually per worktree (like PORT=8001 make run),
which got tedious. Next, I decided to calculate a deterministic port number for each
worktree, using its absolute directory path. Because each worktree lives in a different
directory, every environment gets its own port automatically.
Hashing the directory path in your Makefile
To turn a directory path into a port number, take the SHA-256 hash of the current
working directory, convert the hex digest to an integer, and map it into 400 buckets of
5 ports across the 8000 to 9999 range:
full_path = os.getcwd().encode("utf-8")
path_hash = int(hashlib.sha256(full_path).hexdigest(), 16)
port = 8000 + ((path_hash % 400) * 5)
Spacing base ports 5 numbers apart (8000, 8005,
8010, and so on) gives you 400 buckets, making collisions across a handful
of local worktrees unlikely while reserving four adjacent ports (PORT + 1
through PORT + 4) for secondary services.
To avoid changing your application code, you can compute PORT directly in
your Makefile and export it as an environment variable:
# Allocate a 5-port bucket (8000, 8005, ..., 9995) from the worktree's path
ifeq ($(PORT),)
export PORT := $(shell python3 -c "import hashlib, os; h = hashlib.sha256(os.getcwd().encode('utf-8')).hexdigest(); print(8000 + ((int(h, 16) % 400) * 5))")
else
export PORT
endif
.PHONY: run
run:
@echo "Starting server on http://localhost:$(PORT)"
@uvicorn main:app --host 0.0.0.0 --port $(PORT)
If PORT isn't already set, Make runs the Python command once
(:=) to calculate the port and exports it to child processes. If you pass a
port manually (like PORT=9000 make run), the else branch keeps
your value and exports it instead. Using ifeq with := rather
than PORT ?= avoids re-running the Python subshell every time
$(PORT) is referenced.
Offsetting ports for secondary services
Because each worktree gets a 5-port bucket, you can assign up to four secondary services
to PORT + 1 through PORT + 4 without colliding with another
worktree's base port. Here's an example with the Firestore emulator:
FIRESTORE_PORT ?= $(shell expr $(PORT) + 1)
export FIRESTORE_EMULATOR_HOST ?= 127.0.0.1:$(FIRESTORE_PORT)
.PHONY: emulator
emulator:
@gcloud emulators firestore start --host-port=$(FIRESTORE_EMULATOR_HOST)
expr $(PORT) + 1 adds 1 to the base port, and exporting
FIRESTORE_EMULATOR_HOST tells the Google Cloud client libraries in your app
and test runner which emulator port to connect to automatically. Passing
--host-port=$(FIRESTORE_EMULATOR_HOST) binds the Firestore emulator to that
same address on startup.