If you're using Claude Code's worktree feature for parallel development, you've probably run into the same annoyances I did. Every time a new worktree spins up, you have to:
.env filesnpm install or pnpm install (and wait)It gets old fast, especially when you're spinning up worktrees frequently. Here's how I automated all of it with a simple setup that runs automatically when Claude Code enters a worktree.
Claude Code's worktree feature creates a fresh copy of your repo on a separate git branch. That's great for isolation — but it also means your new worktree is missing everything that's in .gitignore:
.env files — your API keys, database URLs, and service configsnode_modules/ — your entire dependency treepnpm dev in both the main repo and the worktree, they'll fight over the same portYou end up doing the same manual setup every single time. Let's fix that.
We're going to use three things:
.worktreeinclude file that lists which files to symlink from the main reposettings.json to run the script automatically on SessionStart and symlink node_modulesCreate a .worktreeinclude file in your project root. List every file you want available in worktrees — one per line. These are typically your .env files:
# Environment files
apps/web/.env
apps/api/.env
.env
Add whatever your project needs. Comments (lines starting with #) and blank lines are ignored.
Create scripts/worktree/setup.sh:
#!/bin/bash
# Symlinks files from main repo and generates a port offset.
# Called automatically by Claude Code's SessionStart hook.
set -e
MAIN_REPO=$(git worktree list --porcelain | head -1 | cut -d' ' -f2)
WORKTREE_DIR=$(git rev-parse --show-toplevel)
WORKTREES_DIR="$MAIN_REPO/.claude/worktrees"
# Skip if already set up (idempotent)
if [ -f "$WORKTREE_DIR/.worktree-port-offset" ]; then
exit 0
fi
echo "Setting up worktree: $(basename "$WORKTREE_DIR")" >&2
# 1. Symlink files listed in .worktreeinclude
if [ -f "$MAIN_REPO/.worktreeinclude" ]; then
while IFS= read -r file || [ -n "$file" ]; do
[[ -z "$file" || "$file" == \#* ]] && continue
if [ -f "$MAIN_REPO/$file" ]; then
mkdir -p "$WORKTREE_DIR/$(dirname "$file")"
ln -sf "$MAIN_REPO/$file" "$WORKTREE_DIR/$file"
echo " Linked $file" >&2
fi
done < "$MAIN_REPO/.worktreeinclude"
fi
# 2. Generate a unique port offset so dev servers don't collide
USED_OFFSETS=""
for f in "$WORKTREES_DIR"/*/.worktree-port-offset; do
[ -f "$f" ] && USED_OFFSETS="$USED_OFFSETS $(cat "$f")"
done
PORT_OFFSET=10
while echo "$USED_OFFSETS" | grep -qw "$PORT_OFFSET"; do
PORT_OFFSET=$(( PORT_OFFSET + 10 ))
done
echo "$PORT_OFFSET" > "$WORKTREE_DIR/.worktree-port-offset"
echo " Port offset: +$PORT_OFFSET" >&2
Make it executable:
chmod +x scripts/worktree/setup.sh
Here's what the script does:
git worktree list.worktreeinclude — the worktree gets a link to the original, so changes to .env in your main repo are reflected everywhere.worktree-port-offset.worktree-port-offset already exists, it exits immediatelyThis is where the magic happens. In your .claude/settings.json, add:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "MAIN_REPO=$(git worktree list --porcelain | head -1 | cut -d' ' -f2) && WORKTREE_DIR=$(git rev-parse --show-toplevel) && [ \"$MAIN_REPO\" != \"$WORKTREE_DIR\" ] && bash \"$MAIN_REPO/scripts/worktree/setup.sh\" || true",
"timeout": 30
}
]
}
]
},
"worktree": {
"symlinkDirectories": ["node_modules"]
}
}
Two things happening here:
The SessionStart hook fires every time Claude Code starts a session. The command checks if we're inside a worktree (by comparing the main repo path with the current directory). If we are, it runs our setup script. The || true at the end prevents errors if we're in the main repo.
The worktree.symlinkDirectories setting tells Claude Code to symlink node_modules instead of copying it. This is huge — no more waiting for npm install in every worktree. The worktree just points to the same node_modules from your main repo.
The setup script writes a number (10, 20, 30, etc.) to .worktree-port-offset. You need to read it in your dev server config.
For a Nuxt project, create a small utility:
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
export function getPortOffset(configDir: string): number {
try {
return parseInt(
readFileSync(
resolve(configDir, '../../.worktree-port-offset'),
'utf-8'
).trim()
);
} catch {
return 0; // Main repo — no offset
}
}
Then in your nuxt.config.ts:
import { getPortOffset } from './scripts/worktree/port-offset';
export default defineNuxtConfig({
devServer: {
port: 3000 + getPortOffset(import.meta.dirname),
},
// ... rest of your config
});
For Next.js, you can do the same thing in your next.config.js or just read the file in a dev script in package.json:
{
"scripts": {
"dev": "OFFSET=$(cat .worktree-port-offset 2>/dev/null || echo 0) && next dev -p $((3000 + OFFSET))"
}
}
For Vite projects:
import { getPortOffset } from './scripts/worktree/port-offset';
export default defineConfig({
server: {
port: 5173 + getPortOffset(__dirname),
},
});
Add the generated port offset file to .gitignore:
.worktree-port-offset
Here's the flow when Claude Code creates a worktree:
node_modules is automatically symlinked (from worktree.symlinkDirectories)SessionStart hook fires and detects we're in a worktreesetup.sh runs — symlinks all .env files and generates a port offsetpnpm dev, the dev server starts on a unique port (e.g., 3010 instead of 3000)No manual steps. No port collisions. No missing environment variables. No waiting for npm install.
If your project has multiple apps (say, a frontend, an API, and an admin panel), you can assign each a base port and they all shift by the same offset:
| App | Main Repo | Worktree (+10) | Worktree (+20) |
|---|---|---|---|
| Frontend | 3000 | 3010 | 3020 |
| API | 3001 | 3011 | 3021 |
| Admin | 3002 | 3012 | 3022 |
Just use the same getPortOffset() function in each app's config with a different base port.
The full setup is just four files:
.worktreeinclude — list of files to symlinkscripts/worktree/setup.sh — symlink + port offset scriptscripts/worktree/port-offset.ts — utility to read the offset.claude/settings.json — hooks and worktree configOnce it's in place, every worktree Claude Code creates is ready to go immediately. You never have to think about env files, node_modules, or ports again.
Let me know if you run into any issues ✌️