Worktrees for Parallel Development
Formation supports running multiple branches in parallel via git worktrees. A worktree is a second working copy that shares .git with the main checkout but has its own branch and its own files on disk, so you can (for example) keep main running on its usual ports while iterating on a feature branch in a second browser tab.
Everything here is local-dev only — deployed environments are unaffected.
Table of Contents
Section titled “Table of Contents”- Why Worktrees
- Creating a Worktree
- What Gets Copied
- Running the Setup Script
- Port Conventions
- Shared Database
- Cleanup
Why Worktrees
Section titled “Why Worktrees”- Keep the main checkout on a known-good branch (and its dev servers running) while you hack on something risky elsewhere.
- Let Claude Code spawn an isolated worktree for a task so it can’t touch your uncommitted work — via
claude --worktree <name>or theisolation: "worktree"agent flag. - Review a PR locally without stashing or disturbing your current branch.
Creating a Worktree
Section titled “Creating a Worktree”The Claude CLI wrapper creates worktrees under .claude/worktrees/:
claude --worktree featureThat runs git worktree add .claude/worktrees/feature on a new branch and drops you into a fresh Claude session rooted there.
Under the hood it’s git worktree — you can also use git directly if you’re not starting a Claude session:
git worktree add .claude/worktrees/feature -b featureWhat Gets Copied
Section titled “What Gets Copied”Git itself only copies tracked files into a new worktree. Anything gitignored — including every file you need to actually run the services — is missing by default: .env.dev.local, appsettings.Development.json, .claude/settings.local.json, and so on.
To close that gap, /.worktreeinclude lists gitignored files to copy into new worktrees. It uses gitignore glob syntax:
.claude/settings.local.json**/appsettings.Development.json**/appsettings.Local.json**/.env**/.env.local**/.env.*.localClaude Code reads this file when it creates a worktree (via --worktree, desktop parallel sessions, or subagent isolation: "worktree"). Tracked files are never duplicated; only gitignored matches are copied.
If you add a new gitignored file that the services need to start, add its glob here so future worktrees don’t have to recreate it by hand.
Running the Setup Script
Section titled “Running the Setup Script”After a worktree is created, one more step wires it up for parallel dev:
cd .claude/worktrees/feature./src/scripts/setup-worktree.shThe script:
-
Verifies you’re in a worktree under
.claude/worktrees/— refuses to run in the main checkout (it would overwrite the main stack’s.env.dev.local). -
Finds the first free TCP port at or above 5005 (API) and 5173 (web) using a
/dev/tcpprobe. -
Runs
npm installinsrc/services/web/(node_modulesisn’t copied by.worktreeinclude— only config). -
Patches the
API_ENDPOINT=line ofsrc/services/web/.env.dev.localto point at the chosen API port. All other keys in that file are preserved. -
Prints the two commands to run in separate terminals — it does not auto-launch the servers:
Terminal window # APIdotnet run --project src/services/api/app/app.api --urls http://localhost:5006# Web(cd src/services/web && npm run dev -- --port 5174)--urlsis passed as an app argument (not asASPNETCORE_URLSin the environment) becausedotnet runloadslaunchSettings.jsonwhich sets the URL inside the process — an externalASPNETCORE_URLSenv var is overridden by the launch profile and silently falls back to 5005.
The script is idempotent — re-running it re-detects ports and rewrites API_ENDPOINT if the values need to change.
Port Conventions
Section titled “Port Conventions”| Stack | API | Web |
|---|---|---|
| Main checkout | 5005 | 5173 |
| First parallel worktree | 5006 | 5174 |
| Second | 5007 | 5175 |
| … | (first free) | (first free) |
launchSettings.json is tracked, so the API port override is passed via ASPNETCORE_URLS rather than edited in place. Vite’s --port flag does the same job for the web stack.
Only 5173 is pre-forwarded in .devcontainer/devcontainer.json. VS Code auto-forwards new ports on first bind (you’ll see a toast); if you want to silence it, add the ports to forwardPorts locally — but don’t commit them, they’re per-developer.
Shared Database
Section titled “Shared Database”Every worktree’s API talks to the same SQL Server instance (via the appsettings.Development.json that .worktreeinclude copies in). Reads are independent across stacks; writes are visible to everyone.
Practical consequences:
- Fine for UI-only work — each stack can pull the same data and you won’t trip over each other.
- Risky for destructive or schema-changing work — a DACPAC rebuild, a test that truncates tables, or a long-running migration in one worktree will affect every stack.
- Don’t run E2E tests in parallel across worktrees. They mutate shared state.
If you need true isolation, stand up a second local SQL instance and point that worktree’s appsettings.Development.json at it — but that’s a bigger exercise and rarely necessary.
Cleanup
Section titled “Cleanup”When you’re done:
git worktree remove .claude/worktrees/featureThis deletes the working directory and de-registers it from git. The branch stays — delete it separately with git branch -D feature if you don’t need it.
If you just delete the directory by hand, git keeps a stale registration. git worktree prune clears those out.