ローカルは通るのにCIで落ちる
ローカルでは動くのにCIで失敗する。そんなときに差分を探す順番を、取り込むコミット、シェルとコマンド、依存、アーキテクチャの順にまとめた。
エージェントに書かせたコードが、手元では全部通るのに、GitHub Actions だけ赤くなる。エージェントに見せると、たいていテストやアプリのコードを直し始めます。けれども、手元と CI で違うのはコードではなく、コードの周りであることがほとんどです。この記事では、編集部の検証環境で踏んだ例をもとに、差分を探す順番をまとめます。
CI で Claude Code そのものを動かすときの費用と設定は「Claude Code×CIの費用と罠」に書きました。こちらは、CI で落ちた原因を探す話です。
最初に、CI が何を動かしたかを見る
手元と CI で、そもそも同じコミットを動かしているとは限りません。
GitHub の公式ドキュメントによると、pull_request イベントでは GITHUB_REF が refs/pull/<番号>/merge になり、GITHUB_SHA はプルリクエストのマージブランチの最新のマージコミットです。ブランチの先頭のコミットが欲しいなら github.event.pull_request.head.sha を使う、とあります(出典:https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows 、2026-10-16確認)。
つまり CI は、手元のブランチではなく「ベースブランチに取り込んだ結果」を試しています。ベース側に入った変更とぶつかって落ちているなら、手元でもベースを取り込めば再現します。ログの冒頭でチェックアウトしたコミットを確かめ、手元の git rev-parse HEAD と比べるのが最初の一手です。
次に、シェルとコマンドの違いを疑う
コミットが同じなら、次はスクリプトを動かす道具を疑います。シェルスクリプトは、同じ書き方でもシェルやコマンドの実装で結果が変わります。手元で、ダミーのファイルを使って再現しました(GNU bash 5.2.37、mawk 1.3.4、2026-10-16)。
EXIT trap が終了コードを書き換える。 後片付けの関数を trap cleanup EXIT で登録し、exit 0 で終えたスクリプトです。片付けの最後の行が「ファイルが無ければ何もしない」という条件式でした。
set -e
cleanup() { echo "cleanup"; [ -f /tmp/cired/none.tmp ] && rm /tmp/cired/none.tmp; }
trap cleanup EXIT
exit 0
cleanup
t2 exit=1
exit 0 と書いたのに、終了コードは1でした。片付ける対象のファイルが無いと、最後の条件式が偽になり、それが終了コードとして残っています。手元ではたまたま一時ファイルが残っていて通り、まっさらな CI では落ちる、という形になります。片付け関数の最後に true を置くか、if で書きます。
pipefail と早く終わる grep。 set -euo pipefail の下で、大きな出力を grep -q に流しました。
t3 exit=141
grep -q は最初の一致で終わります。書き手側のコマンドが閉じたパイプに書こうとして SIGPIPE で落ち、pipefail がそれを拾って、スクリプト全体が141で終わりました。出力の量やタイミングで結果が変わるので、手元では通ることがあります。
awk の実装の違い。 大文字小文字を無視する IGNORECASE は GNU awk の機能です。mawk で同じ書き方をすると、エラーも出ずに大文字小文字が区別されたままでした。
--- mawk:
error
--- 移植性のある書き方:
Error
ERROR
error
GNU awk のマニュアルは、IGNORECASE を gawk の組み込み変数として載せています(出典:https://www.gnu.org/software/gawk/manual/html_node/User_002dmodified.html 、2026-10-16確認)。macOS の awk は GNU awk ではないので、同じように効かない可能性があります(編集部は macOS では再現していません)。tolower($0) ~ /error/ のように、どの実装でも動く書き方にします。
古い bash と空の配列。 bash の変更履歴によると、set -u(nounset)の下で空の配列を "${a[@]}" と展開しても unbound variable のエラーにならなくなったのは、4.4 からです(出典:https://git.savannah.gnu.org/cgit/bash.git/plain/CHANGES 、2026-10-16確認)。それより前の bash では、この書き方で止まります。手元の bash 5.2 では、空の配列でもそのまま通りました。macOS に付属する /bin/bash は 3.2 系だと言われており、CI の macOS ランナーでそれが使われると、CI だけで落ちる可能性があります(編集部は macOS では再現していません)。bash --version で、CI が使う bash の版を確かめます。
依存の解決を疑う
コードもシェルも同じなら、パッケージの入り方を疑います。
pnpm のワークスペースで、ある型定義のパッケージを自分では依存に書いていないのに、手元では型チェックが通っていたことがあります。別のパッケージが入れたものを、ホイスト(巻き上げ)の結果として拾っていました。CI のきれいなインストールでは拾えず、型エラーで落ちました。使っているものは、自分の package.json に書きます。
手元で長く使っている node_modules は、過去のインストールの残りを含んでいます。CI と同じ条件に近づけるなら、ロックファイルどおりに入れ直してから試します。
ビルドと実行のアーキテクチャを疑う
最後に、ビルドしたマシンと動かすマシンが同じ OS・CPU かを確かめます。
ネイティブのモジュール(C などで書かれ、OS と CPU ごとにバイナリが違うもの)を含むアプリを、別のアーキテクチャのマシンでビルドして配ったことがあります。動かす側のバイナリが同梱されず、そのモジュールを使う経路だけ、すべてのリクエストがエラーになりました。ほかの画面は普通に動くので、気づくのが遅れます。
環境変数は別に確かめる
シークレットや環境変数の有無で CI だけ落ちることもあります。設定したのに効かないときの切り分けは「Claude Codeで環境変数が効かない」にまとめたので、ここでは扱いません。
エージェントに頼むときの順番
CI が赤いときは、コードを直させる前に、次の順で差分を出させます。
CI の失敗を直す前に、手元と CI の差分を次の順で調べて書くこと。
- CI がチェックアウトしたコミットと、手元の HEAD
- シェルとコマンドの実装・版(bash、awk、sed など)
- 依存のインストール方法と、ロックファイルどおりか
- ビルドと実行の OS・CPU
まとめ
- 手元で通って CI で落ちるとき、違うのはコードではなく周りであることが多い。
pull_requestの CI は、ベースに取り込んだ結果を試している。まずコミットを比べる。- EXIT trap、pipefail と
grep -q、awk の実装、古い bash は、手元では通りやすい。 - 依存は自分で宣言し、ロックファイルどおりに入れ直して試す。
- ネイティブのモジュールは、ビルドと実行のアーキテクチャをそろえる。