All Articles
Automating Claude Code Worktree Setup — ENV Files, Node Modules, and Ports
#Web Development

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:

  1. Manually copy all your .env files
  2. Run npm install or pnpm install (and wait)
  3. Figure out which ports are free if you want to run dev servers alongside your main repo

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.

The Problem

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 configs
  • node_modules/ — your entire dependency tree
  • Port assignments — if you run pnpm dev in both the main repo and the worktree, they'll fight over the same port

You end up doing the same manual setup every single time. Let's fix that.

The Solution

We're going to use three things:

  1. A .worktreeinclude file that lists which files to symlink from the main repo
  2. A setup script that symlinks those files and generates unique port offsets
  3. Claude Code's settings.json to run the script automatically on SessionStart and symlink node_modules

Create a .worktreeinclude file in your project root. List every file you want available in worktrees — one per line. These are typically your .env files:

.worktreeinclude
# Environment files
apps/web/.env
apps/api/.env
.env

Add whatever your project needs. Comments (lines starting with #) and blank lines are ignored.

Step 2: The Setup Script

Create scripts/worktree/setup.sh:

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:

  • Finds the main repo using git worktree list
  • Symlinks each file from .worktreeinclude — the worktree gets a link to the original, so changes to .env in your main repo are reflected everywhere
  • Generates a port offset (10, 20, 30, ...) by checking what offsets are already in use by other worktrees. Writes it to .worktree-port-offset
  • Is idempotent — if .worktree-port-offset already exists, it exits immediately

Step 3: Wire It Into Claude Code

This is where the magic happens. In your .claude/settings.json, add:

.claude/settings.json
{
  "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.

Step 4: Use the Port Offset in Your Dev Server

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:

scripts/worktree/port-offset.ts
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:

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:

vite.config.ts
import { getPortOffset } from './scripts/worktree/port-offset';

export default defineConfig({
  server: {
    port: 5173 + getPortOffset(__dirname),
  },
});

Step 5: Update .gitignore

Add the generated port offset file to .gitignore:

.gitignore
.worktree-port-offset

How It All Works Together

Here's the flow when Claude Code creates a worktree:

  1. Claude Code creates a new worktree directory with a fresh branch
  2. node_modules is automatically symlinked (from worktree.symlinkDirectories)
  3. The SessionStart hook fires and detects we're in a worktree
  4. setup.sh runs — symlinks all .env files and generates a port offset
  5. When you run pnpm 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.

Multiple Apps in a Monorepo

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:

AppMain RepoWorktree (+10)Worktree (+20)
Frontend300030103020
API300130113021
Admin300230123022

Just use the same getPortOffset() function in each app's config with a different base port.

Summary

The full setup is just four files:

  • .worktreeinclude — list of files to symlink
  • scripts/worktree/setup.sh — symlink + port offset script
  • scripts/worktree/port-offset.ts — utility to read the offset
  • .claude/settings.json — hooks and worktree config

Once 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 ✌️

2019-2026 Baljeet Singh. All rights reserved.