Auto-copy .env* files to new git worktrees. A one-script global git hook that runs every time you (or your AI coding agent) creates a worktree.
$ git worktree add ../wt-feature-x -b feature-x
Preparing worktree (new branch 'feature-x')
HEAD is now at 1302744 chore: update readme
π Worktree detected: /Users/you/repo/wt-feature-x
β Copied .env
β Copied apps/api/.env.local
β Skipped .env.example (already exists)
β
Environment files synced!
git worktree add doesn't copy untracked files. .env* files are untracked by design β they hold secrets β so every new worktree is dead on arrival until you manually copy them over. This is annoying when you do it by hand. It becomes a real bottleneck when AI coding agents spin up worktrees on demand: Claude Code, Codex, Cursor's agent mode, Aider, and others routinely create worktrees, and every one of them lands without env files.
This repo is a single global post-checkout hook that fixes it. Set it up once. Works for every repo on your machine. Works no matter who creates the worktree β you, an agent, an IDE, a CI script.
On every git worktree add:
- Copies every
.env*file from the main repo into the new worktree, preserving directory structure (soapps/api/.env.locallands atapps/api/.env.localin the worktree, not at the root). - Respects
.gitignoreβ skips dependency directories likenode_modules/,dist/,.venv/so you never copy.envfiles belonging to third-party packages. - Never overwrites an existing file in the worktree. Re-runs are safe.
- Chains to per-repo hooks β if a repo has its own
.git/hooks/post-checkout(e.g. git-lfs's hook), it still runs. We don't break LFS, husky, or anything else.
git clone https://github.com/DaniAkash/worktree-env-copy.git
cd worktree-env-copy
./install.shThat's it. The installer is idempotent β re-running won't hurt anything.
Pick any repo that has a .env file:
cd ~/path/to/some-repo
git worktree add /tmp/wt-test -b chore/test-env-copyYou should see:
π Worktree detected: /tmp/wt-test
β Copied .env
β
Environment files synced!
Cleanup:
git worktree remove /tmp/wt-test
git branch -D chore/test-env-copyModern coding agents (Claude Code, Codex, Cursor agent mode, Aider, etc.) often work in isolated git worktrees so multiple tasks can run in parallel without stepping on each other. But the moment that worktree is missing your .env:
- The agent can't run your dev server
- The agent can't run your tests
- The agent gets confused-looking errors and starts guessing
- You end up babysitting it
With this hook installed, every agent-spawned worktree comes up with the env it needs. The agent never has to know the hook exists.
The hook is a single bash script in this repo: post-checkout. It's ~80 lines, mostly comments. Read it before installing globally if you're cautious β that's a sensible thing to do.
If you'd rather install by hand instead of running install.sh:
mkdir -p ~/.git-hooks
cp post-checkout ~/.git-hooks/post-checkout
chmod +x ~/.git-hooks/post-checkout
git config --global core.hooksPath ~/.git-hooksIf you already have core.hooksPath set somewhere else, copy post-checkout there instead. Don't change core.hooksPath β the hook respects whatever you've already configured.
git config --global core.hooksPath ~/.git-hooks replaces per-repo hooks β it does not stack with them. If a repo had .git/hooks/post-checkout before, that hook stops running once core.hooksPath is set globally.
This would silently break git-lfs, which installs a per-repo post-checkout hook on every LFS-enabled repo. After the env-copy logic finishes, our hook does this:
PER_REPO_HOOK="$GIT_COMMON_DIR/hooks/post-checkout"
if [ -x "$PER_REPO_HOOK" ] && [ "$PER_REPO_HOOK" != "$0" ]; then
exec "$PER_REPO_HOOK" "$@"
fiSo per-repo hooks always run, regardless of whether the env-copy block succeeded, was skipped, or errored out. The env-copy block is wrapped in { ...; } || true for that exact reason β its failure can never block the chain.
This means husky, lefthook, custom team hooks, LFS β they all keep working.
cd worktree-env-copy
./uninstall.shuninstall.sh is standalone β it doesn't need anything else from the repo. You can also download just that one file and run it on its own (recommended if you no longer have the repo cloned):
curl -O https://raw.githubusercontent.com/DaniAkash/worktree-env-copy/main/uninstall.sh
bash uninstall.shThe uninstaller is conservative:
- Identifies our hook by a marker string (the project URL embedded in the hook). Refuses to remove a
post-checkouthook that doesn't carry the marker β pass--forceif you've modified the hook and want to remove it anyway. - Only unsets
core.hooksPathif it points at~/.git-hooksAND that directory is empty afterwards. If you have other hooks in there, the directory and the config are both left alone. - See
./uninstall.sh --helpfor usage.
If something looks unexpected, the uninstaller tells you what to do manually rather than guessing.
Check core.hooksPath is configured:
git config --global core.hooksPath
# Should output something like /Users/you/.git-hooksIf empty, re-run ./install.sh.
Check the hook is executable:
ls -la ~/.git-hooks/post-checkout
# Should show -rwxr-xr-x (note the 'x' for executable)If not, chmod +x ~/.git-hooks/post-checkout.
Confirm the source repo actually has .env* files outside node_modules:
cd /path/to/main-repo
find . -name ".env*" -not -path "./.git/*" -not -path "./node_modules/*"If the only matches are inside gitignored directories, the hook (correctly) skips them.
Run it manually inside a worktree with bash trace:
cd /path/to/worktree
bash -x ~/.git-hooks/post-checkout 0 0 1The third arg (1) tells the hook this is a branch checkout, which is what git worktree add triggers.
.env.example is usually tracked, so when the worktree is created it already exists from the index, and the hook skips it (you'll see β Skipped .env.example (already exists)). If you have an untracked .env.example and want to exclude it, edit your local copy of ~/.git-hooks/post-checkout and add ! -name ".env.example" to the find predicate.
Inside a worktree, git rev-parse --git-dir returns .git/worktrees/<name>. Its parent is the wrong thing β that's .git/worktrees/, not the repo root. --git-common-dir always points to the main .git directory, so dirname of that gives the actual main repo. This is the only reliable way to find the source repo from inside a worktree.
post-checkout receives three args: prev_head new_head branch_flag. The flag is 1 for branch checkouts and 0 for file checkouts (git checkout -- some-file). We gate the env-copy on $3 == 1 so we don't run on every file checkout, but we still chain to per-repo hooks unconditionally because LFS cares about both kinds of checkout.
Instead of finding all .env* and then filtering, the hook walks two levels deep, asks git which of those directories are ignored, and adds them as find -prune arguments upfront. This is faster (no traversing node_modules/ with thousands of files) and uses each repo's actual .gitignore instead of a hardcoded blocklist.
The chain check is [ "$PER_REPO_HOOK" != "$0" ] β paranoia for the case where someone symlinks a per-repo hook to the global one. Without the guard, the hook would call itself forever.
- Machine-global. This sets
core.hooksPathfor your entire git installation, not per-repo. That's intentional β it's what makes the hook fire for tools that bypass your shell. But you should know. - Bash 3.2 compatible. Tested on the macOS system bash. No bash 4+ features used.
maxdepth 3for prune discovery. Gitignored directories deeper than 3 levels won't be pruned andfindwill descend into them. In practice this only matters for unusual nested monorepos and the cost is small.- No Windows support. Bash hook + POSIX paths. PRs welcome.
Adapted from therohitdas/copy-env β original idea and bulk of the env-copy logic. Modified to:
- Always chain to per-repo hooks (so git-lfs, husky, and friends keep working)
- Wrap the env-copy block in
{ ...; } || trueso its failure can't break the chain - Add an idempotent installer with collision detection and a conservative uninstaller
MIT β see LICENSE.