Skip to content

Instantly share code, notes, and snippets.

@Cretezy
Created March 10, 2026 03:20
Show Gist options
  • Select an option

  • Save Cretezy/7744ce223fdfdd50ba8720e37f9a9dee to your computer and use it in GitHub Desktop.

Select an option

Save Cretezy/7744ce223fdfdd50ba8720e37f9a9dee to your computer and use it in GitHub Desktop.

Auto-copy files when creating Git worktrees or Jujutsu workspaces

The problem

Worktrees and workspaces are one of the best ways to work across multiple branches simultaneously — and they're especially powerful when pairing with AI coding tools like Claude Code or Cursor, where you might spin up a fresh workspace per task or conversation to keep things isolated and clean.

The friction point: ignored files don't carry over. Your .env, .env.local, API keys, local config — none of it comes with you. Every new worktree means manually hunting down and copying those files before you can actually get to work.

The solution: .worktree-copy

Add a .worktree-copy file to the root of your repo listing the paths you want copied every time a new worktree or workspace is created:

# Secrets & environment
.env
.env.local

# Local tooling config
.secrets
.tool-versions.local

Lines starting with # are treated as comments. Blank lines are ignored. The file itself should be committed — it's project config, not a secret.


Setup

Git — automatic via hook

This approach runs transparently every time git worktree add is used, with no change to your normal workflow.

Create .git/hooks/post-checkout with the following content, then make it executable with chmod +x .git/hooks/post-checkout.

Note: Git hooks aren't shared via the repo, so each contributor needs to add this themselves — or you can use a hook manager like Lefthook or Husky to distribute them.

#!/bin/bash
MAIN_WORKTREE=$(git worktree list | head -1 | awk '{print $1}')
COPY_LIST="$MAIN_WORKTREE/.worktree-copy"

[[ ! -f "$COPY_LIST" ]] && exit 0

while IFS= read -r f || [[ -n "$f" ]]; do
  [[ -z "$f" || "$f" == \#* ]] && continue
  if [[ -e "$MAIN_WORKTREE/$f" && ! -e "$f" ]]; then
    cp -r "$MAIN_WORKTREE/$f" "$f"
    echo "Copied $f"
  fi
done < "$COPY_LIST"

Git — shell wrapper command

If you prefer an explicit command (or want something more portable across machines without setting up hooks), add a gwt function to your .bashrc or .zshrc:

gwt() {
  git worktree add "$@"
  local dest="${@: -1}"
  local main=$(git worktree list | head -1 | awk '{print $1}')
  local copy_list="$main/.worktree-copy"

  [[ ! -f "$copy_list" ]] && return 0

  while IFS= read -r f || [[ -n "$f" ]]; do
    [[ -z "$f" || "$f" == \#* ]] && continue
    [[ -e "$main/$f" ]] && cp -r "$main/$f" "$dest/$f" && echo "Copied $f"
  done < "$copy_list"
}

Usage is identical to git worktree add — just use gwt instead:

gwt ../my-project-feature feature-branch
Fish shell version

Add to ~/.config/fish/functions/gwt.fish:

function gwt
  git worktree add $argv
  set dest $argv[-1]
  set main (git worktree list | head -1 | awk '{print $1}')
  set copy_list "$main/.worktree-copy"

  if not test -f $copy_list
    return 0
  end

  while read -l f
    string match -qr '^\s*$|^#' -- $f; and continue
    if test -e "$main/$f"
      cp -r "$main/$f" "$dest/$f"
      echo "Copied $f"
    end
  end < $copy_list
end

Jujutsu — shell wrapper command

Jujutsu workspaces share the same underlying store, so there's no hook equivalent. Add a jjws function to your .bashrc or .zshrc:

jjws() {
  jj workspace add "$@"
  local dest="${@: -1}"
  local main=$(jj root)
  local copy_list="$main/.worktree-copy"

  [[ ! -f "$copy_list" ]] && return 0

  while IFS= read -r f || [[ -n "$f" ]]; do
    [[ -z "$f" || "$f" == \#* ]] && continue
    [[ -e "$main/$f" ]] && cp -r "$main/$f" "$dest/$f" && echo "Copied $f"
  done < "$copy_list"
}

Usage mirrors jj workspace add:

jjws ../my-project-feature
Fish shell version

Add to ~/.config/fish/functions/jjws.fish:

function jjws
  jj workspace add $argv
  set dest $argv[-1]
  set main (jj root)
  set copy_list "$main/.worktree-copy"

  if not test -f $copy_list
    return 0
  end

  while read -l f
    string match -qr '^\s*$|^#' -- $f; and continue
    if test -e "$main/$f"
      cp -r "$main/$f" "$dest/$f"
      echo "Copied $f"
    end
  end < $copy_list
end

Tips

Symlink instead of copy — if the file should stay in sync across all worktrees (e.g. a shared .env with no per-branch variation), symlink it instead of copying:

ln -s "$main/$f" "$dest/$f"

Just swap the cp for ln -s in whichever script you're using.

Don't copy if the file already exists — all examples above skip copying if the destination file is already present, so re-running is safe and won't clobber local overrides.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment