You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -31,95 +31,155 @@ import KanvasCTA from "../../../../sections/Kanvas/kanvas-cta";
31
31
</p>
32
32
</div>
33
33
34
-
## The Surprise
35
-
36
-
AI coding assistants like Claude Code, Cline, Aider, and similar tools spawn child shell processes to run commands on your behalf. From where you sit, the terminal looks identical to the one you use every day. But the shell those tools launch is fundamentally different from the one you interact with — and that difference determines which startup files are read, which means it determines what is on your PATH.
37
-
38
-
The result is a confusing experience: tools you have used for years are suddenly invisible to the agent trying to help you. The culprit is not your installation. It is the shell startup file hierarchy.
39
-
40
-
## Zsh Startup Files and When They Are Sourced
41
-
42
-
Zsh loads different configuration files depending on how it was launched. There are four main files, and they are read in this order:
The key column is the last one. A *non-interactive, non-login shell* — the kind that Claude Code spawns — sources **only**`~/.zshenv`. Everything else is skipped entirely.
52
-
53
-
-**`~/.zshenv`** is sourced for every zsh invocation, no matter what. It is the right place for environment variables that must be available universally: `PATH`, `GOPATH`, `JAVA_HOME`, and similar exports.
54
-
-**`~/.zprofile`** is sourced for login shells (e.g., when you open a new terminal window or SSH into a machine). Homebrew places its environment setup here on Apple Silicon Macs (`/opt/homebrew/bin/brew shellenv`), which is why Homebrew tools can also go missing.
55
-
-**`~/.zshrc`** is sourced only for interactive shells — sessions where you type commands. This is where most developers put everything: aliases, prompt configuration, `nvm`, `pyenv`, `rbenv`,
56
-
completions, and — critically — PATH customizations.
57
-
-**`~/.zlogin`** is sourced after `~/.zshrc` for login shells. It is rarely used by developers directly.
34
+
<h2>The Surprise</h2>
35
+
36
+
<p>
37
+
AI coding assistants like Claude Code, Cline, Aider, and similar tools spawn child shell processes to run commands on your behalf. From where you sit, the terminal looks identical to the one you use every day. But the shell those tools launch is fundamentally different from the one you interact with — and that difference determines which startup files are read, which means it determines what is on your PATH.
38
+
</p>
39
+
40
+
<p>
41
+
The result is a confusing experience: tools you have used for years are suddenly invisible to the agent trying to help you. The culprit is not your installation. It is the shell startup file hierarchy.
42
+
</p>
43
+
44
+
<h2>Zsh Startup Files and When They Are Sourced</h2>
45
+
46
+
<p>
47
+
Zsh loads different configuration files depending on how it was launched. There are four main files, and they are read in this order:
48
+
</p>
49
+
50
+
<table>
51
+
<thead>
52
+
<tr>
53
+
<th>File</th>
54
+
<th>Login shell</th>
55
+
<th>Interactive shell</th>
56
+
<th>Non-interactive shell</th>
57
+
</tr>
58
+
</thead>
59
+
<tbody>
60
+
<tr>
61
+
<td><code>{"~/.zshenv"}</code></td>
62
+
<td>Yes</td>
63
+
<td>Yes</td>
64
+
<td>Yes</td>
65
+
</tr>
66
+
<tr>
67
+
<td><code>{"~/.zprofile"}</code></td>
68
+
<td>Yes</td>
69
+
<td>No</td>
70
+
<td>No</td>
71
+
</tr>
72
+
<tr>
73
+
<td><code>{"~/.zshrc"}</code></td>
74
+
<td>No</td>
75
+
<td>Yes</td>
76
+
<td>No</td>
77
+
</tr>
78
+
<tr>
79
+
<td><code>{"~/.zlogin"}</code></td>
80
+
<td>Yes</td>
81
+
<td>No</td>
82
+
<td>No</td>
83
+
</tr>
84
+
</tbody>
85
+
</table>
86
+
87
+
<p>
88
+
The key column is the last one. A <em>non-interactive, non-login shell</em> — the kind that Claude Code spawns — sources <strong>only</strong> <code>{"~/.zshenv"}</code>. Everything else is skipped entirely.
89
+
</p>
90
+
91
+
<ul>
92
+
<li>
93
+
<strong><code>{"~/.zshenv"}</code></strong> is sourced for every zsh invocation, no matter what. It is the right place for environment variables that must be available universally: <code>PATH</code>, <code>GOPATH</code>, <code>JAVA_HOME</code>, and similar exports.
94
+
</li>
95
+
<li>
96
+
<strong><code>{"~/.zprofile"}</code></strong> is sourced for login shells (e.g., when you open a new terminal window or SSH into a machine). Homebrew places its environment setup here on Apple Silicon Macs (<code>/opt/homebrew/bin/brew shellenv</code>), which is why Homebrew tools can also go missing.
97
+
</li>
98
+
<li>
99
+
<strong><code>{"~/.zshrc"}</code></strong> is sourced only for interactive shells — sessions where you type commands. This is where most developers put everything: aliases, prompt configuration, <code>nvm</code>, <code>pyenv</code>, <code>rbenv</code>, completions, and — critically — PATH customizations.
100
+
</li>
101
+
<li>
102
+
<strong><code>{"~/.zlogin"}</code></strong> is sourced after <code>{"~/.zshrc"}</code> for login shells. It is rarely used by developers directly.
103
+
</li>
104
+
</ul>
58
105
59
106
<Blockquote
60
107
quote="A non-interactive, non-login shell sources only ~/.zshenv. Everything in ~/.zshrc is invisible to it — including every PATH export most developers have ever written."
61
108
/>
62
109
63
-
## What PATH a Non-Interactive Shell Actually Sees
110
+
<h2>What PATH a Non-Interactive Shell Actually Sees</h2>
64
111
65
-
You can observe this yourself. Run these two commands and compare the output:
112
+
<p>
113
+
You can observe this yourself. Run these two commands and compare the output:
114
+
</p>
66
115
67
-
```bash
68
-
# What a non-interactive shell sees
116
+
<pre>
117
+
<code>{`# What a non-interactive shell sees
69
118
zsh -c 'echo $PATH'
70
119
71
120
# What your interactive shell sees
72
-
echo$PATH
73
-
```
121
+
echo $PATH`}</code>
122
+
</pre>
74
123
75
-
On a typical macOS developer machine, the non-interactive shell might produce something like:
124
+
<p>
125
+
On a typical macOS developer machine, the non-interactive shell might produce something like:
All those extra paths — Homebrew, nvm, Go binaries — live in `~/.zshrc`, `~/.zprofile`, or in
89
-
scripts those files source. A non-interactive shell never reads any of them.
141
+
<p>
142
+
All those extra paths — Homebrew, nvm, Go binaries — live in <code>{"~/.zshrc"}</code>, <code>{"~/.zprofile"}</code>, or in scripts those files source. A non-interactive shell never reads any of them.
143
+
</p>
90
144
91
-
## Diagnosing the Problem
145
+
<h2>Diagnosing the Problem</h2>
92
146
93
-
Before changing anything, confirm the symptom. Use `zsh -c` to simulate what Claude Code sees:
147
+
<p>
148
+
Before changing anything, confirm the symptom. Use <code>zsh -c</code> to simulate what Claude Code sees:
149
+
</p>
94
150
95
-
```bash
96
-
# Does Claude Code's shell see the tool?
151
+
<pre>
152
+
<code>{`# Does Claude Code's shell see the tool?
97
153
zsh -c 'which gh'
98
154
zsh -c 'which go'
99
155
zsh -c 'which node'
100
156
101
157
# Does your interactive shell see it?
102
158
zsh -i -c 'which gh'
103
159
zsh -i -c 'which go'
104
-
zsh -i -c 'which node'
105
-
```
160
+
zsh -i -c 'which node'`}</code>
161
+
</pre>
106
162
107
-
The `-i` flag forces an interactive shell, which sources `~/.zshrc`. If `zsh -c 'which gh'` prints
108
-
`gh not found` but `zsh -i -c 'which gh'` prints the correct path, your PATH export is in
109
-
`~/.zshrc` and you have confirmed the root cause.
163
+
<p>
164
+
The <code>-i</code> flag forces an interactive shell, which sources <code>{"~/.zshrc"}</code>. If <code>zsh -c 'which gh'</code> prints <code>gh not found</code> but <code>zsh -i -c 'which gh'</code> prints the correct path, your PATH export is in <code>{"~/.zshrc"}</code> and you have confirmed the root cause.
165
+
</p>
110
166
111
167
<divclassName="note">
112
168
<strong>Quick diagnosis:</strong> Run <code>zsh -c 'which gh'</code> (no <code>-i</code> flag). If this fails but <code>which gh</code> in your normal terminal works, your PATH is only set in <code>{"~/.zshrc"}</code>. Move the relevant exports to <code>{"~/.zshenv"}</code> to fix it.
113
169
</div>
114
170
115
-
## The Fix: Move PATH Exports to ~/.zshenv
171
+
<h2>The Fix: Move PATH Exports to <code>{"~/.zshenv"}</code></h2>
116
172
117
-
The solution is straightforward: any environment variable that must be visible to all processes — including non-interactive subshells — belongs in `~/.zshenv`, not `~/.zshrc`.
173
+
<p>
174
+
The solution is straightforward: any environment variable that must be visible to all processes — including non-interactive subshells — belongs in <code>{"~/.zshenv"}</code>, not <code>{"~/.zshrc"}</code>.
175
+
</p>
118
176
119
-
Open (or create) `~/.zshenv` and add your PATH exports there:
177
+
<p>
178
+
Open (or create) <code>{"~/.zshenv"}</code> and add your PATH exports there:
179
+
</p>
120
180
121
-
```bash
122
-
# ~/.zshenv
181
+
<pre>
182
+
<code>{`# ~/.zshenv
123
183
# Sourced for every zsh invocation — interactive, login, and non-interactive alike
124
184
125
185
# Homebrew (Apple Silicon)
@@ -130,42 +190,50 @@ export GOPATH="$HOME/go"
130
190
export PATH="$GOPATH/bin:$PATH"
131
191
132
192
# Local binaries
133
-
export PATH="$HOME/.local/bin:$PATH"
134
-
```
193
+
export PATH="$HOME/.local/bin:$PATH"`}</code>
194
+
</pre>
135
195
136
-
For tools that inject themselves via an eval expression in `~/.zshrc` — such as nvm, pyenv, or rbenv — you need to move or duplicate that initialization into `~/.zshenv` as well:
196
+
<p>
197
+
For tools that inject themselves via an eval expression in <code>{"~/.zshrc"}</code> — such as <code>nvm</code>, <code>pyenv</code>, or <code>rbenv</code> — you need to move or duplicate that initialization into <code>{"~/.zshenv"}</code> as well:
198
+
</p>
137
199
138
-
```bash
139
-
# ~/.zshenv — nvm initialization for non-interactive shells
200
+
<pre>
201
+
<code>{`# ~/.zshenv — nvm initialization for non-interactive shells
Note the `--no-use` flag for nvm: it initializes nvm without switching to the default Node.js version, which speeds up shell startup for non-interactive contexts. Remove it if you want the default version active everywhere.
211
+
<p>
212
+
Note the <code>--no-use</code> flag for nvm: it initializes nvm without switching to the default Node.js version, which speeds up shell startup for non-interactive contexts. Remove it if you want the default version active everywhere.
213
+
</p>
150
214
151
-
After saving `~/.zshenv`, verify without restarting your terminal:
215
+
<p>
216
+
After saving <code>{"~/.zshenv"}</code>, verify without restarting your terminal:
217
+
</p>
152
218
153
-
```bash
154
-
# Reload .zshenv in your current shell
219
+
<pre>
220
+
<code>{`# Reload .zshenv in your current shell
155
221
source ~/.zshenv
156
222
157
223
# Confirm Claude Code's shell type now finds the tool
158
224
zsh -c 'which gh'
159
225
zsh -c 'which go'
160
-
zsh -c 'which node'
161
-
```
226
+
zsh -c 'which node'`}</code>
227
+
</pre>
162
228
163
-
## What to Keep in .zshrc vs .zshenv
229
+
<h2>What to Keep in <code>{"~/.zshrc"}</code> vs <code>{"~/.zshenv"}</code></h2>
164
230
165
-
Moving everything to `~/.zshenv` is not the right answer. Some configuration should stay in `~/.zshrc` because it only makes sense in interactive contexts or because it has side effects that slow down non-interactive shells unnecessarily.
231
+
<p>
232
+
Moving everything to <code>{"~/.zshenv"}</code> is not the right answer. Some configuration should stay in <code>{"~/.zshrc"}</code> because it only makes sense in interactive contexts or because it has side effects that slow down non-interactive shells unnecessarily.
233
+
</p>
166
234
167
235
<divclassName="tip">
168
-
<strong>Guiding principle:</strong> If it is an environment variable that a program needs to find another program, it belongs in <code>~/.zshenv</code>. If it is a user-facing customization for your interactive terminal experience, it belongs in <code>~/.zshrc</code>.
236
+
<strong>Guiding principle:</strong> If it is an environment variable that a program needs to find another program, it belongs in <code>{"~/.zshenv"}</code>. If it is a user-facing customization for your interactive terminal experience, it belongs in <code>{"~/.zshrc"}</code>.
169
237
170
238
<p><strong>Keep in <code>{"~/.zshenv"}</code>:</strong></p>
171
239
<ul>
@@ -187,31 +255,39 @@ Moving everything to `~/.zshenv` is not the right answer. Some configuration sho
187
255
</ul>
188
256
</div>
189
257
190
-
## The Broader Pattern: Any Tool That Spawns Subshells
258
+
<h2>The Broader Pattern: Any Tool That Spawns Subshells</h2>
191
259
192
-
Claude Code is not unique here. This same behavior affects any process that spawns a child shell without the `-i` or `-l` flags:
260
+
<p>
261
+
Claude Code is not unique here. This same behavior affects any process that spawns a child shell without the <code>-i</code> or <code>-l</code> flags:
262
+
</p>
193
263
194
-
- CI/CD pipelines (GitHub Actions, GitLab CI) run commands in non-interactive shells. This is why you often see pipelines that explicitly `source ~/.bashrc` or set up PATH at the top of every job.
195
-
- Cron jobs run in minimal environments with almost no PATH set.
196
-
- VS Code integrated terminal tasks and `launch.json` configurations may use non-interactive shells depending on the operating system and configuration.
197
-
- SSH remote command execution (`ssh host 'command'`) uses a non-interactive shell unless you pass `-t` to force a TTY.
198
-
- Make and other build systems that shell out to run commands.
199
-
- Docker `RUN` instructions in Dockerfiles.
264
+
<ul>
265
+
<li>CI/CD pipelines (GitHub Actions, GitLab CI) run commands in non-interactive shells. This is why you often see pipelines that explicitly <code>source ~/.bashrc</code> or set up PATH at the top of every job.</li>
266
+
<li>Cron jobs run in minimal environments with almost no PATH set.</li>
267
+
<li>VS Code integrated terminal tasks and <code>launch.json</code> configurations may use non-interactive shells depending on the operating system and configuration.</li>
268
+
<li>SSH remote command execution (<code>ssh host 'command'</code>) uses a non-interactive shell unless you pass <code>-t</code> to force a TTY.</li>
269
+
<li>Make and other build systems that shell out to run commands.</li>
270
+
<li>Docker <code>RUN</code> instructions in Dockerfiles.</li>
271
+
</ul>
200
272
201
-
If you have ever fixed a "works on my machine" problem by adding `export PATH=...` to a CI configuration or a Dockerfile, you have already solved the same class of problem. The `~/.zshenv` fix is just the developer workstation equivalent.
273
+
<p>
274
+
If you have ever fixed a <em>works on my machine</em> problem by adding <code>export PATH=...</code> to a CI configuration or a Dockerfile, you have already solved the same class of problem. The <code>{"~/.zshenv"}</code> fix is just the developer workstation equivalent.
275
+
</p>
202
276
203
277
<Blockquote
204
278
quote="If a command works in your terminal but fails in a script, a CI job, or an AI agent, the first question to ask is: which startup files does this shell read?"
205
279
/>
206
280
207
281
<KanvasCTA />
208
282
209
-
## Putting It Together: A Minimal ~/.zshenv Template
283
+
<h2>Putting It Together: A Minimal <code>{"~/.zshenv"}</code> Template</h2>
210
284
211
-
Here is a starting point for a `~/.zshenv` that covers the most common developer tools on macOS. Adjust paths to match your actual installations:
285
+
<p>
286
+
Here is a starting point for a <code>{"~/.zshenv"}</code> that covers the most common developer tools on macOS. Adjust paths to match your actual installations:
287
+
</p>
212
288
213
-
```bash
214
-
# ~/.zshenv
289
+
<pre>
290
+
<code>{`# ~/.zshenv
215
291
# Sourced for ALL zsh shells — interactive, login, and non-interactive.
216
292
# Keep this file fast and free of output-producing commands.
217
293
@@ -244,31 +320,47 @@ fi
244
320
# Editor and locale
245
321
export EDITOR="vim"
246
322
export LANG="en_US.UTF-8"
247
-
export LC_ALL="en_US.UTF-8"
248
-
```
323
+
export LC_ALL="en_US.UTF-8"`}</code>
324
+
</pre>
249
325
250
-
With this in place, restart Claude Code (or any tool that spawns subshells) and run your verification:
326
+
<p>
327
+
With this in place, restart Claude Code (or any tool that spawns subshells) and run your verification:
328
+
</p>
251
329
252
-
```bash
253
-
zsh -c 'which gh && which go && which node'
254
-
```
330
+
<pre>
331
+
<code>{`zsh -c 'which gh && which go && which node'`}</code>
332
+
</pre>
255
333
256
-
All three should now resolve to their correct paths.
334
+
<p>
335
+
All three should now resolve to their correct paths.
336
+
</p>
257
337
258
-
## Summary
338
+
<h2>Summary</h2>
259
339
260
-
The "command not found" error in Claude Code and similar AI coding assistants is a shell startup file problem, not a tool installation problem. Zsh only sources `~/.zshenv` for non-interactive, non-login shells. Everything most developers have placed in `~/.zshrc` — including PATH exports, version manager initializations, and tool-specific environment variables — is invisible to those shells.
340
+
<p>
341
+
The <em>command not found</em> error in Claude Code and similar AI coding assistants is a shell startup file problem, not a tool installation problem. Zsh only sources <code>{"~/.zshenv"}</code> for non-interactive, non-login shells. Everything most developers have placed in <code>{"~/.zshrc"}</code> — including PATH exports, version manager initializations, and tool-specific environment variables — is invisible to those shells.
342
+
</p>
261
343
262
-
The fix is permanent and simple:
344
+
<p>
345
+
The fix is permanent and simple:
346
+
</p>
263
347
264
-
1. Move PATH exports and tool-specific environment variables to `~/.zshenv`.
265
-
2. Verify with `zsh -c 'which <tool>'` before and after.
266
-
3. Keep interactive customizations (aliases, prompt, completions) in `~/.zshrc`.
348
+
<ol>
349
+
<li>Move PATH exports and tool-specific environment variables to <code>{"~/.zshenv"}</code>.</li>
350
+
<li>Verify with <code>{`zsh -c 'which <tool>'`}</code> before and after.</li>
351
+
<li>Keep interactive customizations (aliases, prompt, completions) in <code>{"~/.zshrc"}</code>.</li>
352
+
</ol>
267
353
268
-
The same fix benefits CI pipelines, cron jobs, Makefiles, Docker builds, and any other context where commands run in a non-interactive shell environment.
354
+
<p>
355
+
The same fix benefits CI pipelines, cron jobs, Makefiles, Docker builds, and any other context where commands run in a non-interactive shell environment.
356
+
</p>
269
357
270
-
---
358
+
<hr />
271
359
272
-
*Exploring AI-assisted development workflows and developer tooling? The <Linkto="/community">Layer5 community</Link> is an active group of platform engineers, open source contributors, and DevOps practitioners. Join us on [Slack](https://slack.layer5.io) to share what you are building and get help when you hit walls like this one. You can also follow the <Linkto="/blog">Layer5 blog</Link> for more practical engineering posts.*
360
+
<p>
361
+
<em>
362
+
Exploring AI-assisted development workflows and developer tooling? The <Linkto="/community">Layer5 community</Link> is an active group of platform engineers, open source contributors, and DevOps practitioners. Join us on <ahref="https://slack.layer5.io">Slack</a> to share what you are building and get help when you hit walls like this one. You can also follow the <Linkto="/blog">Layer5 blog</Link> for more practical engineering posts.
0 commit comments