Many dev servers, many worktrees, no port fights

Parallel Claude Code worktrees all want port 3000. How Shuriken finds dev servers, hands each worktree its own port, and tips that work without it.

:3000 in large pixel lettering beside a real screenshot of Shuriken with chats at work, each on its own branch
Contents

Claude Code worktree port conflicts went away in our own repository once every worktree got its own port and the tools read it, instead of all of them hard-coding 3000. In Shuriken, a script you mark as a service in shuriken.json gets a free port per worktree in SHURIKEN_PORT, and the Dev servers list shows every server with its port and its device. Without Shuriken, read the port from an environment variable and make the dev server exit when the port is taken.

Every worktree wants port 3000#

In Shuriken, a chat can work on its own branch in its own git worktree, so agents stop editing each other's files (parallel agents explains how). That holds until the third such chat runs npm run dev.

A worktree is a full checkout. Every copy of the project has the same package.json and the same dev script, so the first one takes 3000 and the second one crashes with EADDRINUSE. Some tools don't crash. They move to 3001 and say so in a log line nobody reads, and the agent opens localhost:3000, tests the other branch's code and reports that the fix works.

We hit this often in our own repository, where a background service, an app and a desktop shell run side by side in many worktrees at once. So we fixed it for us first.

Left, before: three worktrees, main, fix-login and new-nav, all point at port 3000; the two later ones are crossed out in red. Right, after: each worktree points at its own port, 3000, 3101 and 3102. Before: every worktree asks for 3000 After: a port per worktree main fix-login new-nav :3000 The second server fails, or drifts to a port nobody told the agent about. main :3000 fix-login :3101 new-nav :3102 Each service reads its own port from SHURIKEN_PORT.

How Shuriken finds your dev servers#

It looks in two places.

The first is the project's files. It reads package.json and its lockfile, because the lockfile decides between pnpm run dev and npm run dev, plus the Python, Ruby, PHP and Docker files beside it (manage.py, pyproject.toml, Gemfile, artisan, docker-compose.yml, a Procfile). From those it picks the long-running scripts. It names a port only when the command says it, with --port or -p, or when the tool has a documented default. Otherwise it waits to see.

Server it recognises Default port it expects
Vite 5173
Next.js (next dev) 3000
Storybook 6006
Rails (bin/rails server) 3000
PHP's built-in server (php -S), php artisan serve 8000
Django (python3 manage.py runserver) 8000

The second is what is really listening, which catches a server you started by hand in another terminal too. Shuriken asks the operating system, under the chats it runs and in your project folders:

System What it runs
macOS lsof -nP -a -p <pids> -iTCP -sTCP:LISTEN
Linux ss -Hltnp, or lsof when ss fails
Windows netstat -ano -p TCP and -p TCPv6

Both feed the Dev servers list: live, idle or failed, with the port and the device each one runs on. When a server can't start because its port is taken, the row names the holder, as in "Port 3000 is in use by node (PID 48211)", and says that freeing the port lets yours start.

"Start this server", right where you need it#

Open a page in Shuriken's browser whose server isn't running and the page shows a Start this server button instead of a blank error. Press it, and the page loads as soon as the server answers.

The server can be on another computer. Since 3.1, a dev server running on another of your devices opens live in Shuriken, so a chat running on the desktop can be checked from the laptop.

A port per worktree, handed out for you#

Mark the long-running scripts as services in shuriken.json:

json
{
  "scripts": {
    "web": { "type": "service", "command": "PORT=$SHURIKEN_PORT npm run dev" },
    "api": { "type": "service", "command": "PORT=$SHURIKEN_PORT npm run api" }
  }
}

When a service starts, Shuriken picks a free port for it in that worktree and passes it as SHURIKEN_PORT, along with HOST. It doesn't set PORT, which is why the command above passes it on. (PORT=… before a command is macOS and Linux shell syntax.)

Each service also gets the others' addresses. In the example, web sees SHURIKEN_SERVICE_API_PORT and can reach the api of its own worktree through SHURIKEN_SERVICE_API_URL, never another branch's. Through Shuriken's local proxy each service has a stable name made of the script, the branch and the project, like web--fix-login--shop.localhost.

To keep ports in a known range, say for a firewall rule, add "worktree": { "servicePorts": { "range": "3000-4000" } }. Shuriken then picks free ports from that range, both ends included.

Some tools ignore the port you hand them: a monorepo's npm run dev may start Vite on its own port anyway. Our own Run menu fell for it. It said the service was running, and opening it asked the device for a port nothing listened on. Shuriken now watches the service's process tree, every half second for the first 10 seconds and then every 2 seconds for up to 5 minutes, and follows the port it really listens on. The Dev servers list and the browser open that port.

Fixing worktree port conflicts without Shuriken#

The same ideas work with no app at all.

  1. Never hard-code the port. Read it from an environment variable with a default, PORT=${PORT:-3000}. Most frameworks already do.
  2. Fail loudly. With --strictPort, Vite exits when the port is taken instead of drifting to the next one, and a crash in the terminal is easier to see than a server on a port nobody expected.
  3. Give each worktree its own number: PORT=3102 npm run dev, or the tool's flag.
  4. Tell the agent the port, in the chat or in the project's agent instructions: "the dev server for this worktree is on port 3102". Agents guess 3000 otherwise.
  5. Find the squatter with lsof -nP -iTCP:3000 -sTCP:LISTEN on macOS and Linux, or netstat -ano | findstr :3000 on Windows. It's usually yesterday's server from a worktree you forgot.
Tool Port flag Notes
Next.js next dev -p 3102 Respects PORT, but reads it from the shell, not from .env.local: put the number in the command or a script
Vite vite --port 3102 Add --strictPort to exit instead of moving on

Merging the branches back one at a time, and the limits of a worktree per agent, are covered in how to run Claude Code agents in parallel; parallel agents is how Shuriken runs them.

The quiet win#

Port fights look small until an agent "verifies" a change against the wrong server. With a port per worktree, an agent that says the page works has looked at its own page.

The Dev servers list has been there since 3.1; the SHURIKEN_PORT and SHURIKEN_SERVICE_* names in this post are the ones Shuriken 5.1 uses. Try Shuriken to get it.

Frequently asked questions

Why do parallel Claude Code worktrees fight over ports?

Because each worktree is a full copy of your project, each one's dev script asks for the same port. Whichever server starts first gets it; the next one fails, or quietly moves to another port the agent doesn't know about.

How do I find what is holding a port?

lsof -nP -iTCP:3000 -sTCP:LISTEN prints the process on macOS and Linux, and netstat -ano | findstr :3000 prints its PID on Windows. If a dev server can't start in Shuriken because its port is taken, the Dev servers list already names the command and PID holding it.

Does Shuriken set PORT for my dev server?

No, it sets SHURIKEN_PORT (and HOST) for scripts you declare as services in shuriken.json. You pass it on yourself, for example with PORT=$SHURIKEN_PORT npm run dev.

Can I keep ports in a fixed range?

Yes: set worktree.servicePorts.range in shuriken.json to something like 3000-4000, and Shuriken picks free ports from that range.

Free tools