# 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.

Source: https://shuriken.sh/blog/worktree-port-conflicts

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.

[Story](https://shuriken.sh/blog) By Shuriken Updated 8 November 2026 5 min read

**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](https://shuriken.sh/features/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.

## 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](https://shuriken.sh/features/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.

In Shuriken a chat can work on its own branch in its own worktree, and Finish merges it after your checks.

[Try Shuriken](https://shuriken.sh/)

## 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.

- Never hard-code the port. Read it from an environment variable with a default, PORT=${PORT:-3000}. Most frameworks already do.
- 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.
- Give each worktree its own number: PORT=3102 npm run dev, or the tool's flag.
- 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.
- 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](https://shuriken.sh/guides/run-claude-code-agents-in-parallel); [parallel agents](https://shuriken.sh/features/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](https://shuriken.sh/) to get it.

Share

[X](https://x.com/intent/post?url=https%3A%2F%2Fshuriken.sh%2Fblog%2Fworktree-port-conflicts&text=Many%20dev%20servers%2C%20many%20worktrees%2C%20no%20port%20fights)[LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fshuriken.sh%2Fblog%2Fworktree-port-conflicts)[Hacker News](https://news.ycombinator.com/submitlink?u=https%3A%2F%2Fshuriken.sh%2Fblog%2Fworktree-port-conflicts&t=Many%20dev%20servers%2C%20many%20worktrees%2C%20no%20port%20fights)[Reddit](https://www.reddit.com/submit?url=https%3A%2F%2Fshuriken.sh%2Fblog%2Fworktree-port-conflicts&title=Many%20dev%20servers%2C%20many%20worktrees%2C%20no%20port%20fights)[E-mail](mailto:?subject=Many%20dev%20servers%2C%20many%20worktrees%2C%20no%20port%20fights&body=https%3A%2F%2Fshuriken.sh%2Fblog%2Fworktree-port-conflicts)

## 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.

Parallel agents

Agents side by side, each on its own branch if you want.

A new chat gets its own branch and worktree unless you pick the project folder, so agents don't step on each other. Finish runs your checks and merges each one back.

[Try Shuriken](https://shuriken.sh/)

[How it works](https://shuriken.sh/features/parallel-agents)

## Keep reading

- [How Shuriken started: a tool we built for ourselves: Story 27 Oct 2026 4 min read We build software for clients and ourselves. No harness fit how heavily we use AI, so we built our own. Then a programmer asked: why don't we sell this?](https://shuriken.sh/blog/how-shuriken-started)
- [A git workflow for Claude Code that keeps main clean: Story 27 Oct 2026 4 min read The Claude Code git workflow we'd give anyone running several agents: a branch per task, small commits, read the diff, checks before merge, then squash.](https://shuriken.sh/blog/claude-code-git-workflow)
- [How to review AI-generated code without reading every line: Story 27 Oct 2026 5 min read We review agent diffs tests first: would each test fail without the change? Then deletions, the diff's edges, secrets, and a run. Our real misses.](https://shuriken.sh/blog/review-ai-generated-code)
- [Fork a chat from any reply, then merge it back: Update 27 Oct 2026 4 min read Try another approach without losing the first: fork a Shuriken chat from any reply, keep the fork under its parent, then merge its work back in.](https://shuriken.sh/blog/fork-and-merge-into)
- [Finish: checks, rebase and merge in one click: Update 27 Oct 2026 3 min read A Shuriken chat can work on its own branch. Finish commits what's left, rebases, runs your checks and squash-merges it, then cleans up.](https://shuriken.sh/blog/finish-one-click-merge)

## Free tools

- [Claude limit reset tracker](https://shuriken.sh/tools/limit-resets/claude)
- [Codex limit reset tracker](https://shuriken.sh/tools/limit-resets/codex)
