Skip to content

therohitdas/copy-env

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 

Repository files navigation

Git Worktree .env Auto-Copy Setup

Problem

When working with Git worktrees, environment files (.env, .env.local, etc.) are not automatically copied from the main repository to new worktrees. This causes issues because:

  • ❌ New worktrees can't run the project without .env files
  • ❌ You have to manually copy .env files every time you create a worktree
  • ❌ Easy to forget, leading to confusing errors when trying to test
  • ❌ Especially problematic with Claude Code which creates worktrees automatically
  • ❌ Monorepos and nested projects have .env files in subdirectories too

Solution

Set up a global Git hook that automatically copies all .env* files from the main repository to any new worktree when it's created. It searches recursively through the project, preserving directory structure, while skipping gitignored directories like node_modules/, dist/, vendor/, etc.

Setup Instructions

Step 1: Check for Existing Global Hooks Directory

git config --global core.hooksPath

If this returns a path:

  • You already have a global hooks directory configured
  • Use that path instead of ~/.git-hooks in the steps below
  • Skip to Step 3

If this returns nothing (or an error):

  • No global hooks configured yet
  • Proceed to Step 2

Step 2: Create Global Hooks Directory

# Create the directory
mkdir -p ~/.git-hooks

# Configure Git to use it globally
git config --global core.hooksPath ~/.git-hooks

# Verify it's set
git config --global core.hooksPath
# Should output: /Users/yourname/.git-hooks

Step 3: Create the Post-Checkout Hook

Run this command to create the hook file:

cat > ~/.git-hooks/post-checkout << 'EOF'
#!/bin/bash
# Global Git hook: post-checkout
# Automatically copy .env files to new worktrees (including nested directories)
# Skips gitignored directories (node_modules, vendor, dist, etc.)

# Only run for worktree checkouts (not regular branch switches in main repo)
# $3 = 1 means it's a branch checkout (not file checkout)
if [ "$3" != "1" ]; then
    exit 0
fi

# Get the common git directory (works for both main repo and worktrees)
GIT_COMMON_DIR=$(git rev-parse --git-common-dir)
MAIN_REPO=$(cd "$GIT_COMMON_DIR/.." && pwd)
CURRENT_DIR=$(pwd)

# If we're in a worktree (not the main repo), copy .env files
if [ "$CURRENT_DIR" != "$MAIN_REPO" ]; then
    echo "πŸ“‹ Worktree detected: $CURRENT_DIR"

    # Build a list of gitignored directories to prune from traversal
    # This prevents find from even entering node_modules/, dist/, etc.
    prune_args=(-name ".git" -prune)
    while IFS= read -r dir; do
        [ -z "$dir" ] && continue
        prune_args+=(-o -path "$MAIN_REPO/$dir" -prune)
    done < <(
        # List all directories, check which ones are gitignored
        find "$MAIN_REPO" -mindepth 1 -maxdepth 3 -type d -not -path "*/.git/*" -not -name ".git" | while read -r d; do
            reldir="${d#$MAIN_REPO/}"
            if git -C "$MAIN_REPO" check-ignore -q "$reldir" 2>/dev/null; then
                echo "$reldir"
            fi
        done
    )

    # Find .env* files, skipping pruned (gitignored) directories
    find "$MAIN_REPO" \( "${prune_args[@]}" \) -o -name ".env*" -type f -print | while read -r envfile; do
        relpath="${envfile#$MAIN_REPO/}"

        # Copy preserving directory structure
        target="$CURRENT_DIR/$relpath"
        if [ ! -f "$target" ]; then
            mkdir -p "$(dirname "$target")"
            cp "$envfile" "$target"
            echo "  βœ“ Copied $relpath"
        else
            echo "  ⊘ Skipped $relpath (already exists)"
        fi
    done

    echo "βœ… Environment files synced!"
fi
EOF

Step 4: Make the Hook Executable

chmod +x ~/.git-hooks/post-checkout

Step 5: Verify Installation

# Check the hook exists and is executable
ls -la ~/.git-hooks/post-checkout

# Should show something like:
# -rwxr-xr-x  1 yourname  staff  XXX Jan 14 XX:XX /Users/yourname/.git-hooks/post-checkout

Testing

Create a test worktree to verify it works:

# Navigate to any repo with .env files
cd /path/to/your/repo

# Create a test worktree
git worktree add ~/.test-worktree -b test-branch

# You should see output like:
# Preparing worktree (new branch 'test-branch')
# HEAD is now at XXXXXXX <commit message>
# πŸ“‹ Worktree detected: /Users/yourname/.test-worktree
#   βœ“ Copied .env
#   βœ“ Copied .env.local
#   βœ“ Copied packages/api/.env
# βœ… Environment files synced!

# Verify files were copied
find ~/.test-worktree -name ".env*"

# Clean up
git worktree remove ~/.test-worktree
git branch -D test-branch

How It Works

  1. Trigger: When you run git worktree add, Git executes the post-checkout hook
  2. Prune: The hook identifies gitignored directories (like node_modules/) using git check-ignore and excludes them from traversal
  3. Search: It recursively finds all .env* files in non-ignored directories
  4. Copy: Copies them to the new worktree, preserving the directory structure (skips if they already exist)

What Gets Copied

All files matching .env* pattern, at any depth in the project:

  • .env
  • .env.local
  • .env.development
  • .env.production
  • .env.test
  • .env.example
  • packages/api/.env
  • apps/web/.env.local
  • etc.

What Gets Skipped

  • Files inside gitignored directories (node_modules/, dist/, vendor/, .venv/, etc.)
  • Files that already exist in the worktree

Safety Features

  • βœ… Won't overwrite: If a file already exists in the worktree, it's skipped
  • βœ… Only worktrees: Won't affect your main repository
  • βœ… Only on creation: Only runs when checking out a new worktree, not on branch switches
  • βœ… Global: Works for all repositories on your machine
  • βœ… Respects .gitignore: Won't traverse into ignored directories like node_modules/
  • βœ… Preserves structure: Nested .env files keep their relative paths

Benefits

βœ… Zero manual copying: Environment files are automatically synced to new worktrees

βœ… Works with monorepos: Recursively copies .env files from nested packages and apps

βœ… Works with Claude Code: Claude creates worktrees automatically, and this hook ensures they have the necessary .env files

βœ… Works with manual worktrees: Also works when you create worktrees manually with git worktree add

βœ… Set once, use everywhere: Global hook applies to all repositories on your machine

βœ… No maintenance: Set it up once and it works forever

Troubleshooting

Hook doesn't run when creating worktree

Check if global hooks path is set:

git config --global core.hooksPath

If not set, configure it:

git config --global core.hooksPath ~/.git-hooks

Hook runs but files aren't copied

Check if hook is executable:

ls -la ~/.git-hooks/post-checkout
# Should show -rwxr-xr-x (note the 'x' for executable)

If not executable, fix it:

chmod +x ~/.git-hooks/post-checkout

Files still not copied

Verify .env files exist in main repo:

cd /path/to/main/repo
find . -name ".env*" -not -path "./.git/*"

Test hook manually in a worktree:

cd /path/to/worktree
bash -x ~/.git-hooks/post-checkout 0 0 1

This will show debug output to help identify the issue.

Important Technical Detail

Why --git-common-dir instead of --git-dir?

In worktrees, git rev-parse --git-dir returns .git/worktrees/<worktree-name>, which is the worktree-specific git directory. If we use cd "$GIT_DIR/..", we end up in .git/worktrees/ instead of the main repo root.

Using --git-common-dir always points to the main .git directory, so cd "$GIT_COMMON_DIR/.." correctly resolves to the main repository root where .env files are stored.

Incorrect approach (buggy):

GIT_DIR=$(git rev-parse --git-dir)
MAIN_REPO=$(cd "$GIT_DIR/.." && pwd)
# Results in: /path/to/repo/.git/worktrees (WRONG!)

Correct approach:

GIT_COMMON_DIR=$(git rev-parse --git-common-dir)
MAIN_REPO=$(cd "$GIT_COMMON_DIR/.." && pwd)
# Results in: /path/to/repo (CORRECT!)

Why prune gitignored directories?

Instead of finding all .env* files and then filtering, the hook prunes gitignored directories upfront so find never enters them. This is both faster (no traversing node_modules/ with thousands of files) and correct (no .env files from dependencies get copied).

For Team Members

After setting this up, you can:

  • Create worktrees with git worktree add and environment files will be automatically copied
  • Use Claude Code without worrying about missing .env files
  • Test features in isolated worktrees without manual setup

One-Time Setup Script

If you prefer, you can run this all-in-one script:

# Create global hooks directory if it doesn't exist
mkdir -p ~/.git-hooks

# Create the post-checkout hook
cat > ~/.git-hooks/post-checkout << 'HOOKEOF'
#!/bin/bash
if [ "$3" != "1" ]; then exit 0; fi
GIT_COMMON_DIR=$(git rev-parse --git-common-dir)
MAIN_REPO=$(cd "$GIT_COMMON_DIR/.." && pwd)
CURRENT_DIR=$(pwd)
if [ "$CURRENT_DIR" != "$MAIN_REPO" ]; then
    echo "πŸ“‹ Worktree detected: $CURRENT_DIR"
    prune_args=(-name ".git" -prune)
    while IFS= read -r dir; do
        [ -z "$dir" ] && continue
        prune_args+=(-o -path "$MAIN_REPO/$dir" -prune)
    done < <(
        find "$MAIN_REPO" -mindepth 1 -maxdepth 3 -type d -not -path "*/.git/*" -not -name ".git" | while read -r d; do
            reldir="${d#$MAIN_REPO/}"
            if git -C "$MAIN_REPO" check-ignore -q "$reldir" 2>/dev/null; then
                echo "$reldir"
            fi
        done
    )
    find "$MAIN_REPO" \( "${prune_args[@]}" \) -o -name ".env*" -type f -print | while read -r envfile; do
        relpath="${envfile#$MAIN_REPO/}"
        target="$CURRENT_DIR/$relpath"
        if [ ! -f "$target" ]; then
            mkdir -p "$(dirname "$target")"
            cp "$envfile" "$target"
            echo "  βœ“ Copied $relpath"
        else
            echo "  ⊘ Skipped $relpath (already exists)"
        fi
    done
    echo "βœ… Environment files synced!"
fi
HOOKEOF

# Make it executable
chmod +x ~/.git-hooks/post-checkout

# Configure Git to use global hooks
git config --global core.hooksPath ~/.git-hooks

echo "βœ… Global Git worktree .env auto-copy hook installed!"

Questions?

If you run into issues, check:

  1. Global hooks path is configured: git config --global core.hooksPath
  2. Hook file exists: ls ~/.git-hooks/post-checkout
  3. Hook is executable: ls -la ~/.git-hooks/post-checkout
  4. .env files exist in main repo: find . -name ".env*" -not -path "./.git/*"

About

Copy local .env files to created worktrees. Works with all coding agents.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages