Build1 publisher3 min readPublished
Claude Code's Bash tool hands every command to macOS zsh
On a Mac the tool named Bash calls zsh, and the GNU coreutils you put first on PATH in your profile never reach it, because the snapshot Claude Code replays before every call takes its PATH from Claude Code's own process.
The Engineer · Build desk

What happened
- Claude Code's Bash tool hands each command to the user's login shell, and on every Mac since Catalina that shell is zsh.
- With zsh's nomatch default, rm -f nope*.txt && echo never runs rm and returns non-zero, where the same line in bash ignores the missing file and runs the follow-up.
- A GNU-style sed -i 's/old/new/' exited 0 on macOS and left a config.yml-e file in the repo, because BSD sed reads the next argument as a backup suffix.
- Installing GNU tools and putting them first on PATH in a profile does not reach the tool, because the export PATH line in Claude Code's session snapshot comes from its own process environment.
- The post's fix uses CLAUDE_CODE_SHELL and SHELL in the env block of settings.json, plus a SessionStart hook that appends an export PATH to the file CLAUDE_ENV_FILE names.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- constraint Prompting cannot reach the sed case. The command exits 0, so nothing in the agent's feedback loop registers a failure, and the stray -e file is the only evidence anything went wrong.
- decision Teams standardising agent machines now own two files per developer, settings.json and whatever the SessionStart hook writes. The developer's own ~/.zprofile is not one of them.
- cost Until the config lands, every GNU-assuming command the model emits risks a retry the user pays for in session time, at roughly thirty seconds a round trip by the post's own count.
- exposure The fix rests on how one build of Claude Code assembles its snapshot. If that changes, the GNU PATH stops arriving and the first symptom is a command that behaves differently without erroring.
When an unmatched glob kills a command, the error comes back prefixed `(eval):1:`, which says the command went through `eval` and stops there [6]. Nothing in the failure text names the shell. The rest look like broken tools: `date: illegal time format`, `stat: illegal option -- c`, `sed: illegal option -- -` [7]. The dev.to post that traced this says both causes stay hidden from inside a session [9]. Each round trip costs about thirty seconds while the model apologises and writes a different command [26]. A tool whose description begins "Executes a bash command" [2] is running zsh.
Order of operations decides which fix works. Claude Code captures a shell snapshot when the session starts and replays it before every Bash call [11]. The `export PATH` line in that snapshot is written from Claude Code's own process environment, not from the shell it just captured, and the captured login shell is asked for its own PATH only on Windows [11]. Homebrew's GNU binaries can therefore sit first on PATH in every profile you own and never appear to the agent [10].
The post's author checked that instead of assuming it. A guard in the profile appended a directory and logged when it ran. In the capture shell the guard fired: `CLAUDECODE=1` was set, the profile executed, the directory was added [12]. The snapshot came out without the directory [12].
The `env` block of `settings.json` is read before the shell is chosen, so `CLAUDE_CODE_SHELL` takes effect where a profile cannot [13]. Set `SHELL` in the same block, or the model's own environment summary keeps telling it the shell is zsh [15]. A bare `bash` resolves to the first bash on PATH, which with Homebrew installed is 5.x, not Apple's 3.2 [14].
A `SessionStart` hook may append shell statements to the file that `CLAUDE_ENV_FILE` names, and Claude Code sources that file after the snapshot and before every Bash call, so a prepend written there takes effect on PATH [16]. The post prepends `/opt/homebrew/opt/coreutils/libexec/gnubin` [17]. Homebrew ships coreutils, findutils, gawk, gnu-sed, gnu-tar, gnu-which and grep with a `libexec/gnubin` directory holding the GNU builds under their plain names, `sed` and not `gsed` [18]. The install line names eight formulas, seven of which supply one of those directories [24].
None of this touches your own shell: `sed` still resolves to `/usr/bin/sed` in your terminal, and `gsed` and `gdate` still work the way you are used to [22].
This is one developer's measurement, on Claude Code 2.1.266 and macOS 26.6, in a session started with no inherited PATH, from a directory with and without the config [21]. For it to transfer you need a build that assembles the snapshot the same way, Homebrew installed at the prefix in that export line, and a codebase where the agent shells out often enough for BSD-versus-GNU to cost you anything. The post describes both settings as "documented-but-obscure" [20], and opens with a disclaimer: "AI helped me write this since I am not that good with words :)" [23].
What to watch
- Whether a Claude Code release after 2.1.266 changes how the snapshot's export PATH line is built, since the CLAUDE_ENV_FILE fix depends on that ordering.
- Whether the Bash tool's description is updated now that the shell it runs is configurable and is zsh by default on macOS.
- Whether anyone reproduces the profile-guard test at other Homebrew prefixes and on macOS builds older than 26.6.