Every Claude Code session started the same way: the agent would try to run gh pr create, get “command not found,” and then spend a dozen iterations trying to work around it — constructing raw curl commands to the GitHub API, attempting to locate the binary manually, or suggesting I install tools that were already installed. Multiply that by every session, and the wasted effort adds up fast.
The root cause took about 30 minutes to track down. The PATH inside worktree sessions was stripped to /usr/bin:/bin:/usr/sbin:/sbin — no /opt/homebrew/bin, no /opt/homebrew/sbin, nothing Homebrew-installed. Here’s what I tried, what didn’t work, and the fix that actually stuck.
The Environment
- macOS on Apple Silicon (Darwin arm64)
- zsh shell
- Homebrew installed at
/opt/homebrew - Claude Code CLI, using the git worktree feature for isolated agent sessions
The worktree feature is great — it gives each agent an isolated copy of your repo. But the shell environment inside those sessions was bare.
What Didn’t Work
Attempt 1: ~/.zshenv
The standard zsh advice: put your PATH setup in ~/.zshenv because zsh sources it for all invocations — interactive, login, non-interactive, scripts, everything. A previous Claude Code session had already created this file:
# ~/.zshenv
eval "$(/opt/homebrew/bin/brew shellenv)"
This works perfectly in a normal terminal. Inside Claude Code? No effect. Despite $0 reporting /bin/zsh, Claude Code’s Bash tool appears to launch zsh with flags that skip startup files, or otherwise bypasses the standard zsh initialization chain. The one file that’s supposed to always get sourced… doesn’t.
Attempt 2: The env Key in settings.json
Claude Code’s documentation describes an env setting for injecting environment variables:
{
"env": {
"PATH": "/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
}
}
Added this to ~/.claude/settings.json, fully restarted Claude Code, started a fresh session. The PATH remained unchanged at the minimal system default. I tried variations — quoting, not quoting, referencing $PATH, hardcoding the full value. None of it took.
I can’t say definitively whether this is a bug or a misunderstanding of the feature’s scope, but the env key did not reliably override PATH in my setup.
What Actually Works: SessionStart Hook + $CLAUDE_ENV_FILE
The fix uses two Claude Code mechanisms together: SessionStart hooks and the $CLAUDE_ENV_FILE.
$CLAUDE_ENV_FILE is a special file that Claude Code sources before each Bash tool invocation. Write environment variables there, and they persist across every command in the session. SessionStart hooks run once when a session begins — including worktree sessions.
Add this to ~/.claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "source ~/.zshenv 2>/dev/null; echo \"export PATH=\\\"$PATH\\\"\" >> \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}
What this does:
- On session start, it sources
~/.zshenv(which runsbrew shellenvand sets up the full Homebrew PATH) - It writes the resulting
PATHvalue into$CLAUDE_ENV_FILE - Claude Code picks up that file and applies the PATH to every subsequent Bash tool call
After adding this, gh, gpg, and every other Homebrew-installed tool worked immediately in the next session.
Bonus: Watch for GNU-isms in Your Hook Scripts
Once the PATH was fixed, a different problem surfaced. The project had a SessionStart hook script (scripts/session-start.sh) for JDK setup that used grep -oP — the -P flag enables Perl-compatible regex, which is a GNU grep extension. macOS ships with BSD grep, which doesn’t support it.
The fix: replace GNU-specific grep -oP patterns with portable sed -n equivalents. If your hooks need to run on both macOS and Linux, stick to POSIX-compatible commands, or at least avoid these common traps:
grep -Porgrep -oP(GNU only) — usesed -norawkinsteadreadlink -f(GNU only on macOS) — userealpathor a shell functionsed -i ''vssed -i(BSD vs GNU syntax differs)
Key Takeaways
-
~/.zshenvis ineffective inside Claude Code. Despite being the zsh startup file that’s supposed to run everywhere, Claude Code’s shell execution bypasses it. -
The
envkey insettings.jsondoesn’t reliably override PATH. It might work for other environment variables, but I couldn’t get it to work for PATH. -
$CLAUDE_ENV_FILEvia a SessionStart hook is the correct mechanism. It’s the supported, reliable way to inject environment variables into Claude Code sessions. -
Write portable shell scripts. If your hooks run on macOS, avoid GNU-specific flags. BSD and GNU coreutils diverge in subtle, breaking ways.
-
The real cost is per-session iteration waste. Without the fix, every session starts with the agent discovering
ghis missing, then burning through iterations trying workarounds — rawcurlcommands to the GitHub API, manual binary searches, increasingly convoluted shell one-liners. It’s not just one bad session; it’s every session, every time.
The whole issue comes down to Claude Code having its own shell execution model that doesn’t follow the conventions most developers rely on. Once you know that $CLAUDE_ENV_FILE is the intended mechanism, the fix is straightforward. Getting to that point — and stopping the per-session churn — is the hard part.