-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Blog: Why claude code can't find your tools #7457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
14d24a6
Blog: Why claude code can't find your tools
yi-nuo426 1956af4
Apply suggestions from code review
pontusringblom b45eb5e
Apply suggestion from @pontusringblom
pontusringblom 11bee19
Update dependencies and improve blog content for clarity
yi-nuo426 9e4c916
Merge branch 'blog/claude/non-interactive-shell' of https://github.co…
yi-nuo426 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+9.07 MB
...blog/2026/03-09-why-claude-code-cant-find-your-tools/claude-code-shell-path.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
274 changes: 274 additions & 0 deletions
274
src/collections/blog/2026/03-09-why-claude-code-cant-find-your-tools/index.mdx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,274 @@ | ||
| --- | ||
| title: "Why Claude Code Can't Find Your Tools" | ||
| subtitle: "Understanding non-interactive shells and the .zshenv fix" | ||
| date: 2026-03-09 10:00:00 -0530 | ||
| author: Layer5 Team | ||
| thumbnail: ./claude-code-shell-path.png | ||
| darkthumbnail: ./claude-code-shell-path.png | ||
| description: "Claude Code runs in a non-interactive shell that never sources .zshrc. Learn why tools vanish and how moving PATH exports to .zshenv fixes it for good." | ||
| type: Blog | ||
| category: Engineering | ||
| tags: | ||
| - Engineering | ||
| - ai | ||
| - Open Source | ||
| featured: false | ||
| published: true | ||
| resource: true | ||
| --- | ||
|
|
||
|
|
||
| import { BlogWrapper } from "../../Blog.style.js"; | ||
| import { Link } from "gatsby"; | ||
| import Blockquote from "../../../../reusecore/Blockquote"; | ||
| import KanvasCTA from "../../../../sections/Kanvas/kanvas-cta"; | ||
|
|
||
| <BlogWrapper> | ||
|
|
||
| <div className="intro"> | ||
| <p> | ||
| 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. | ||
| </p> | ||
| </div> | ||
|
|
||
| ## The Surprise | ||
|
|
||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| ## Zsh Startup Files and When They Are Sourced | ||
|
|
||
| Zsh loads different configuration files depending on how it was launched. There are four main files, and they are read in this order: | ||
|
|
||
| | File | Login shell | Interactive shell | Non-interactive shell | | ||
| |---|---|---|---| | ||
| | `~/.zshenv` | Yes | Yes | Yes | | ||
| | `~/.zprofile` | Yes | No | No | | ||
| | `~/.zshrc` | No | Yes | No | | ||
| | `~/.zlogin` | Yes | No | No | | ||
|
leecalcote marked this conversation as resolved.
|
||
|
|
||
| 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. | ||
|
|
||
| - **`~/.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. | ||
| - **`~/.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. | ||
| - **`~/.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`, | ||
| completions, and — critically — PATH customizations. | ||
| - **`~/.zlogin`** is sourced after `~/.zshrc` for login shells. It is rarely used by developers directly. | ||
|
|
||
| <Blockquote | ||
| 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." | ||
| /> | ||
|
|
||
| ## What PATH a Non-Interactive Shell Actually Sees | ||
|
|
||
| You can observe this yourself. Run these two commands and compare the output: | ||
|
|
||
| ```bash | ||
| # What a non-interactive shell sees | ||
| zsh -c 'echo $PATH' | ||
|
|
||
| # What your interactive shell sees | ||
| echo $PATH | ||
| ``` | ||
|
|
||
| On a typical macOS developer machine, the non-interactive shell might produce something like: | ||
|
|
||
| ``` | ||
| /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin | ||
| ``` | ||
|
|
||
| While your interactive shell shows something far richer: | ||
|
|
||
| ``` | ||
| /opt/homebrew/bin:/opt/homebrew/sbin:/Users/you/.nvm/versions/node/v20.11.0/bin: | ||
| /Users/you/go/bin:/Users/you/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin | ||
| ``` | ||
|
|
||
| All those extra paths — Homebrew, nvm, Go binaries — live in `~/.zshrc`, `~/.zprofile`, or in | ||
| scripts those files source. A non-interactive shell never reads any of them. | ||
|
|
||
| ## Diagnosing the Problem | ||
|
|
||
| Before changing anything, confirm the symptom. Use `zsh -c` to simulate what Claude Code sees: | ||
|
|
||
| ```bash | ||
| # Does Claude Code's shell see the tool? | ||
| zsh -c 'which gh' | ||
| zsh -c 'which go' | ||
| zsh -c 'which node' | ||
|
|
||
| # Does your interactive shell see it? | ||
| zsh -i -c 'which gh' | ||
| zsh -i -c 'which go' | ||
| zsh -i -c 'which node' | ||
| ``` | ||
|
|
||
| The `-i` flag forces an interactive shell, which sources `~/.zshrc`. If `zsh -c 'which gh'` prints | ||
| `gh not found` but `zsh -i -c 'which gh'` prints the correct path, your PATH export is in | ||
| `~/.zshrc` and you have confirmed the root cause. | ||
|
|
||
| <div className="note"> | ||
| <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. | ||
| </div> | ||
|
|
||
| ## The Fix: Move PATH Exports to ~/.zshenv | ||
|
|
||
| The solution is straightforward: any environment variable that must be visible to all processes — including non-interactive subshells — belongs in `~/.zshenv`, not `~/.zshrc`. | ||
|
|
||
| Open (or create) `~/.zshenv` and add your PATH exports there: | ||
|
|
||
| ```bash | ||
| # ~/.zshenv | ||
| # Sourced for every zsh invocation — interactive, login, and non-interactive alike | ||
|
|
||
| # Homebrew (Apple Silicon) | ||
| export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH" | ||
|
|
||
| # Go | ||
| export GOPATH="$HOME/go" | ||
| export PATH="$GOPATH/bin:$PATH" | ||
|
|
||
| # Local binaries | ||
| export PATH="$HOME/.local/bin:$PATH" | ||
| ``` | ||
|
|
||
| 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: | ||
|
|
||
| ```bash | ||
| # ~/.zshenv — nvm initialization for non-interactive shells | ||
| export NVM_DIR="$HOME/.nvm" | ||
| [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" --no-use | ||
|
|
||
| # pyenv | ||
| export PYENV_ROOT="$HOME/.pyenv" | ||
| export PATH="$PYENV_ROOT/bin:$PATH" | ||
| eval "$(pyenv init --path)" | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| After saving `~/.zshenv`, verify without restarting your terminal: | ||
|
|
||
| ```bash | ||
| # Reload .zshenv in your current shell | ||
| source ~/.zshenv | ||
|
|
||
| # Confirm Claude Code's shell type now finds the tool | ||
| zsh -c 'which gh' | ||
| zsh -c 'which go' | ||
| zsh -c 'which node' | ||
| ``` | ||
|
|
||
| ## What to Keep in .zshrc vs .zshenv | ||
|
|
||
| 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. | ||
|
|
||
| <div className="tip"> | ||
| <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>. | ||
|
|
||
| <p><strong>Keep in <code>{"~/.zshenv"}</code>:</strong></p> | ||
| <ul> | ||
| <li><code>PATH</code> exports and modifications</li> | ||
| <li><code>GOPATH</code>, <code>JAVA_HOME</code>, <code>PYTHONPATH</code>, <code>CARGO_HOME</code>, and similar tool-specific env vars</li> | ||
| <li><code>NVM_DIR</code>, <code>PYENV_ROOT</code>, <code>RBENV_ROOT</code> and their <code>PATH</code> injections</li> | ||
| <li><code>EDITOR</code>, <code>PAGER</code>, <code>LANG</code>, <code>LC_ALL</code></li> | ||
| </ul> | ||
|
|
||
| <p><strong>Keep in <code>{"~/.zshrc"}</code>:</strong></p> | ||
| <ul> | ||
| <li>Shell aliases (<code>alias ll='ls -la'</code>)</li> | ||
| <li>Prompt configuration (Starship, Powerlevel10k, oh-my-zsh)</li> | ||
| <li>Tab completion setup</li> | ||
| <li>Shell functions for interactive use</li> | ||
| <li>History settings</li> | ||
| <li><code>zsh</code> plugins and plugin managers</li> | ||
| <li>Anything that prints output (welcome messages, <code>neofetch</code>, etc.)</li> | ||
| </ul> | ||
| </div> | ||
|
|
||
| ## The Broader Pattern: Any Tool That Spawns Subshells | ||
|
|
||
| Claude Code is not unique here. This same behavior affects any process that spawns a child shell without the `-i` or `-l` flags: | ||
|
|
||
| - 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. | ||
| - Cron jobs run in minimal environments with almost no PATH set. | ||
| - VS Code integrated terminal tasks and `launch.json` configurations may use non-interactive shells depending on the operating system and configuration. | ||
| - SSH remote command execution (`ssh host 'command'`) uses a non-interactive shell unless you pass `-t` to force a TTY. | ||
| - Make and other build systems that shell out to run commands. | ||
| - Docker `RUN` instructions in Dockerfiles. | ||
|
|
||
| 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. | ||
|
|
||
| <Blockquote | ||
| 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?" | ||
| /> | ||
|
|
||
| <KanvasCTA /> | ||
|
|
||
| ## Putting It Together: A Minimal ~/.zshenv Template | ||
|
|
||
| Here is a starting point for a `~/.zshenv` that covers the most common developer tools on macOS. Adjust paths to match your actual installations: | ||
|
|
||
| ```bash | ||
| # ~/.zshenv | ||
| # Sourced for ALL zsh shells — interactive, login, and non-interactive. | ||
| # Keep this file fast and free of output-producing commands. | ||
|
|
||
| # Homebrew (Apple Silicon Mac — change to /usr/local for Intel) | ||
| if [[ -x /opt/homebrew/bin/brew ]]; then | ||
| eval "$(/opt/homebrew/bin/brew shellenv)" | ||
| fi | ||
|
|
||
| # Go | ||
| export GOPATH="$HOME/go" | ||
| export PATH="$GOPATH/bin:$PATH" | ||
|
|
||
| # Rust / Cargo | ||
| export PATH="$HOME/.cargo/bin:$PATH" | ||
|
|
||
| # Local user binaries | ||
| export PATH="$HOME/.local/bin:$PATH" | ||
|
|
||
| # nvm (initialize without switching to default version) | ||
| export NVM_DIR="$HOME/.nvm" | ||
| [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" --no-use | ||
|
|
||
| # pyenv | ||
| if command -v pyenv &>/dev/null || [[ -d "$HOME/.pyenv" ]]; then | ||
| export PYENV_ROOT="$HOME/.pyenv" | ||
| export PATH="$PYENV_ROOT/bin:$PATH" | ||
| eval "$(pyenv init --path)" | ||
| fi | ||
|
|
||
| # Editor and locale | ||
| export EDITOR="vim" | ||
| export LANG="en_US.UTF-8" | ||
| export LC_ALL="en_US.UTF-8" | ||
| ``` | ||
|
|
||
| With this in place, restart Claude Code (or any tool that spawns subshells) and run your verification: | ||
|
|
||
| ```bash | ||
| zsh -c 'which gh && which go && which node' | ||
| ``` | ||
|
|
||
| All three should now resolve to their correct paths. | ||
|
|
||
| ## Summary | ||
|
|
||
| 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. | ||
|
|
||
| The fix is permanent and simple: | ||
|
|
||
| 1. Move PATH exports and tool-specific environment variables to `~/.zshenv`. | ||
| 2. Verify with `zsh -c 'which <tool>'` before and after. | ||
| 3. Keep interactive customizations (aliases, prompt, completions) in `~/.zshrc`. | ||
|
|
||
| The same fix benefits CI pipelines, cron jobs, Makefiles, Docker builds, and any other context where commands run in a non-interactive shell environment. | ||
|
|
||
| --- | ||
|
|
||
| *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.* | ||
|
|
||
| </BlogWrapper> | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.