Claude Codeがzshでbash構文を書いて失敗する原因と対処
macOSでログインシェルがzshのとき、Bashツールはzshでコマンドを走らせます。bash流の書き方で出る失敗の型と、エラーが出力に埋もれる理由、CLAUDE.mdとCLAUDE_CODE_SHELLによる回避をまとめます。
macOSのログインシェルがzshなら、Claude CodeのBashツールもzshでコマンドを実行します。ところがClaudeは、ツール名に引きずられてbash流の書き方をしがちです。grep --include=*.tsのようなglob(ワイルドカード)やecho ====といった何気ない一行が、zshではエラーになります。
しかもエラーは、コマンド全体の終了コードを非0にするとは限りません。出力の途中に1行混ざるだけで、Claudeは気づかないまま先へ進みます。対処は大きく3つです。CLAUDE.mdで書き方を固定する、CLAUDE_CODE_SHELLでbashを指定する、CLAUDE_ENV_FILEでzshのオプションを変える。この記事は症状の型、見分け方、3つの対処の使い分けを順に扱います。
Bashツールはzshで走るのに、Claudeはbashを書く
Bashツールがどのシェルでコマンドを実行するかは、環境変数CLAUDE_CODE_SHELLの説明に書かれています。値を指定しなければ自動検出になり、$SHELLがbashかzshを指していればそれを使います。macOSの標準ログインシェルはzshなので、何も設定していないMacではzshで動きます。
シェルが決まる流れと、Claude側の癖がずれるのが問題の根です。起動ファイルの扱いはBashツールとシェル起動設定の解説に譲り、ここでは「zshで実行される」前提でどんな書き方が壊れるかに絞ります。
GitHubのissue #103027は、この症状を12週間分の自分のトランスクリプトで数えた報告です。条件はmacOS、zshがログインシェル、Claude Code 2.1.x、主にOpus 5.5。数字は次のとおりです。
報告された失敗の規模(2026-07-28〜10-17)
Bashコール総数
61,040
287セッション
zshエラーを含む
2,570
全体の約4%
終了コード0のまま
76%
エラーが出力の途中に埋もれた
同じ型の再発
87%
同一セッション内で既に踏んだ型
発生率は、7月28日〜9月20日の1,000コールあたり14.7件から、9月21日〜10月17日の51.3件へ約3.5倍に上がったと報告されています。Claudeが応答の中で原因をzshだと言及したのは、約3%にあたる82回だけでした。
直接のコストは小さく、API単価換算で約53ドル(支出の0.7%)、モデルの待ち時間で約71分です。報告者が大きな問題として挙げているのは別の点で、出力が欠けたまま、Claudeがそれを前提に結論を出すことです。
このissueはopenのままです。同じ報告者が先行して立てた#67146は、活動がないまま「not planned」で閉じています。
zshで壊れるbash流の書き方7型
issueの表を基に、型ごとにzshの挙動を並べます。右端の結果は、zsh 5.9でzsh -cを空のディレクトリで実行して得た出力です。報告の件数は表の「件数」列にあります。
| 型 | 件数 | Claudeが書きがちな形 | zsh 5.9での結果 |
|---|---|---|---|
| フラグ値のglob | 件数1,296 | Claudeが書きがちな形grep -rn foo src --include=*.ts | zsh 5.9での結果no matches found: --include=*.ts |
| 一致しないパスglob | 件数656 | Claudeが書きがちな形ls libs/*/README.md | zsh 5.9での結果no matches found: libs/*/README.md |
=始まりの語 | 件数549 | Claudeが書きがちな形echo ==== | zsh 5.9での結果=== not found |
| 変数の単語分割 | 件数28 | Claudeが書きがちな形G="git -C repo"; $G log | zsh 5.9での結果全体が1語のコマンド名として扱われる |
| コロン修飾子 | 件数21 | Claudeが書きがちな形git show $B:shared/package.json | zsh 5.9での結果bad substitution |
| 配列添字 | 件数7 | Claudeが書きがちな形"$pat[^};]*" | zsh 5.9での結果bad math expression |
| bash専用の展開 | 件数2 | Claudeが書きがちな形${!v} | zsh 5.9での結果bad substitution |
9月下旬以降の増加は、上の3行でほぼ説明がつくとされています。
上位3型がzshで失敗する理由
glob: 展開に失敗するとコマンドごと止まる
bashは一致するファイルがなければパターンをそのまま渡します。zshの既定は逆で、一致しないとエラーにして実行をやめます。
--include=*.tsは--include=の後ろまで含めて1語のglobとして評価されるため、カレントに該当ファイルがなければ失敗します。=始まり: コマンド名の展開が走る
zshは
=cmdを、そのコマンドの絶対パスに置き換えます。====は===というコマンドを探し、見つからずにエラーです。区切り線を出したいだけのecho ====が、それで落ちます。変数: 単語分割をしない
bashは
$Gを空白で分割して複数の引数にします。zshの既定は分割せず、"git -C repo"全体を1つのコマンド名として探します。
手元で、SH_WORD_SPLITを付けないG="printf [%s] a b"; $Gを実行するとcommand not found: printf [%s] a bになりました。同じ文をbashで実行すると[a][b]と出ます。
コロン修飾子は、$B:shared/package.jsonが$Bに対する:s(置換)などの修飾子として解釈されるためです。${B}:shared/package.jsonと波括弧で閉じれば、main:shared/package.jsonのように期待どおりの文字列になりました。
エラーは「失敗」として返らないことがある
上の表の挙動は、1行のコマンドだけを見ると派手ですが、実際の被害は別の形で出ます。ls src; echo ====; echo doneをzshで実行すると、lsの結果の後に=== not foundが出て、echo doneは実行されませんでした。ここまでは分かりやすい失敗です。
一方、一致しないglobの後ろに;で別コマンドを続けた場合は、エラー行が出たあとも後続のコマンドが走ります。ls libs/*/README.md; echo afterでは、エラーの後にafterが出力されました。つなぎ方次第で、全体の終了コードは最後のコマンドの0になります。issueが「76%は終了コード0」と報告するのは、こうした経路が日常的に起きているためと読めます。
Claudeは出力の末尾だけを見て成功と判断しやすく、途中のzsh: no matches foundは読み飛ばされます。その結果、検索が空振りしたのに「該当ファイルはありません」と結論づけるような誤りが起きうる、というのが報告者の懸念です。
見分け方: 自分の環境でどのくらい起きているか
issueでは、トランスクリプトに残ったzshのエラー行を数えて発生率を出しています。(eval):N:という形のエラー行が目印です。手元のログを同じ方法で調べるなら、~/.claude/projects配下のjsonlをgrepします。
grep -rhoE '\(eval\):[0-9]+: [^"\\]{0,60}' ~/.claude/projects \
| sed -E 's/\(eval\):[0-9]+: //' \
| sort | uniq -c | sort -rn | head -20出てくる文言がno matches found・not found・bad substitutionなら、この記事の型に当たります。サブエージェントのトランスクリプトもこの配下に入るため、まとめて集計できます。
対処1: CLAUDE.mdでzsh向けの書き方を固定する
最も副作用が小さいのは、Claudeに書き方を教えることです。CLAUDE.mdはセッション開始時に読み込まれる常設の指示で、プロジェクトのものと個人用のものを重ねて使えます。
issueの表から導ける規則を、そのまま短い指示にします。次のような断片が出発点です。
## Bashツールの書き方(zsh前提)
- フラグ値のglobは引用符で囲む: `--include="*.ts"`。パスのglobは`find libs -name README.md`で代える
- `=`で始まる文字列は引用符で囲む: `echo "===="`
- 変数でコマンドを組み立てない。必要なら配列かbash -cを使う
- `$var:path`は`${var}:path`と書く
- `${!v}`などbash専用の展開は使わない
- 標準出力に`no matches found`や`not found`が混ざったら、成功扱いにせず原因を直して再実行する最後の行は、前節の「静かな失敗」に対する保険です。Claudeが出力の途中のエラーを読み飛ばさないよう、見たら再実行するという行動を決めておきます。
CLAUDE.mdは短いほど守られやすく、1ファイル200行以下が目安とされています。zshの注意書きのために行数を使いすぎないよう、上の6行程度にとどめます。モデルの癖を完全に消す仕組みではない点は変わりません。issueでは87%が同じセッション内で既に踏んだ型の再発だったので、効果は「そのセッションの2回目以降を減らす」方向に出ると見込めます。
対処2: CLAUDE_CODE_SHELLでbashに固定する
書き方を直すより、実行するシェルをbashにそろえるほうが確実です。CLAUDE_CODE_SHELLは、bashかzshのバイナリのパスを受け取ります。/opt/homebrew/bin/bashが例として挙がっていて、fishなど他のシェルは未対応です。動かないパスを渡すと無視され、自動検出に戻ります。
export CLAUDE_CODE_SHELL=/bin/bash
claude毎回の指定を避けるなら、settings.jsonのenvに書きます。
{
"env": {
"CLAUDE_CODE_SHELL": "/bin/bash"
}
}この方法はissueの回避策としても書かれています。ただし、副作用を考える必要があります。Claude Codeはセッション開始時に、使うシェルに応じて~/.zshrc・~/.bashrc・~/.profileのどれかを読み込み、エイリアスと関数を取り込みます。bashに切り替えると、読み込む起動ファイルが~/.bashrc側に変わる、と文書の記述から読めます。zshでしか定義していないエイリアスや関数、PATHの設定は、そのままでは見えなくなる可能性があります。切り替えたら、よく使うコマンドが通るか確かめるのが安全です。
issueの提案は、デフォルトをbashにする、モデルにzshだと伝える、エラーを失敗として返す、の3点です。CLAUDE_CODE_SHELLのbash化を求める#74746は閉じています。現状は、自分で設定する運用になります。ログインシェルがNushellやfishの人は、事情が異なります。Nushellの場合の整理を参照してください。
対処3: zshのオプションをBashツール側だけ変える
シェルを替えずに、zshの挙動をbash寄りに寄せる手もあります。issueのコメントで、ある利用者が似た内容のプラグインを紹介しています。仕組みはSessionStartフックでCLAUDE_ENV_FILEに1行を書き、BashツールのシェルがzshのときだけNO_NOMATCH・NO_EQUALS・SH_WORD_SPLITを設定するものです。
CLAUDE_ENV_FILEは、各Bashコマンドの前に同じシェルプロセスで実行されるスクリプトです。ここにsetoptを書けば、コマンドごとにオプションが効きます。記述の骨格は次のとおりです。
#!/bin/bash
# SessionStartフックの例(zshのときだけオプションを変える)
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo '[ -n "$ZSH_VERSION" ] && setopt NO_NOMATCH NO_EQUALS SH_WORD_SPLIT' \
>> "$CLAUDE_ENV_FILE"
fi
exit 03つのオプションをzsh -cに付けて試すと、ls libs/*/README.mdはNo such file or directoryというlsの通常のエラーに変わり、echo ====は====をそのまま出力し、$Gは[a][b]と分割されました。
ただし副作用もあります。SH_WORD_SPLITを有効にすると、zshで書かれた既存のスクリプトの変数展開の意味が変わります。Bashツールのシェルだけに閉じた設定なので、対話ターミナルには及びません。Claudeが書くスクリプトをzsh前提でレビューしている人は、挙動の差を承知のうえで選ぶ必要があります。
issueのコメントによると、この方法で防げないのは${!v}、コロン修飾子、$pat[...]の3つです。後者2つはKSH_ARRAYSのような別スイッチが要り、既定ではオフとされています。そのため、対処1のCLAUDE.mdと組み合わせるのが現実的です。フックの書き方の全体像はHooks完全ガイドにあります。
どの対処を選ぶか
3つは排他ではありません。影響範囲と手間で並べると次のようになります。
| 対処 | 効く範囲 | 副作用 | 向く状況 |
|---|---|---|---|
| CLAUDE.mdで書き方を固定 | 効く範囲Claudeの書き方 | 副作用ほぼなし。守られない回もある | 向く状況まず試す。チーム共有もできる |
CLAUDE_CODE_SHELLでbash | 効く範囲Bashツール全体 | 副作用zshの起動ファイルが読まれない | 向く状況型が多く、zsh固有の設定を使わない |
CLAUDE_ENV_FILEでzshオプション | 効く範囲glob・=・単語分割 | 副作用zsh既定動作が変わる | 向く状況zshのままで3型を抑えたい |
迷ったら、対処1を入れて数日様子を見ます。前節のgrepで件数を数え、減らなければ対処2か3へ進みます。検索や置換の結果が空だったときは、Claudeに「コマンドの出力にエラー行がないか」を確認させる一言も有効です。
シェルの外側の失敗、たとえばプロセス起動時のE2BIGのような別系統のBashツール失敗はspawn E2BIGの解説が扱っています。zshのエラー行が出ない失敗は、ここで挙げた型には当たりません。