cd /news/developer-tools/auto-copy-files-when-creating-git-wo… · home topics developer-tools article
[ARTICLE · art-84043] src=cretezy.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Auto-copy files when creating Git worktrees or Jujutsu workspaces

A developer has published a technique to automatically copy ignored files like .env and API keys into new Git worktrees and Jujutsu workspaces, using a post-checkout hook or shell wrapper that leverages `git ls-files --others --ignored --exclude-standard --directory` to enumerate ignored files from the main worktree. The method eliminates manual copying and requires no extra config, with implementations for Git hooks, a `gwt` shell function for Bash/Zsh and Fish, and a Jujutsu wrapper for colocated repositories.

read5 min views1 publishedAug 3, 2026

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: your .gitignore #

is already the list

The files that don’t carry over are, by definition, the ones in your .gitignore

. So instead of maintaining a separate config file, ask Git directly which ignored files exist in the main worktree:

git ls-files --others --ignored --exclude-standard --directory

This lists every untracked file matched by your ignore rules (.gitignore

, .git/info/exclude

, global excludes). The --directory

flag collapses fully-ignored directories like node_modules/

into a single entry, so a cp -r

copies them in one go instead of file-by-file.

No extra config to commit, and the list stays correct as your .gitignore

evolves.

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

:

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

[[ "$(git rev-parse --show-toplevel)" == "$MAIN_WORKTREE" ]] && exit 0

git -C "$MAIN_WORKTREE" ls-files --others --ignored --exclude-standard --directory -z |
  while IFS= read -r -d '' f; do
    if [[ -e "$MAIN_WORKTREE/$f" && ! -e "$f" ]]; then
      mkdir -p "$(dirname "$f")"
      cp -r "$MAIN_WORKTREE/$f" "$f"
      echo "Copied $f"
    fi
  done

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.

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 "$@" || return
  local dest="$1"
  local main=$(git worktree list | head -1 | awk '{print $1}')

  git -C "$main" ls-files --others --ignored --exclude-standard --directory -z |
    while IFS= read -r -d '' f; do
      if [[ -e "$main/$f" && ! -e "$dest/$f" ]]; then
        mkdir -p "$dest/$(dirname "$f")"
        cp -r "$main/$f" "$dest/$f"
        echo "Copied $f"
      fi
    done
}

Usage is identical to git worktree add

, with one rule: the worktree path comes first (Git happily accepts options after it):

gwt ../my-project-feature feature-branch
gwt ../my-project-feature -b new-branch

For Fish shell, add to ~/.config/fish/functions/gwt.fish

:

View Fish code #

function gwt
  git worktree add $argv; or return
  set dest $argv[1]
  set main (git worktree list | head -1 | awk '{print $1}')

  git -C $main ls-files --others --ignored --exclude-standard --directory -z | while read -lz f
    if test -e "$main/$f"; and not test -e "$dest/$f"
      mkdir -p "$dest/"(dirname $f)
      cp -r "$main/$f" "$dest/$f"
      echo "Copied $f"
    end
  end
end

Jujutsu — shell wrapper command

Jujutsu workspaces share the same underlying store, so there’s no hook equivalent. Since Jujutsu reads .gitignore

files too, the same Git command enumerates the ignored files — this assumes a colocated repository (created with jj git init --colocate

or jj git clone --colocate

), which is the common setup.

Add a jjws

function to your .bashrc

or .zshrc

, and run it from your main workspace:

jjws() {
  jj workspace add "$@" || return
  local dest="$1"
  local main=$(jj root)

  git -C "$main" ls-files --others --ignored --exclude-standard --directory -z -- ':(exclude).jj' |
    while IFS= read -r -d '' f; do
      if [[ -e "$main/$f" && ! -e "$dest/$f" ]]; then
        mkdir -p "$dest/$(dirname "$f")"
        cp -r "$main/$f" "$dest/$f"
        echo "Copied $f"
      fi
    done
}

The ':(exclude).jj'

pathspec matters: in a colocated repo, Git sees Jujutsu’s internal .jj/

directory as ignored, and copying it into the new workspace would corrupt it.

Usage mirrors jj workspace add

, with the destination path first:

jjws ../my-project-feature

For Fish shell, add to ~/.config/fish/functions/jjws.fish

:

View Fish code #

function jjws
  jj workspace add $argv; or return
  set dest $argv[1]
  set main (jj root)

  git -C $main ls-files --others --ignored --exclude-standard --directory -z -- ':(exclude).jj' | while read -lz f
    if test -e "$main/$f"; and not test -e "$dest/$f"
      mkdir -p "$dest/"(dirname $f)
      cp -r "$main/$f" "$dest/$f"
      echo "Copied $f"
    end
  end
end

Tips #

Install with AI — Ask your AI agent to install this post to your repository or to your shell.

Skip the heavy stuff — your ignore list probably includes things you’d rather rebuild than copy, like node_modules/

or build output. Filter them out of the pipeline:

git -C "$main" ls-files --others --ignored --exclude-standard --directory -z |
  grep -zEv '^(node_modules|dist|target)/'

(Or leave node_modules/

in — copying it is often faster than reinstalling.)

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.

Who am I?

Hello! My name is Cretezy and I am a software developer. I write about programming, personal projects, and more.

See more posts here.

── more in #developer-tools 4 stories · sorted by recency
── more on @git 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/auto-copy-files-when…] indexed:0 read:5min 2026-08-03 ·