Skip to content

Commit 2ea5849

Browse files
committed
add right-facing arrow in menu
Signed-off-by: Lee Calcote <lee.calcote@layer5.io>
1 parent e617049 commit 2ea5849

File tree

2 files changed

+193
-101
lines changed
  • src
    • collections/blog/2026/03-09-why-claude-code-cant-find-your-tools
    • sections/General/Navigation/utility

2 files changed

+193
-101
lines changed

src/collections/blog/2026/03-09-why-claude-code-cant-find-your-tools/index.mdx

Lines changed: 192 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -31,95 +31,155 @@ import KanvasCTA from "../../../../sections/Kanvas/kanvas-cta";
3131
</p>
3232
</div>
3333

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:
43-
44-
| File | Login shell | Interactive shell | Non-interactive shell |
45-
|---|---|---|---|
46-
| `~/.zshenv` | Yes | Yes | Yes |
47-
| `~/.zprofile` | Yes | No | No |
48-
| `~/.zshrc` | No | Yes | No |
49-
| `~/.zlogin` | Yes | No | No |
50-
51-
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>
58105

59106
<Blockquote
60107
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."
61108
/>
62109

63-
## What PATH a Non-Interactive Shell Actually Sees
110+
<h2>What PATH a Non-Interactive Shell Actually Sees</h2>
64111

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>
66115

67-
```bash
68-
# What a non-interactive shell sees
116+
<pre>
117+
<code>{`# What a non-interactive shell sees
69118
zsh -c 'echo $PATH'
70119
71120
# What your interactive shell sees
72-
echo $PATH
73-
```
121+
echo $PATH`}</code>
122+
</pre>
74123

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:
126+
</p>
76127

77-
```
78-
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
79-
```
128+
<pre>
129+
<code>{`/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`}</code>
130+
</pre>
80131

81-
While your interactive shell shows something far richer:
132+
<p>
133+
While your interactive shell shows something far richer:
134+
</p>
82135

83-
```
84-
/opt/homebrew/bin:/opt/homebrew/sbin:/Users/you/.nvm/versions/node/v20.11.0/bin:
85-
/Users/you/go/bin:/Users/you/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
86-
```
136+
<pre>
137+
<code>{`/opt/homebrew/bin:/opt/homebrew/sbin:/Users/you/.nvm/versions/node/v20.11.0/bin:
138+
/Users/you/go/bin:/Users/you/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`}</code>
139+
</pre>
87140

88-
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>
90144

91-
## Diagnosing the Problem
145+
<h2>Diagnosing the Problem</h2>
92146

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>
94150

95-
```bash
96-
# Does Claude Code's shell see the tool?
151+
<pre>
152+
<code>{`# Does Claude Code's shell see the tool?
97153
zsh -c 'which gh'
98154
zsh -c 'which go'
99155
zsh -c 'which node'
100156
101157
# Does your interactive shell see it?
102158
zsh -i -c 'which gh'
103159
zsh -i -c 'which go'
104-
zsh -i -c 'which node'
105-
```
160+
zsh -i -c 'which node'`}</code>
161+
</pre>
106162

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>
110166

111167
<div className="note">
112168
<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.
113169
</div>
114170

115-
## The Fix: Move PATH Exports to ~/.zshenv
171+
<h2>The Fix: Move PATH Exports to <code>{"~/.zshenv"}</code></h2>
116172

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>
118176

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>
120180

121-
```bash
122-
# ~/.zshenv
181+
<pre>
182+
<code>{`# ~/.zshenv
123183
# Sourced for every zsh invocation — interactive, login, and non-interactive alike
124184
125185
# Homebrew (Apple Silicon)
@@ -130,42 +190,50 @@ export GOPATH="$HOME/go"
130190
export PATH="$GOPATH/bin:$PATH"
131191
132192
# Local binaries
133-
export PATH="$HOME/.local/bin:$PATH"
134-
```
193+
export PATH="$HOME/.local/bin:$PATH"`}</code>
194+
</pre>
135195

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>
137199

138-
```bash
139-
# ~/.zshenv — nvm initialization for non-interactive shells
200+
<pre>
201+
<code>{`# ~/.zshenv — nvm initialization for non-interactive shells
140202
export NVM_DIR="$HOME/.nvm"
141203
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" --no-use
142204
143205
# pyenv
144206
export PYENV_ROOT="$HOME/.pyenv"
145207
export PATH="$PYENV_ROOT/bin:$PATH"
146-
eval "$(pyenv init --path)"
147-
```
208+
eval "$(pyenv init --path)"`}</code>
209+
</pre>
148210

149-
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>
150214

151-
After saving `~/.zshenv`, verify without restarting your terminal:
215+
<p>
216+
After saving <code>{"~/.zshenv"}</code>, verify without restarting your terminal:
217+
</p>
152218

153-
```bash
154-
# Reload .zshenv in your current shell
219+
<pre>
220+
<code>{`# Reload .zshenv in your current shell
155221
source ~/.zshenv
156222
157223
# Confirm Claude Code's shell type now finds the tool
158224
zsh -c 'which gh'
159225
zsh -c 'which go'
160-
zsh -c 'which node'
161-
```
226+
zsh -c 'which node'`}</code>
227+
</pre>
162228

163-
## What to Keep in .zshrc vs .zshenv
229+
<h2>What to Keep in <code>{"~/.zshrc"}</code> vs <code>{"~/.zshenv"}</code></h2>
164230

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>
166234

167235
<div className="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>.
169237

170238
<p><strong>Keep in <code>{"~/.zshenv"}</code>:</strong></p>
171239
<ul>
@@ -187,31 +255,39 @@ Moving everything to `~/.zshenv` is not the right answer. Some configuration sho
187255
</ul>
188256
</div>
189257

190-
## The Broader Pattern: Any Tool That Spawns Subshells
258+
<h2>The Broader Pattern: Any Tool That Spawns Subshells</h2>
191259

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>
193263

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>
200272

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>
202276

203277
<Blockquote
204278
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?"
205279
/>
206280

207281
<KanvasCTA />
208282

209-
## Putting It Together: A Minimal ~/.zshenv Template
283+
<h2>Putting It Together: A Minimal <code>{"~/.zshenv"}</code> Template</h2>
210284

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>
212288

213-
```bash
214-
# ~/.zshenv
289+
<pre>
290+
<code>{`# ~/.zshenv
215291
# Sourced for ALL zsh shells — interactive, login, and non-interactive.
216292
# Keep this file fast and free of output-producing commands.
217293
@@ -244,31 +320,47 @@ fi
244320
# Editor and locale
245321
export EDITOR="vim"
246322
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>
249325

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>
251329

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>
255333

256-
All three should now resolve to their correct paths.
334+
<p>
335+
All three should now resolve to their correct paths.
336+
</p>
257337

258-
## Summary
338+
<h2>Summary</h2>
259339

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>
261343

262-
The fix is permanent and simple:
344+
<p>
345+
The fix is permanent and simple:
346+
</p>
263347

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>
267353

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>
269357

270-
---
358+
<hr />
271359

272-
*Exploring AI-assisted development workflows and developer tooling? The <Link to="/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 <Link to="/blog">Layer5 blog</Link> for more practical engineering posts.*
360+
<p>
361+
<em>
362+
Exploring AI-assisted development workflows and developer tooling? The <Link to="/community">Layer5 community</Link> is an active group of platform engineers, open source contributors, and DevOps practitioners. Join us on <a href="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 <Link to="/blog">Layer5 blog</Link> for more practical engineering posts.
363+
</em>
364+
</p>
273365

274366
</BlogWrapper>

0 commit comments

Comments
 (0)