Parallel worktrees
Run multiple branches in parallel via `git worktree add`. What env files to mirror, the cookie-jar caveat, and the future per-worktree hostname plan.
The repo expects you to use git worktrees for parallel branches. Each
worktree is a complete checkout pointing at a different branch, with shared
.git/ storage. The .worktrees/ directory at the repo root is gitignored —
that's the conventional location.
Creating a worktree
git fetch origin dev
git worktree add -b pr-x/topic .worktrees/pr-x-topic origin/dev
cd .worktrees/pr-x-topicYou now have a complete checkout at .worktrees/pr-x-topic/ on a new branch
pr-x/topic tracking origin/dev. Run pnpm install once and the worktree
is ready (pnpm shares the global store, so it's fast).
Env files to mirror
Fresh worktree checkouts have an empty working tree for every gitignored
file. The next-app per-package .env*.local files plus the root .env.cloud
are gitignored but required for the dev server and cloud-credential scripts
to work. Mirror them from your main checkout the first time:
SRC="/path/to/your/main/checkout"
for f in \
.env.cloud \
apps/web/.env.development.local \
apps/admin/.env.development.local \
apps/onboarding/.env.local \
design-system/docs/.env.local; do
[ -f "$SRC/$f" ] && cp "$SRC/$f" "./$f" && echo "copied $f"
doneWhat each one is:
| File | Purpose |
|---|---|
.env.cloud | Claude Code cloud-session env vars (VERCEL_TOKEN, AWS, GH PAT, E2E secrets, Slack tokens, bypass token). Template: .env.cloud.example |
apps/web/.env.development.local | OIDC client_id + secret for web's dev profile. Without it, auth init fails |
apps/admin/.env.development.local | Same shape for admin |
apps/onboarding/.env.local | Vite-style overrides (VITE_API_URL, etc.) |
design-system/docs/.env.local | Docs-site overrides |
pnpm cloud:bootstrap regenerates the two Next apps' .env.development.local
via vercel pull if VERCEL_TOKEN is in .env.cloud — useful when the
upstream values change.
Do not run pnpm dev:local or pnpm dev:remote in a fresh worktree before
mirroring — they delete or overwrite .env.development.local and you'll
lose your OIDC creds.
Per-worktree dev ports — only apps/web is automatic today
scripts/dev-server.mjs scans 4060-4069 for the first free port. So if
worktree A is running apps/web's pnpm dev on :4060, worktree B's pnpm dev
lands on :4061 without configuration.
apps/admin, apps/onboarding, and apps/dev-docs do not use the scanner.
They are hardcoded:
apps/admin→next dev --turbopack -p 4061apps/onboarding→vite --port 5174(Vite usesstrictPort: true)apps/dev-docs→next dev --turbopack -p 4070
So two worktrees can run their apps/web side by side, but only one worktree
at a time can run any of the other three. Generalizing the scanner to admin
- dev-docs is on the follow-up list;
apps/onboarding's native-WebView behavior depends on a fixed port and is intentionally out of scope.
Cookie jar caveat — host-scoped, not port-scoped
Two worktrees on localhost:<different ports> share one cookie jar within
a single browser profile (RFC 6265 — port is not part of cookie scope).
Logging into one replaces the other's session in that profile.
Workarounds:
- Automation: Playwright
browser.newContext()andagent-browser --session <name>each isolate their own cookie jar. Verified by the two-worktree simultaneous-login E2E in PR #656. - Humans: use a separate Chrome profile per worktree when you need two worktrees logged in at the same time. Clunky but it's the only workaround at the port layer today.
Future direction — per-worktree hostnames
There's a planned (not yet built) scheme to give each worktree its own hostname instead of port. Schema:
<project>-<feature>.localhost:<port>Examples: today-web-localhost-bff.localhost:4060,
today-web-pr-ca-dev-docs.localhost:4061. *.localhost resolves to
loopback in Chrome and Firefox without /etc/hosts edits, so a distinct
hostname = distinct cookie jar = true same-profile parallel login.
The constraint to know: keep the name single-label (one dot before
.localhost). A multi-level form like today.dev-docs.localhost is
unreliable behind DNS-intercepting proxies (Surge, Proxyman) which can route
depth-greater-than-1 .localhost names to the real internet. The project
name is therefore folded into the single label with a dash.
Tracked in the team-lead memory feedback_dev_preview_url_scheme.
Cleanup
# From the main checkout
git worktree list # see all live worktrees
git worktree remove .worktrees/pr-x-topic # delete the checkout + branch metadata
git worktree prune # clean up stale worktree recordsDon't delete .worktrees/<name>/ manually with rm -rf — git keeps a
record of every worktree under .git/worktrees/ that gets stale.
Local state noise to ignore
git status in the main checkout sometimes shows:
D .claude/scheduled_tasks.lock— Claude Code session state, ephemeral?? .claude/worktrees/— Claude Code's own worktree registry
Both are local agent state. Don't stage them.