Skip to content

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.

  • 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 the isolation: "worktree" agent flag.
  • Review a PR locally without stashing or disturbing your current branch.

The Claude CLI wrapper creates worktrees under .claude/worktrees/:

Terminal window
claude --worktree feature

That 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:

Terminal window
git worktree add .claude/worktrees/feature -b feature

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.*.local

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

After a worktree is created, one more step wires it up for parallel dev:

Terminal window
cd .claude/worktrees/feature
./src/scripts/setup-worktree.sh

The script:

  1. 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).

  2. Finds the first free TCP port at or above 5005 (API) and 5173 (web) using a /dev/tcp probe.

  3. Runs npm install in src/services/web/ (node_modules isn’t copied by .worktreeinclude — only config).

  4. Patches the API_ENDPOINT= line of src/services/web/.env.dev.local to point at the chosen API port. All other keys in that file are preserved.

  5. Prints the two commands to run in separate terminals — it does not auto-launch the servers:

    Terminal window
    # API
    dotnet run --project src/services/api/app/app.api --urls http://localhost:5006
    # Web
    (cd src/services/web && npm run dev -- --port 5174)

    --urls is passed as an app argument (not as ASPNETCORE_URLS in the environment) because dotnet run loads launchSettings.json which sets the URL inside the process — an external ASPNETCORE_URLS env 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.

StackAPIWeb
Main checkout50055173
First parallel worktree50065174
Second50075175
(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.

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.

When you’re done:

Terminal window
git worktree remove .claude/worktrees/feature

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