Claude Media
Claude Codeがzshでbash構文を書いて失敗する原因と対処

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,296Claudeが書きがちな形grep -rn foo src --include=*.tszsh 5.9での結果no matches found: --include=*.ts
一致しないパスglob件数656Claudeが書きがちな形ls libs/*/README.mdzsh 5.9での結果no matches found: libs/*/README.md
=始まりの語件数549Claudeが書きがちな形echo ====zsh 5.9での結果=== not found
変数の単語分割件数28Claudeが書きがちな形G="git -C repo"; $G logzsh 5.9での結果全体が1語のコマンド名として扱われる
コロン修飾子件数21Claudeが書きがちな形git show $B:shared/package.jsonzsh 5.9での結果bad substitution
配列添字件数7Claudeが書きがちな形"$pat[^};]*"zsh 5.9での結果bad math expression
bash専用の展開件数2Claudeが書きがちな形${!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 0

3つのオプションを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のエラー行が出ない失敗は、ここで挙げた型には当たりません。

この記事を共有:XはてブLinkedIn