最新资讯
dev.to 为 CI/CD 构建 AI 质量门禁 dev.to Kakao 发布面向 AI Agent 的决策模型 Kanana-2-v-decide computerworld.com Microsoft 对 Copilot Chat 企业用户扩大未公开的使用限制 pub.towardsai.net Google 发布 Gemini 4 Carbon:主打网络安全能力的前沿模型 gradientflow.com METR 研究:AI 正在加速构建 AI 的团队本身 www.infoq.com GitLab 19.5 将 Agentic 开发纳入 DevOps 平台的治理体系 aiacceleratorinstitute.com 研究:AI Agent 的复杂流程编排可能反而是问题所在 pub.towardsai.net MongoDB 推出 Vercel AI SDK 集成:为 AI 应用提供持久记忆 thenewstack.io Google 捐出被 Anthropic 和 OpenAI 使用的代码沙箱 gVisor,押注 AI Agent 基础设施开源 dev.to 在信任 AI 生成的测试用例前,如何逐条复核 unite.ai Franklin Templeton 构建 AI 研究工作流以揭示风险 techxplore.com AI 助力寻找更优的钛合金 3D 打印工艺参数
多元动态
登录 注册
AI编码 dev.to · @Morten Larsen

为什么 Claude Code 总写出在 Mac 上失败的 shell 命令

原文 我的收藏

文章以 sed -i 's/old/new/' config.yml 为例:该命令在多数 StackOverflow 答案、博客和 Dockerfile 中常见,退出码为 0,但在 macOS 上语义不同,会在仓库中留下 config.yml-e 文件;更明显的失败包括 date: illegal time format、stat: illegal option -- c、sed: illegal option -- - 等。作者认为这并非模型粗心,而是有两个具体且可修复的原因,且从会话内部无法看到,但正文仅展开到问题与原因部分。

AI推荐理由 具体分析 Claude Code 在 macOS 上生成 GNU 风格 shell 命令而失败的原因,给出可复现的报错样例,对使用编码 Agent 的开发者有实用价值;但正文仅展示问题与部分原因,未给出完整修复方案。

68

正文

Disclaimer: AI helped me write this since I am not that good with words :) Claude wrote this, mid-task, while refactoring something unrelated: sed -i 's/old/new/' config.yml Enter fullscreen mode Exit fullscreen mode It's correct. It's the form you'll find in almost every StackOverflow answer, every blog post, every Dockerfile. It exited 0. And it left a file called config.yml-e sitting in my repo, because on macOS that command means something else entirely. That's the good case, the one that leaves evidence. The louder failures look like this: date: illegal time format stat: illegal option -- c sed: illegal option -- - (eval):1: no matches found: nope*.txt Enter fullscreen mode Exit fullscreen mode If you've used Claude Code on a Mac for more than a week, you've seen some of these. Claude writes a command, it fails, Claude apologises and writes a different one, and you lose thirty seconds and a bit of trust. It happens often enough to feel like the model being sloppy. It isn't. There are two concrete, fixable reasons, and neither one is visible from inside a session. Reason 1: the Bash tool is not bash Claude Code's Bash tool runs your login shell. On every Mac since Catalina, that's zsh. So the tool named Bash, whose description begins "Executes a bash command," is handing your commands to zsh. Everything the model knows about bash — mapfile, ${x^^}, word splitting, how an unmatched glob behaves — is subtly wrong. That last one is a good example of how quiet this gets: rm -f nope*.txt && echo "FOLLOW-UP RAN" Enter fullscreen mode Exit fullscreen mode In bash, rm -f shrugs at the missing file and the follow-up runs. In zsh, nomatch is on by default, so the unmatched glob is a shell error, rm never runs, and the chain returns non-zero. Same line, opposite outcome, and the error you get back is prefixed (eval):1: — which tells you it went through eval, but never mentions zsh. Reason 2: the userland is BSD macOS ships BSD versions of the standard tools. Claude — like nearly every shell example ever written — assumes GNU. What Claude writes What macOS does sed -i 's/a/b/' file creates file-e, edits the original, exits 0 date -d yesterday +%F date: illegal time format stat -c %s file stat: illegal option -- c sed -E 's/\s+/_/' doesn't match — BSD sed has no \s sed -i is the one worth internalising. It doesn't fail. BSD sed -i reads the next argument as a backup suffix, so -i 's/a/b/' means "back up with suffix s/a/b/"... except it doesn't even do that here; it takes the empty string that follows -i, edits in place, and drops an -e file beside it. An agent that checks the exit code sees success and moves on. Why you can't fix this in your shell profile This is the part that cost me an afternoon, and it's the reason a five-minute fix turns into a project. The obvious move is to brew install coreutils gnu-sed and put the GNU binaries first on PATH in ~/.zprofile or ~/.bashrc. It has no effect whatsoever on the Bash tool. Claude Code captures a shell snapshot when a session starts and replays it before every Bash call. The export PATH line in that snapshot is written from Claude Code's own process environment — not from the shell it captured in. The captured login shell is asked for its own PATH only on Windows. I verified this rather than assuming it. I put a guard in my profile that 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. The resulting snapshot still came out without it. So your profile isn't being ignored. It runs. Its PATH just never reaches the tool. What actually works Two pieces of documented-but-obscure surface, one for each problem. The shell: CLAUDE_CODE_SHELL This is read from the env block of settings.json before the shell is chosen, which is exactly why it works where a profile can't: { "env": { "CLAUDE_CODE_SHELL": "bash", "SHELL": "bash" } } Enter fullscreen mode Exit fullscreen mode A bare bash means the first bash on PATH — Homebrew's 5.x, not Apple's 3.2. Set SHELL alongside it, or the model's own environment summary keeps telling it the shell is zsh. The tools: CLAUDE_ENV_FILE A SessionStart hook may append shell statements to the file that CLAUDE_ENV_FILE 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}" Enter fullscreen mode Exit fullscreen mode Homebrew ships coreutils, findutils, gawk, gnu-sed, gnu-tar, gnu-which and grep with a libexec/gnubin directory containing the GNU builds under their plain names — sed, not gsed. Putting those directories on the agent's PATH gives it a GNU userland without symlinking anything or touching your own terminal. brew install bash coreutils findutils gawk gnu-sed gnu-tar gnu-which grep Enter fullscreen mode Exit fullscreen mode The result Measured on Claude Code 2.1.266, macOS 26.6, in a session started with no inherited PATH, from a directory with and without the config: Without With Shell zsh 5.9 bash 5.3.15 sed --version sed: illegal option -- - GNU sed 4.10 date --version date: illegal option -- - GNU coreutils 9.11 awk --version error GNU Awk 5.4.1 date -d yesterday +%F error 2026-09-08 Your own terminal is untouched. sed still resolves to /usr/bin/sed in your shell, gsed and gdate still work the way you're used to, and nothing is copied or linked — a brew install or brew uninstall takes effect at the next session with no step in between. Packaged I put both halves in a repo, because I was tired of pasting them into every project: https://github.com/Baune8D/claude-code-macos (MIT) As a plugin, once, for every repo you open: /plugin marketplace add Baune8D/claude-code-macos /plugin install claude-code-macos@claude-code-macos Enter fullscreen mode Exit fullscreen mode then add that env block to ~/.claude/settings.json by hand. A plugin manifest has no env block, and CLAUDE_CODE_SHELL is read before the shell is chosen so no hook can write it either — which means a plugin install alone gets you the GNU tools while leaving you on zsh. There's a hook that tells you when that's the state you're in, rather than letting the session run half-fixed. Or copy .claude/settings.json and .claude/hooks/ into a repo, which is the whole fix in one step and travels to everyone who clones it. It's silent when it succeeds. It speaks once, at session start, when it can't deliver: Homebrew missing, a formula not installed, the Bash tool not running bash, or the bash that won still being Apple's 3.2. The agent gets the fact ("This session's sed is the macOS build, not GNU."), you get the fix (brew install gnu-sed). One deliberate omission: grep and find Claude Code ships its own. Both are installed into the session as shell functions that re-exec the claude binary as ugrep and bfs — and a function beats a PATH lookup, so those two names stay Claude Code's however you arrange PATH. An earlier version of my hook removed those functions. I took it back out after measuring: across a couple dozen common idioms (-rn, --include, -oP, \b, -A, -printf, -regex, -exec {} +) the engines never disagreed on syntax. Every difference was in ugrep's defaults — a bare grep -r honours .gitignore, skips binaries, and omits the ./ prefix. For an agent searching a repository, not walking node_modules is simply the better default. The GNU grep and findutils formulas are still worth installing, because scripts, an explicit command grep, and the wrapper's own fall-through for -z/--null all resolve through PATH. Upstream This should ideally not need a workaround, so both halves are filed: anthropics/claude-code#91498 — the Bash tool runs the login shell (not my issue; I found it afterwards) anthropics/claude-code#95705 — the userland is BSD and nothing says so Until one of those lands, this is what I run. If you've been quietly assuming Claude is just bad at shell on your machine — it's the environment, and it takes about two minutes to fix.

来源报道 · 1 篇 · 以下链接均前往原始媒体

← 返回精选动态 原文

更多内容