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. Auto-copy files when creating Git worktrees or Jujutsu workspaces 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 : bash /bin/bash MAIN WORKTREE=$ git worktree list | head -1 | awk '{print $1}' Only act in linked worktrees, not on regular checkouts in the main one "$ 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 https://jj-vcs.github.io/jj/latest/git-compatibility/ co-located-jujutsugit-repos 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. /