Skip to content

Commit 11bee19

Browse files
committed
Update dependencies and improve blog content for clarity
Signed-off-by: Yi Nuo <218099172+yi-nuo426@users.noreply.github.com>
1 parent 14d24a6 commit 11bee19

3 files changed

Lines changed: 69 additions & 112 deletions

File tree

package-lock.json

Lines changed: 11 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@
128128
"@eslint/eslintrc": "^3.3.3",
129129
"@eslint/js": "^9.39.2",
130130
"babel-plugin-module-resolver": "^5.0.0",
131-
"baseline-browser-mapping": "^2.8.32",
132131
"cpx": "^1.5.0",
133132
"cross-env": "^10.0.0",
134133
"env-cmd": "^10.1.0",

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

Lines changed: 58 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -22,37 +22,25 @@ resource: true
2222
import { BlogWrapper } from "../../Blog.style.js";
2323
import { Link } from "gatsby";
2424
import Blockquote from "../../../../reusecore/Blockquote";
25-
import CTA_FullWidth from "../../../../components/Call-To-Actions/CTA_FullWidth";
26-
import CTAImg from "../../../../assets/images/layer5/5 icon/png/light/5-light-no-trim.webp";
25+
import KanvasCTA from "../../../../sections/Kanvas/kanvas-cta";
2726

2827
<BlogWrapper>
2928

30-
<div class="intro">
29+
<div className="intro">
3130
<p>
32-
You type <code>gh pr list</code> in your terminal and it works perfectly. You ask Claude Code to
33-
do the same, and it replies: <em>"command not found: gh"</em>. Your Go toolchain, nvm, pyenv,
34-
Homebrew binaries — all invisible to the agent. This is not a bug in Claude Code. It is a
35-
fundamental property of how Unix shells start up, and once you understand it, the fix is a
36-
one-time five-minute change.
31+
You type <code>gh pr list</code> in your terminal and it works perfectly. You ask Claude Code to do the same, and it replies: <em>"command not found: gh"</em>. Your Go toolchain, nvm, pyenv, Homebrew binaries — all invisible to the agent. This is not a bug in Claude Code. It is a fundamental property of how Unix shells start up, and once you understand it, the fix is a one-time, five-minute change.
3732
</p>
3833
</div>
3934

4035
## The Surprise
4136

42-
AI coding assistants like Claude Code, Cline, Aider, and similar tools spawn child shell processes
43-
to run commands on your behalf. From where you sit, the terminal looks identical to the one you use
44-
every day. But the shell those tools launch is fundamentally different from the one you interact
45-
with — and that difference determines which startup files are read, which means it determines what
46-
is on your PATH.
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.
4738

48-
The result is a confusing experience: tools you have used for years are suddenly invisible to the
49-
agent trying to help you. The culprit is not your installation. It is the shell startup file
50-
hierarchy.
39+
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.
5140

5241
## Zsh Startup Files and When They Are Sourced
5342

54-
Zsh loads different configuration files depending on how it was launched. There are four main files,
55-
and they are read in this order:
43+
Zsh loads different configuration files depending on how it was launched. There are four main files, and they are read in this order:
5644

5745
| File | Login shell | Interactive shell | Non-interactive shell |
5846
|---|---|---|---|
@@ -61,25 +49,16 @@ and they are read in this order:
6149
| `~/.zshrc` | No | Yes | No |
6250
| `~/.zlogin` | Yes | No | No |
6351

64-
The key column is the last one. A **non-interactive, non-login shell** — the kind that Claude Code
65-
spawns — sources **only** `~/.zshenv`. Everything else is skipped entirely.
66-
67-
- **`~/.zshenv`** is sourced for every zsh invocation, no matter what. It is the right place for
68-
environment variables that must be available universally: `PATH`, `GOPATH`, `JAVA_HOME`, and
69-
similar exports.
70-
- **`~/.zprofile`** is sourced for login shells (e.g., when you open a new terminal window or SSH
71-
into a machine). Homebrew places its environment setup here on Apple Silicon Macs
72-
(`/opt/homebrew/bin/brew shellenv`), which is why Homebrew tools can also go missing.
73-
- **`~/.zshrc`** is sourced only for interactive shells — sessions where you type commands. This is
74-
where most developers put everything: aliases, prompt configuration, `nvm`, `pyenv`, `rbenv`,
52+
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.
53+
54+
- **`~/.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.
55+
- **`~/.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.
56+
- **`~/.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`,
7557
completions, and — critically — PATH customizations.
76-
- **`~/.zlogin`** is sourced after `~/.zshrc` for login shells. It is rarely used by developers
77-
directly.
58+
- **`~/.zlogin`** is sourced after `~/.zshrc` for login shells. It is rarely used by developers directly.
7859

7960
<Blockquote
8061
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."
81-
person="Zsh Documentation"
82-
title="Shell Startup File Hierarchy"
8362
/>
8463

8564
## What PATH a Non-Interactive Shell Actually Sees
@@ -130,16 +109,13 @@ The `-i` flag forces an interactive shell, which sources `~/.zshrc`. If `zsh -c
130109
`gh not found` but `zsh -i -c 'which gh'` prints the correct path, your PATH export is in
131110
`~/.zshrc` and you have confirmed the root cause.
132111

133-
<div class="note">
134-
<strong>Quick diagnosis:</strong> Run <code>zsh -c 'which gh'</code> (no <code>-i</code> flag).
135-
If this fails but <code>which gh</code> in your normal terminal works, your PATH is only set in
136-
<code>~/.zshrc</code>. Move the relevant exports to <code>~/.zshenv</code> to fix it.
112+
<div className="note">
113+
<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.
137114
</div>
138115

139116
## The Fix: Move PATH Exports to ~/.zshenv
140117

141-
The solution is straightforward: any environment variable that must be visible to all processes —
142-
including non-interactive subshells — belongs in `~/.zshenv`, not `~/.zshrc`.
118+
The solution is straightforward: any environment variable that must be visible to all processes — including non-interactive subshells — belongs in `~/.zshenv`, not `~/.zshrc`.
143119

144120
Open (or create) `~/.zshenv` and add your PATH exports there:
145121

@@ -158,8 +134,7 @@ export PATH="$GOPATH/bin:$PATH"
158134
export PATH="$HOME/.local/bin:$PATH"
159135
```
160136

161-
For tools that inject themselves via an eval expression in `~/.zshrc` — such as nvm, pyenv, or
162-
rbenv — you need to move or duplicate that initialization into `~/.zshenv` as well:
137+
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:
163138

164139
```bash
165140
# ~/.zshenv — nvm initialization for non-interactive shells
@@ -172,9 +147,7 @@ export PATH="$PYENV_ROOT/bin:$PATH"
172147
eval "$(pyenv init --path)"
173148
```
174149

175-
Note the `--no-use` flag for nvm: it initializes nvm without switching to the default Node.js
176-
version, which speeds up shell startup for non-interactive contexts. Remove it if you want the
177-
default version active everywhere.
150+
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.
178151

179152
After saving `~/.zshenv`, verify without restarting your terminal:
180153

@@ -190,61 +163,57 @@ zsh -c 'which node'
190163

191164
## What to Keep in .zshrc vs .zshenv
192165

193-
Moving everything to `~/.zshenv` is not the right answer. Some configuration should stay in
194-
`~/.zshrc` because it only makes sense in interactive contexts or because it has side effects
195-
that slow down non-interactive shells unnecessarily.
196-
197-
<div class="tip">
198-
<strong>Guiding principle:</strong> If it is an environment variable that a program needs to
199-
find another program, it belongs in <code>~/.zshenv</code>. If it is a user-facing customization
200-
for your interactive terminal experience, it belongs in <code>~/.zshrc</code>.
201-
202-
**Keep in `~/.zshenv`:**
203-
- `PATH` exports and modifications
204-
- `GOPATH`, `JAVA_HOME`, `PYTHONPATH`, `CARGO_HOME`, and similar tool-specific env vars
205-
- `NVM_DIR`, `PYENV_ROOT`, `RBENV_ROOT` and their `PATH` injections
206-
- `EDITOR`, `PAGER`, `LANG`, `LC_ALL`
207-
208-
**Keep in `~/.zshrc`:**
209-
- Shell aliases (`alias ll='ls -la'`)
210-
- Prompt configuration (Starship, Powerlevel10k, oh-my-zsh)
211-
- Tab completion setup
212-
- Shell functions for interactive use
213-
- History settings
214-
- `zsh` plugins and plugin managers
215-
- Anything that prints output (welcome messages, `neofetch`, etc.)
166+
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.
167+
168+
<div className="tip">
169+
<p>
170+
<strong>Guiding principle:</strong> If it is an environment variable that a program needs to
171+
find another program, it belongs in <code>{"~/.zshenv"}</code>. If it is a user-facing customization
172+
for your interactive terminal experience, it belongs in <code>{"~/.zshrc"}</code>.
173+
</p>
174+
175+
<p><strong>Keep in <code>{"~/.zshenv"}</code>:</strong></p>
176+
<ul>
177+
<li><code>PATH</code> exports and modifications</li>
178+
<li><code>GOPATH</code>, <code>JAVA_HOME</code>, <code>PYTHONPATH</code>, <code>CARGO_HOME</code>, and similar tool-specific env vars</li>
179+
<li><code>NVM_DIR</code>, <code>PYENV_ROOT</code>, <code>RBENV_ROOT</code> and their <code>PATH</code> injections</li>
180+
<li><code>EDITOR</code>, <code>PAGER</code>, <code>LANG</code>, <code>LC_ALL</code></li>
181+
</ul>
182+
183+
<p><strong>Keep in <code>{"~/.zshrc"}</code>:</strong></p>
184+
<ul>
185+
<li>Shell aliases (<code>alias ll='ls -la'</code>)</li>
186+
<li>Prompt configuration (Starship, Powerlevel10k, oh-my-zsh)</li>
187+
<li>Tab completion setup</li>
188+
<li>Shell functions for interactive use</li>
189+
<li>History settings</li>
190+
<li><code>zsh</code> plugins and plugin managers</li>
191+
<li>Anything that prints output (welcome messages, <code>neofetch</code>, etc.)</li>
192+
</ul>
216193
</div>
217194

218195
## The Broader Pattern: Any Tool That Spawns Subshells
219196

220-
Claude Code is not unique here. This same behavior affects any process that spawns a child shell
221-
without the `-i` or `-l` flags:
197+
Claude Code is not unique here. This same behavior affects any process that spawns a child shell without the `-i` or `-l` flags:
222198

223-
- **CI/CD pipelines** (GitHub Actions, GitLab CI) run commands in non-interactive shells. This is
224-
why you often see pipelines that explicitly `source ~/.bashrc` or set up PATH at the top of
225-
every job.
226-
- **Cron jobs** run in minimal environments with almost no PATH set.
227-
- **VS Code integrated terminal tasks** and `launch.json` configurations may use non-interactive
228-
shells depending on the operating system and configuration.
229-
- **SSH remote command execution** (`ssh host 'command'`) uses a non-interactive shell unless you
230-
pass `-t` to force a TTY.
231-
- **Make** and other build systems that shell out to run commands.
232-
- **Docker `RUN` instructions** in Dockerfiles.
199+
- 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.
200+
- Cron jobs run in minimal environments with almost no PATH set.
201+
- VS Code integrated terminal tasks and `launch.json` configurations may use non-interactive shells depending on the operating system and configuration.
202+
- SSH remote command execution (`ssh host 'command'`) uses a non-interactive shell unless you pass `-t` to force a TTY.
203+
- Make and other build systems that shell out to run commands.
204+
- Docker `RUN` instructions in Dockerfiles.
233205

234-
If you have ever fixed a "works on my machine" problem by adding `export PATH=...` to a CI
235-
configuration or a Dockerfile, you have already solved the same class of problem. The `~/.zshenv`
236-
fix is just the developer workstation equivalent.
206+
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.
237207

238208
<Blockquote
239209
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?"
240-
person="Platform Engineering"
241-
title="Debugging Shell Environment Issues"
242210
/>
243211

212+
<KanvasCTA />
213+
244214
## Putting It Together: A Minimal ~/.zshenv Template
245215

246-
Here is a starting point for a `~/.zshenv` that covers the most common developer tools on macOS.
247-
Adjust paths to match your actual installations:
216+
Here is a starting point for a `~/.zshenv` that covers the most common developer tools on macOS. Adjust paths to match your actual installations:
248217

249218
```bash
250219
# ~/.zshenv
@@ -283,8 +252,7 @@ export LANG="en_US.UTF-8"
283252
export LC_ALL="en_US.UTF-8"
284253
```
285254

286-
With this in place, restart Claude Code (or any tool that spawns subshells) and run your
287-
verification:
255+
With this in place, restart Claude Code (or any tool that spawns subshells) and run your verification:
288256

289257
```bash
290258
zsh -c 'which gh && which go && which node'
@@ -294,33 +262,18 @@ All three should now resolve to their correct paths.
294262

295263
## Summary
296264

297-
The "command not found" error in Claude Code and similar AI coding assistants is a shell startup
298-
file problem, not a tool installation problem. Zsh only sources `~/.zshenv` for non-interactive,
299-
non-login shells. Everything most developers have placed in `~/.zshrc` — including PATH exports,
300-
version manager initializations, and tool-specific environment variables — is invisible to those
301-
shells.
265+
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.
302266

303267
The fix is permanent and simple:
304268

305269
1. Move PATH exports and tool-specific environment variables to `~/.zshenv`.
306270
2. Verify with `zsh -c 'which <tool>'` before and after.
307271
3. Keep interactive customizations (aliases, prompt, completions) in `~/.zshrc`.
308272

309-
The same fix benefits CI pipelines, cron jobs, Makefiles, Docker builds, and any other context
310-
where commands run in a non-interactive shell environment.
273+
The same fix benefits CI pipelines, cron jobs, Makefiles, Docker builds, and any other context where commands run in a non-interactive shell environment.
311274

312275
---
313276

314277
*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.*
315278

316-
<CTA_FullWidth
317-
image={CTAImg}
318-
heading="Join the Layer5 Community"
319-
alt="Layer5 Community"
320-
content="Connect with platform engineers, DevOps practitioners, and open source contributors who are building the future of cloud native infrastructure."
321-
button_text="Join the Community"
322-
url="/community"
323-
external_link={false}
324-
/>
325-
326279
</BlogWrapper>

0 commit comments

Comments
 (0)