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:

  1. On session start, it sources ~/.zshenv (which runs brew shellenv and sets up the full Homebrew PATH)
  2. It writes the resulting PATH value into $CLAUDE_ENV_FILE
  3. 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 -P or grep -oP (GNU only) — use sed -n or awk instead
  • readlink -f (GNU only on macOS) — use realpath or a shell function
  • sed -i '' vs sed -i (BSD vs GNU syntax differs)

Key Takeaways

  1. ~/.zshenv is ineffective inside Claude Code. Despite being the zsh startup file that’s supposed to run everywhere, Claude Code’s shell execution bypasses it.

  2. The env key in settings.json doesn’t reliably override PATH. It might work for other environment variables, but I couldn’t get it to work for PATH.

  3. $CLAUDE_ENV_FILE via a SessionStart hook is the correct mechanism. It’s the supported, reliable way to inject environment variables into Claude Code sessions.

  4. Write portable shell scripts. If your hooks run on macOS, avoid GNU-specific flags. BSD and GNU coreutils diverge in subtle, breaking ways.

  5. The real cost is per-session iteration waste. Without the fix, every session starts with the agent discovering gh is missing, then burning through iterations trying workarounds — raw curl commands 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.