-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathautomating-env-files-in-git-worktrees.html
More file actions
298 lines (239 loc) · 19.5 KB
/
automating-env-files-in-git-worktrees.html
File metadata and controls
298 lines (239 loc) · 19.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="HandheldFriendly" content="True" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="robots" content="" />
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Source+Sans+Pro:300,400,400i,700" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="./theme/stylesheet/style.min.css">
<link rel="stylesheet" type="text/css" href="./theme/pygments/github.min.css">
<link rel="stylesheet" type="text/css" href="./theme/font-awesome/css/font-awesome.min.css">
<link href="https://sephib.github.io/feeds/all.atom.xml" type="application/atom+xml" rel="alternate" title="Geo Berry Atom">
<link rel="shortcut icon" href="/images/favicon.ico" type="image/x-icon">
<link rel="icon" href="/images/favicon.ico" type="image/x-icon">
<meta name="author" content="Sephi Berry" />
<meta name="description" content="Git worktrees respect .gitignore — which means they don't have .env files from the bare git cloned repo. Here's a bash hook that fixed this." />
<meta name="keywords" content="git, worktrees, automation, devops, agor">
<meta property="og:site_name" content="Geo Berry"/>
<meta property="og:title" content="Automating .env Files in Git Worktrees"/>
<meta property="og:description" content="Git worktrees respect .gitignore — which means they don't have .env files from the bare git cloned repo. Here's a bash hook that fixed this."/>
<meta property="og:locale" content="en_US"/>
<meta property="og:url" content="./automating-env-files-in-git-worktrees.html"/>
<meta property="og:type" content="article"/>
<meta property="article:published_time" content="2026-03-12 00:00:00+02:00"/>
<meta property="article:modified_time" content=""/>
<meta property="article:author" content="./author/sephi-berry.html">
<meta property="article:section" content="posts"/>
<meta property="article:tag" content="git"/>
<meta property="article:tag" content="worktrees"/>
<meta property="article:tag" content="automation"/>
<meta property="article:tag" content="devops"/>
<meta property="article:tag" content="agor"/>
<meta property="og:image" content="/images/avatar_osnx.png">
<title>Geo Berry – Automating .env Files in Git Worktrees</title>
</head>
<body>
<aside>
<div>
<a href=".">
<img src="/images/avatar_osnx.png" alt="Sephi's Blog" title="Sephi's Blog">
</a>
<h1><a href=".">Sephi's Blog</a></h1>
<p>ML / Data Engineer | Project Manager | Geo-Spatial Specialist</p>
<nav>
<ul class="list">
<li><a href="./pages/about.html#about">About</a></li>
</ul>
</nav>
<ul class="social">
<li><a class="sc-linkedin" href="https://www.linkedin.com/in/berrygis" target="_blank"><i class="fa fa-linkedin"></i></a></li>
<li><a class="sc-github" href="https://github.com/sephib" target="_blank"><i class="fa fa-github"></i></a></li>
<li><a class="sc-twitter" href="https://twitter.com/geosephi" target="_blank"><i class="fa fa-twitter"></i></a></li>
</ul>
</div>
</aside>
<main>
<article class="single">
<header>
<h1 id="automating-env-files-in-git-worktrees">Automating .env Files in Git Worktrees</h1>
<p>
Posted on Thu 12 March 2026 in <a href="./category/posts.html">posts</a>
</p>
</header>
<div>
<h1>The Moment I Automated Away</h1>
<p>It's 11:47 PM. I've just spun up my fourth worktree of the day to review a PR before bed. Run the tests. They fail.</p>
<div class="highlight"><pre><span></span><code><span class="n">Error</span><span class="o">:</span><span class="w"> </span><span class="n">DATABASE_URL</span><span class="w"> </span><span class="k">is</span><span class="w"> </span><span class="n">not</span><span class="w"> </span><span class="n">defined</span>
</code></pre></div>
<p>Again.</p>
<p>For the fourth time today, I've forgotten to copy <code>.env</code> files to the new worktree. I spend 90 seconds finding the repository path (because Agor uses bare repos), another 30 seconds copying the files, and—crucially—I've lost the mental thread of what I was reviewing.</p>
<p>When you're using tools like <a href="https://agor.live">Agor</a> that create git worktrees automatically for each agent task, manual environment propagation becomes untenable.</p>
<hr>
<h1>Why Git Worktrees Don't Copy .env Files (And Why That's Correct)</h1>
<p>Git worktrees solve a problem we've had in the past: the branch-switching dance. Before worktrees, switching branches meant stashing work, hoping the checkout didn't break, and watching your IDE rebuild indexes. Worktrees give each branch its own isolated working directory:</p>
<div class="highlight"><pre><span></span><code>project/
├── main/ # main branch worktree
├── feature-auth/ # feature branch worktree
└── .git/ # shared git metadata
</code></pre></div>
<p>But worktrees inherit a design decision from <code>git checkout</code>: <strong>they respect <code>.gitignore</code></strong>. This is architecturally correct—gitignored files are local configuration, not source code. They shouldn't propagate automatically.</p>
<p>The problem emerges when you have:
- <strong>Multiple environment files</strong> (<code>.env</code>, <code>.env.test</code>, <code>.env.staging</code>)
- <strong>Automated worktree creation</strong> (CI/CD, orchestration platforms)
- <strong>Bare repositories</strong> (modern tooling like Agor, GitLab runners, GitHub Actions)</p>
<p>We used to solve "environment per branch" by checking in <code>.env.example</code> and copying it manually. Then once the <code>.env</code> was configured - any branch in that was in the folder had an up-to-date <code>.env</code> file. But now when we have a separate folder for each branch - we need to manually copy the <code>.env</code> whenever we create a new <em>git worktree</em></p>
<p>Let's fix it.</p>
<hr>
<h1>Global Git Hook (Zero-Touch Automation)</h1>
<p>A <code>post-checkout</code> hook that runs automatically when worktrees are created.</p>
<h3>The Hook Architecture</h3>
<p>Git provides hooks—scripts that run at specific lifecycle events. <code>post-checkout</code> runs after any checkout operation, including worktree creation. We can detect worktrees by comparing two git internals:</p>
<div class="highlight"><pre><span></span><code>--git-common-dir<span class="w"> </span><span class="c1"># The shared .git directory (main repo)</span>
--git-dir<span class="w"> </span><span class="c1"># The current .git directory (worktree-specific)</span>
</code></pre></div>
<p>In a regular repo, these paths are identical. In a worktree, they differ:</p>
<div class="highlight"><pre><span></span><code># Main repo
$ git rev-parse --git-common-dir
~/.agor/repos/my-repo/.git
$ git rev-parse --git-dir
~/.agor/repos/my-repo/.git # Same!
# Worktree
$ git rev-parse --git-common-dir
~/.agor/repos/my-repo/.git # Points to main
$ git rev-parse --git-dir
~/.agor/worktrees/my-repo/feature-x/.git # Different!
</code></pre></div>
<p>When they differ, we know we're in a worktree. Then we can copy <code>.env</code> files from the main repo.</p>
<h3>Setup (One-Time)</h3>
<div class="highlight"><pre><span></span><code><span class="c1"># Create global hooks directory</span>
mkdir<span class="w"> </span>-p<span class="w"> </span>~/.git-hooks
<span class="c1"># Tell git to use it for all repos</span>
git<span class="w"> </span>config<span class="w"> </span>--global<span class="w"> </span>core.hooksPath<span class="w"> </span>~/.git-hooks
<span class="c1"># Create the hook</span>
touch<span class="w"> </span>~/.git-hooks/post-checkout
chmod<span class="w"> </span>+x<span class="w"> </span>~/.git-hooks/post-checkout
</code></pre></div>
<h3>The Hook Implementation</h3>
<p>Here's the full script with inline commentary on the critical decisions:</p>
<div class="highlight"><pre><span></span><code><span class="ch">#!/bin/bash</span>
<span class="c1"># Global git hook: automatically copy .env files to new worktrees</span>
<span class="c1"># Get paths to git directories</span>
<span class="nv">MAIN_REPO_PATH</span><span class="o">=</span><span class="k">$(</span>git<span class="w"> </span>rev-parse<span class="w"> </span>--git-common-dir<span class="w"> </span><span class="m">2</span>>/dev/null<span class="k">)</span>
<span class="nv">CURRENT_PATH</span><span class="o">=</span><span class="k">$(</span>git<span class="w"> </span>rev-parse<span class="w"> </span>--git-dir<span class="w"> </span><span class="m">2</span>>/dev/null<span class="k">)</span>
<span class="c1"># Detect if we're in a worktree (paths differ)</span>
<span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span><span class="s2">"</span><span class="nv">$MAIN_REPO_PATH</span><span class="s2">"</span><span class="w"> </span>!<span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$CURRENT_PATH</span><span class="s2">"</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w"> </span><span class="c1"># Get filesystem paths (not .git internals)</span>
<span class="w"> </span><span class="nv">MAIN_REPO_ROOT</span><span class="o">=</span><span class="k">$(</span>dirname<span class="w"> </span><span class="s2">"</span><span class="nv">$MAIN_REPO_PATH</span><span class="s2">"</span><span class="k">)</span>
<span class="w"> </span><span class="nv">WORKTREE_ROOT</span><span class="o">=</span><span class="k">$(</span>git<span class="w"> </span>rev-parse<span class="w"> </span>--show-toplevel<span class="k">)</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"🔧 Git worktree detected - checking for .env files to copy..."</span>
<span class="w"> </span><span class="c1"># Find all .env files in main repo</span>
<span class="w"> </span><span class="c1"># maxdepth 3: prevents performance death in monorepos</span>
<span class="w"> </span><span class="c1"># 2>/dev/null: suppress errors in repos without .env files</span>
<span class="w"> </span><span class="nv">env_files</span><span class="o">=</span><span class="k">$(</span>find<span class="w"> </span><span class="s2">"</span><span class="nv">$MAIN_REPO_ROOT</span><span class="s2">"</span><span class="w"> </span>-maxdepth<span class="w"> </span><span class="m">3</span><span class="w"> </span>-name<span class="w"> </span><span class="s1">'.env*'</span><span class="w"> </span>-type<span class="w"> </span>f<span class="w"> </span><span class="m">2</span>>/dev/null<span class="k">)</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$env_files</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="k">while</span><span class="w"> </span><span class="nb">read</span><span class="w"> </span>-r<span class="w"> </span>env_file<span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w"> </span><span class="c1"># Get relative path (e.g., ".env" or "apps/web/.env")</span>
<span class="w"> </span><span class="nv">rel_path</span><span class="o">=</span><span class="s2">"</span><span class="si">${</span><span class="nv">env_file</span><span class="p">#</span><span class="nv">$MAIN_REPO_ROOT</span><span class="p">/</span><span class="si">}</span><span class="s2">"</span>
<span class="w"> </span><span class="c1"># CRITICAL: Check gitignore from worktree context</span>
<span class="w"> </span><span class="c1"># This is the line that makes it work with bare repositories</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="o">(</span><span class="nb">cd</span><span class="w"> </span><span class="s2">"</span><span class="nv">$WORKTREE_ROOT</span><span class="s2">"</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span>git<span class="w"> </span>check-ignore<span class="w"> </span>-q<span class="w"> </span><span class="s2">"</span><span class="nv">$rel_path</span><span class="s2">"</span><span class="w"> </span><span class="m">2</span>>/dev/null<span class="o">)</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w"> </span><span class="nv">target</span><span class="o">=</span><span class="s2">"</span><span class="nv">$WORKTREE_ROOT</span><span class="s2">/</span><span class="nv">$rel_path</span><span class="s2">"</span>
<span class="w"> </span><span class="c1"># Only copy if file doesn't exist (respect manual configs)</span>
<span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="o">[</span><span class="w"> </span>!<span class="w"> </span>-f<span class="w"> </span><span class="s2">"</span><span class="nv">$target</span><span class="s2">"</span><span class="w"> </span><span class="o">]</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w"> </span>mkdir<span class="w"> </span>-p<span class="w"> </span><span class="s2">"</span><span class="k">$(</span>dirname<span class="w"> </span><span class="s2">"</span><span class="nv">$target</span><span class="s2">"</span><span class="k">)</span><span class="s2">"</span>
<span class="w"> </span>cp<span class="w"> </span><span class="s2">"</span><span class="nv">$env_file</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$target</span><span class="s2">"</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">" ✓ Copied </span><span class="nv">$rel_path</span><span class="s2"> to worktree"</span>
<span class="w"> </span><span class="k">fi</span>
<span class="w"> </span><span class="k">fi</span>
<span class="w"> </span><span class="k">done</span>
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"✅ .env setup complete for worktree"</span>
<span class="k">fi</span>
</code></pre></div>
<h3>Why the <code>cd $WORKTREE_ROOT</code> Matters</h3>
<p>The most important line in this script is:</p>
<div class="highlight"><pre><span></span><code><span class="k">if</span><span class="w"> </span><span class="o">(</span><span class="nb">cd</span><span class="w"> </span><span class="s2">"</span><span class="nv">$WORKTREE_ROOT</span><span class="s2">"</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span>git<span class="w"> </span>check-ignore<span class="w"> </span>-q<span class="w"> </span><span class="s2">"</span><span class="nv">$rel_path</span><span class="s2">"</span><span class="w"> </span><span class="m">2</span>>/dev/null<span class="o">)</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
</code></pre></div>
<p>Why run <code>git check-ignore</code> from the worktree context instead of the current directory?</p>
<p><strong>Bare repositories don't have a working tree.</strong></p>
<p>If you're using Agor, GitLab CI, or GitHub Actions with worktrees, you're dealing with bare repos:</p>
<div class="highlight"><pre><span></span><code><span class="na">.agor</span><span class="err">/</span><span class="no">repos</span><span class="err">/</span><span class="no">my-repo</span><span class="err">/</span><span class="no">.git</span><span class="err">/</span><span class="w"> </span><span class="c1"># Bare repo (no checkout)</span>
<span class="na">.agor</span><span class="err">/</span><span class="no">worktrees</span><span class="err">/</span><span class="no">my-repo</span><span class="err">/</span><span class="no">main</span><span class="err">/</span><span class="w"> </span><span class="c1"># Worktree (has checkout)</span>
</code></pre></div>
<p>If you try to run <code>git check-ignore</code> from a bare repo directory, git fails:</p>
<div class="highlight"><pre><span></span><code>$<span class="w"> </span><span class="nb">cd</span><span class="w"> </span>.agor/repos/my-repo/.git
$<span class="w"> </span>git<span class="w"> </span>check-ignore<span class="w"> </span>.env
fatal:<span class="w"> </span>this<span class="w"> </span>operation<span class="w"> </span>must<span class="w"> </span>be<span class="w"> </span>run<span class="w"> </span><span class="k">in</span><span class="w"> </span>a<span class="w"> </span>work<span class="w"> </span>tree
</code></pre></div>
<p>By switching to the worktree context with <code>cd "$WORKTREE_ROOT"</code>, we sidestep this entirely. The worktree <em>does</em> have a working tree, so it can evaluate <code>.gitignore</code> rules.</p>
<p><strong>Lesson:</strong> Always validate that your automation did what you think it did. Don't trust exit codes—check the filesystem.</p>
<hr>
<h1>Conclusion: Automate Friction, But Measure the Cost</h1>
<p>Developer experience is measured in <strong>friction removed</strong>, not features added.</p>
<p><strong>The principle:</strong> Automate the things you do frequently, but measure the cost.</p>
<p>Global hooks are powerful.</p>
<hr>
<h1>Next Steps</h1>
<p><strong>To implement this yourself:</strong></p>
<ol>
<li>
<p><strong>Create the global hooks directory:</strong>
<code>bash
mkdir -p ~/.git-hooks
git config --global core.hooksPath ~/.git-hooks</code></p>
</li>
<li>
<p><strong>Copy the hook script</strong> (from the "Hook Implementation" section above) to <code>~/.git-hooks/post-checkout</code></p>
</li>
<li>
<p><strong>Make it executable:</strong>
<code>bash
chmod +x ~/.git-hooks/post-checkout</code></p>
</li>
<li>
<p><strong>Test with a bare repo:</strong>
<code>bash
git clone --bare <your-repo-url> test-bare
cd test-bare
git worktree add ../test-worktree
ls ../test-worktree/.env* # Verify files copied</code></p>
</li>
<li>
<p><strong>Add to your team docs</strong> so onboarding doesn't break</p>
</li>
</ol>
<p><strong>Further reading:</strong></p>
<ul>
<li><a href="https://github.com/therohitdas/copy-env">therohitdas/copy-env</a> - Similar approach - was the inspiration for this solution</li>
<li><a href="https://chrisdicarlo.ca/blog/working-with-git-worktrees-part-2/">Working with Git Worktrees - Chris DiCarlo</a> - A different solution based on a stand alone script</li>
<li><a href="https://git-scm.com/docs/git-worktree">Git Worktrees Official Docs</a> </li>
<li><a href="https://git-scm.com/docs/githooks">Git Hooks Official Docs</a> </li>
</ul>
</div>
<div class="tag-cloud">
<p>
<a href="./tag/git.html">git</a>
<a href="./tag/worktrees.html">worktrees</a>
<a href="./tag/automation.html">automation</a>
<a href="./tag/devops.html">devops</a>
<a href="./tag/agor.html">agor</a>
</p>
</div>
</article>
<footer>
<p>© </p>
<p> Powered by <a href="http://getpelican.com" target="_blank">Pelican</a> - <a href="https://github.com/alexandrevicenzi/flex" target="_blank">Flex</a> theme by <a href="http://alexandrevicenzi.com" target="_blank">Alexandre Vicenzi</a>
</p> </footer>
</main>
<script type="application/ld+json">
{
"@context" : "http://schema.org",
"@type" : "Blog",
"name": " Geo Berry ",
"url" : ".",
"image": "/images/avatar_osnx.png",
"description": "Sephi's Thoughts and Writings"
}
</script>
</body>
</html>