{"slug": "wtdev-deterministic-dev-server-ports-for-parallel-git-worktrees", "title": "Wtdev – deterministic dev-server ports for parallel Git worktrees", "summary": "Wtdev, a deterministic dev-server port manager for parallel Git worktrees, has been released as a single POSIX shell script. It assigns stable ports based on worktree paths to prevent collisions and supports automation hooks for coding agents like Claude Code. The tool also copies gitignored env files and provides a live dashboard of all checkouts.", "body_md": "One dev server per git worktree. No port collisions, stable URLs, and a live dashboard of every checkout.\n\nIf you run coding agents (Claude Code, Cursor, etc.) in parallel git worktrees, you've hit this: every worktree's dev server wants port 3000, agents kill each other's servers or drift to random ports, and you can never remember which branch is on which port.\n\n`wtdev`\n\nfixes it with one rule: **the port is a pure function of the worktree's path.**\n\n- main checkout →\n`3000`\n\n- every linked worktree → a stable port in\n`3001–3999`\n\n, hashed from its path\n\nSame worktree, same port, every time — across restarts, across agents, with zero coordination and zero state. In the rare case two worktrees hash to the same port, the later one (in `git worktree list`\n\norder) simply takes the next free port, so ports stay unique without anything to configure.\n\n```\ncurl -fsSL https://raw.githubusercontent.com/Dave-56/wtdev/main/wtdev -o /usr/local/bin/wtdev\nchmod +x /usr/local/bin/wtdev\n```\n\nIt's a single dependency-free POSIX shell script; you can also just copy it into your repo.\n\n```\nwtdev run                 # start the dev server on this checkout's port\nwtdev run pnpm dev        # ...with an explicit command ({port} is substituted)\nwtdev up                  # start it in the background if not already serving\nwtdev port                # print this checkout's port\nwtdev list                # every worktree with its branch + port\nwtdev dashboard           # generate a live-status HTML dashboard\n```\n\nThe easiest setup is to route your dev script through it, so agents and humans alike get the right port without thinking:\n\n```\n{ \"scripts\": { \"dev\": \"wtdev run next dev -p {port}\" } }\n```\n\n`wtdev run`\n\nexports `PORT`\n\n(which Next.js, Vite via config, CRA, Remix, and most Node servers respect) and also substitutes `{port}`\n\ninto the command for tools that want a flag.\n\nIt also copies gitignored env files (`.env`\n\n, `.env.local`\n\nby default) from the main checkout into fresh worktrees, since those never come along with `git worktree add`\n\n.\n\n`wtdev up`\n\nis `run`\n\nfor automation: it starts the server in the background (logging to `.wtdev.log`\n\n— add it to `.gitignore`\n\n) only if the checkout's port isn't already serving, and prints the URL either way. Because it's idempotent, any number of sessions can call it without double-starting servers.\n\nWire it into your agent's session-start hook and every session begins with its checkout's dev URL already in context. For Claude Code, put this in `.claude/settings.json`\n\nat the repo root:\n\n```\n{\n  \"hooks\": {\n    \"SessionStart\": [\n      {\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"[ -d node_modules ] || npm install >/dev/null 2>&1; wtdev up 2>/dev/null || true\",\n            \"timeout\": 180,\n            \"statusMessage\": \"Starting dev server for this checkout\"\n          }\n        ]\n      }\n    ],\n    \"PostToolUse\": [\n      {\n        \"matcher\": \"EnterWorktree\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"[ -d node_modules ] || npm install >/dev/null 2>&1; wtdev up 2>/dev/null || true\",\n            \"timeout\": 180,\n            \"statusMessage\": \"Starting dev server for this worktree\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nBoth hooks matter. `SessionStart`\n\ncovers the checkout a session launches in — but a worktree created *mid-session* (Claude Code's `EnterWorktree`\n\n) never re-fires it, so without the `PostToolUse`\n\nhook those worktrees silently run without a server until someone notices. The hook runs after the session has switched into the new worktree, so `wtdev up`\n\npicks up that worktree's port.\n\nThe `npm install`\n\nguard makes fresh worktrees self-sufficient — `git worktree add`\n\ndoesn't bring node_modules along. If you vendor the script instead of installing it globally, use `sh scripts/wtdev up`\n\n.\n\n```\nwtdev dashboard   # manual regenerate; run/up also regenerate it automatically\n```\n\nEvery `wtdev run`\n\n/ `wtdev up`\n\nrewrites the dashboard (after `git worktree prune`\n\n), so deleted worktrees drop off the page as soon as any server starts — no manual upkeep. The page shows live servers by default; idle checkouts collapse behind a \"show N idle checkouts\" toggle.\n\nWrites `worktrees.html`\n\n(into `public/`\n\nif you have one, so your dev server serves it) listing every worktree's branch, last commit, and URL, with a green/red live-status dot probed from the page every 5 seconds. Re-run it when you add or remove worktrees.\n\nIf [localias](https://github.com/peterldowns/localias) is installed and running (`brew install peterldowns/localias/localias && localias start`\n\n), `wtdev run`\n\nalso registers `http://<worktree-name>.localhost`\n\nfor each worktree. `my-feature.localhost`\n\nbeats `localhost:3417`\n\n.\n\nAliases don't leak: when a worktree is deleted, its alias is pruned automatically on the next server start. An alias is only removed if it's inside wtdev's port range, matches no current worktree, and nothing is serving on its port — so live aliases (including other repos') are never touched.\n\n| Env var | Default | |\n|---|---|---|\n`PORT` |\n— | overrides everything |\n`WTDEV_BASE_PORT` |\n`3000` |\nport for the main checkout |\n`WTDEV_PORT_RANGE` |\n`999` |\nworktree ports span `base+1 … base+range` |\n`WTDEV_CMD` |\n`npm run dev` |\ndev command template for `wtdev run` |\n`WTDEV_ENV_FILES` |\n`.env .env.local` |\nenv files borrowed from the main checkout |\n\nThere's no daemon, no lockfile, no registry. The worktree path is hashed with `cksum`\n\nand mapped into the port range. Worktrees are discovered with `git worktree list`\n\n, so any layout works (including Claude Code's `.claude/worktrees/`\n\n). Collisions are possible in principle (999 slots), astronomically unlikely with a handful of worktrees — and if you ever hit one, `PORT`\n\nwins.\n\nMIT", "url": "https://wpnews.pro/news/wtdev-deterministic-dev-server-ports-for-parallel-git-worktrees", "canonical_source": "https://github.com/Dave-56/wtdev", "published_at": "2026-07-14 21:42:59+00:00", "updated_at": "2026-07-14 22:18:36.636557+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Wtdev", "Claude Code", "Cursor", "Next.js", "Vite", "CRA", "Remix", "Node"], "alternates": {"html": "https://wpnews.pro/news/wtdev-deterministic-dev-server-ports-for-parallel-git-worktrees", "markdown": "https://wpnews.pro/news/wtdev-deterministic-dev-server-ports-for-parallel-git-worktrees.md", "text": "https://wpnews.pro/news/wtdev-deterministic-dev-server-ports-for-parallel-git-worktrees.txt", "jsonld": "https://wpnews.pro/news/wtdev-deterministic-dev-server-ports-for-parallel-git-worktrees.jsonld"}}