[BUG] The Bash tool's userland is BSD on macOS and nothing says so — the model writes GNU sed/date/stat and patches around the failure

Status Open
Reported on v2.1.263
Maintainer reply None cached
Activity 0 comments · opened Sep 20, 2026

Summary

On macOS the Bash tool's userland is BSD, and nothing in the session says so. The model writes the GNU form — which is what almost all shell on the internet is — and the command fails, or worse, succeeds and does something else. The environment block reports the OS and the shell; it does not report that sed, date, stat, awk, tar, which and xargs are the BSD builds with different flags.

This is a separate defect from #91498, which is about the interpreter being the login shell rather than bash. This one survives every fix proposed there: set CLAUDE_CODE_SHELL=bash, or run it on a Mac whose login shell is already bash, and the tools are still BSD.

Repro

One Bash tool call on a stock Mac:

printf 'a\n' > /tmp/t.txt
sed -i 's/a/b/' /tmp/t.txt   # GNU: edits in place. BSD: creates /tmp/t.txt-e
ls /tmp/t.txt*
date -d yesterday +%F        # GNU: a date. BSD: error
stat -c %s /tmp/t.txt        # GNU: a size. BSD: error

Actual, via the Bash tool:

/tmp/t.txt  /tmp/t.txt-e
date: illegal time format
stat: illegal option -- c

sed -i is the one that matters most: it does not fail. It writes the edit and silently leaves a -e backup file beside it, because BSD sed -i reads the next argument as a suffix. A model that checks the exit status sees success.

Why the existing signals aren't sufficient

  • The environment block gives Platform: darwin and OS Version, which is the information needed to infer BSD userland, and empirically the inference doesn't happen reliably — see #77592, where the model emitted a GNU-only grep flag on a session that stated Darwin.
  • The tool description says nothing about which flavour of the standard tools is present. Its guidance is about which tool to prefer, not which dialect it speaks.
  • The failure gives no hint about the cause. sed: illegal option -- - doesn't say "this is BSD sed, GNU sed would have accepted this", so the model patches around the symptom rather than switching to the portable form.

The same cause has been reported at least twice as one-off instances

  • #79056 — Claude wraps commands in timeout/gtimeout, which macOS doesn't ship. Itself a re-file of locked #3361 and #10600.
  • #77592 — Sonnet 4.6 emitted a GNU grep flag on a Darwin session.

Each was handled as a specific tool. The general shape is the same in all of them, and I'd suggest it's worth addressing as one thing rather than per-command.

A user can't straightforwardly fix this themselves

Putting PATH exports in ~/.zprofile or ~/.bashrc has no effect on the Bash tool. Claude Code writes a shell snapshot at session start and replays it before every Bash call, and the export PATH line in that snapshot is written from Claude Code's own process environment, not from the shell the snapshot was captured in — the login shell it captures in is asked for its PATH only on Windows. Measured on 2.1.263: the capture shell had CLAUDECODE=1 and a profile guard fired in it, and the snapshot still came out without the directory it added.

What does work is CLAUDE_ENV_FILE. A SessionStart hook may append shell statements to the file that variable names, and Claude Code sources that file after the snapshot, before every Bash call, so a PATH prepend written there wins:

export PATH="/opt/homebrew/opt/coreutils/libexec/gnubin:...:${PATH}"

Homebrew ships coreutils, findutils, gawk, gnu-sed, gnu-tar, gnu-which and grep with a libexec/gnubin directory holding the builds under their plain names, so nothing outside a Claude Code session changes — the developer's own terminal keeps the macOS tools.

Measured on 2.1.266, macOS 26.6, in a session started without an inherited PATH, from a directory with and without that hook:

| | Without | With |
| :-- | :-- | :-- |
| sed --version | sed: illegal option -- - | GNU sed 4.10 |
| date -d yesterday +%F | error | 2026-09-08 |
| awk --version | error | GNU Awk 5.4.1 |

That it takes a documented-but-obscure hook variable, and that the two obvious places to put a PATH don't work, is most of why I'm filing this rather than just fixing my own setup.

Suggested fix

Any one of these would remove the class:

  1. State the userland in the environment block on Darwin — one line, e.g. Userland: BSD (sed, date, stat, awk, tar are not GNU). Cheapest, and it targets the actual gap: the model has the OS but doesn't act on it.
  2. Prefer GNU builds when they are present. If $(brew --prefix)/opt/*/libexec/gnubin exists, put it on the Bash tool's PATH.
  3. Say it in the Bash tool description, the way the shell initialization note is there now.

Workaround

I packaged 1 and 2 as hooks, with the CLAUDE_CODE_SHELL fix from #91498 alongside, since in practice you hit both: https://github.com/Baune8D/claude-code-macos

Environment

  • macOS 26.6, Apple silicon, Homebrew at /opt/homebrew, login shell zsh
  • Claude Code 2.1.266 for the table above, 2.1.263 for the snapshot measurement

View original on GitHub ↗