【ITニュース解説】The checks failed, but the AI saw exit code 0. It came from tail
2026年11月05日に「Dev.to」が公開したITニュース「The checks failed, but the AI saw exit code 0. It came from tail」について初心者にもわかりやすく解説しています。
ITニュース概要
AIがスクリプトの結果を誤認識し、問題を見過ごす例があった。これは、パイプでコマンドをつなぐと、最後のコマンド(`tail`など)が成功すれば全体の処理も成功と判断されるためだ。正確な結果を知るには、ログをファイルに書き出し、スクリプト本来の終了コードやエラー内容を直接調べる必要がある。
ITニュース解説
システムエンジニアを目指す初心者の皆さん、日々進化するITの世界では、ちょっとしたコマンドの実行方法が予期せぬ大きな問題を引き起こすことがあります。今回は、AIがテスト結果を誤って判断してしまったという事例を通して、シェルスクリプトやコマンド実行の基本的な落とし穴とその対策について詳しく解説します。
ある開発者が、自身のiPhoneアプリの品質をチェックする run.sh というシェルスクリプトを使っていました。このスクリプトは、複数のチェック項目を実行し、それぞれの結果を「OK」または「NG」で出力します。もし一つでも「NG」が出たら、スクリプトは異常終了を示す終了コード「1」を返します。終了コードとは、コマンドやプログラムが処理を終えたときにOSに伝える数値で、一般的に「0」は正常終了、「0以外」は異常終了を意味します。
この run.sh スクリプトを、AIアシスタントのClaude Codeが実行しました。しかし、AIはコマンドを ./run.sh 2>&1 | tail -60 のように実行しました。これは、run.sh の標準出力と標準エラー出力を tail -60 コマンドの入力にパイプ(|)で渡し、その出力の最後の60行だけを表示するという意味です。結果として、AIは「NG」は検出されず、チェックは成功したと判断してしまいました。実際には run.sh は「NG」を検出して終了コード「1」を返していたのですが、AIはこれを見落としたのです。
なぜこのような誤った判断が起きたのでしょうか。それは、シェルにおける「パイプラインの終了コード」という挙動に原因があります。シェルで command1 | command2 のように複数のコマンドをパイプでつなげて実行した場合、パイプライン全体の終了コードは 最後のコマンドの終了コード になります。今回のケースでは、run.sh の後に tail -60 が実行されています。tail -60 は、入力されたデータの最後の60行を表示するコマンドであり、それ自体はエラーなく正常に動作します。そのため、tail -60 は常に終了コード「0」(成功)を返します。結果として、run.sh が終了コード「1」を返していても、パイプライン全体の終了コードは tail の「0」になってしまい、AIはチェックが成功したと誤認してしまったのです。
さらに、tail -60 を使用したことで、run.sh が出力した「NG」メッセージが最後の60行に含まれていなければ、AIは物理的にもその情報にアクセスできず、問題を見つけることができませんでした。これは、開発者が以前から「パイプすると終了コードが tail のものになるため、一度ファイルに書き出すべきだ」というコメントをスクリプト内に残していたにもかかわらず、その意図が正しく伝わっていなかったことからも分かります。
この終了コードの落とし穴を実際に体験してみましょう。シェルで false | tail -1; echo $? と入力して実行してみてください。false コマンドは常に終了コード「1」(失敗)を返し、true コマンドは常に終了コード「0」(成功)を返すテスト用のコマンドです。このコマンドを実行すると、false は失敗しているにもかかわらず、echo $? の出力は「0」と表示されます。これは、パイプラインの最後のコマンドである tail -1 が正常に実行されたため、その終了コード「0」がパイプライン全体の終了コードとして取得されてしまったからです。
では、パイプライン内の途中のコマンドの終了コードを正しく取得するにはどうすれば良いでしょうか。一つは、シェルが提供する特殊な変数 PIPESTATUS を使う方法です。例えば false | tail -1; echo "${PIPESTATUS[0]}" と実行すると、パイプラインの最初のコマンド(false)の終了コードである「1」が正しく表示されます。ただし、この変数名はシェルによって異なり、bashでは PIPESTATUS[0]、zshでは pipestatus[1] のようにインデックスの始まりも異なるため、使用するシェルに合わせて記述する必要があります。
もう一つの、より強力な解決策は、set -o pipefail というシェルオプションを使うことです。このオプションを有効にすると、パイプライン内のコマンドのどれか一つでも終了コード「0以外」を返した場合、パイプライン全体の終了コードも「0以外」(失敗)となるようにシェルの挙動が変わります。set -o pipefail; false | tail -1; echo $? と実行してみてください。今度は「1」が正しく表示されるはずです。このオプションは、パイプライン全体の成功/失敗をより直感的に判断できるようになるため、シェルスクリプトでのエラーハンドリングに非常に有用です。ただし、AIがシェルコマンドを実行するような環境では、コマンドごとにシェルの状態がリセットされる場合があるため、set -o pipefail はパイプを含むコマンドと同じ行で記述する必要があります。
この終了コードの扱いは、GitHub ActionsのようなCI/CDツールでも非常に重要です。GitHub Actionsの run: ステップでは、shell: bash を明示的に指定しない場合と指定した場合で、内部で実行されるシェルの挙動が異なることがあります。特に、shell: bash を明示的に指定すると pipefail オプションがデフォルトで有効になる一方で、指定しない場合は pipefail が無効な状態で実行されることがあります。このため、同じコマンドを実行しても、GitHub Actionsの設定一つでステップの成否が変わり、開発者が意図しない結果を招く可能性があります。自動化された環境では、シェルの挙動を正確に理解し、必要に応じて明示的に設定することがプロジェクトの品質保証において不可欠です。
このような問題を根本的に解決し、堅牢なチェック結果を得るための推奨される方法は、パイプラインを使わずにスクリプトの出力と終了コードを個別に確認することです。具体的には、./run.sh > /tmp/run.log 2>&1; echo "EXIT=$?" のように、run.sh の出力を一旦 /tmp/run.log というファイルにリダイレクトし、その直後に echo "EXIT=$?" でスクリプト自身の終了コードを EXIT= という形式で出力します。$? は直前に実行されたコマンドの終了コードを保持する特殊な変数です。
そして、ログファイル全体を grep -nw NG /tmp/run.log のように検索し、「NG」という文字列が含まれているかどうかを確認します。grep -nw は「NG」という単語を正確に探し(-w オプション)、行番号とともに表示してくれる(-n オプション)ため、エラーの内容と場所を正確に把握できます。この方法であれば、tail のように重要な情報が隠れることもなく、また grep の終了コードに惑わされることもありません(grep はマッチする行があれば終了コード0、なければ1を返すため、それを直接チェックの成否として利用すると、今度は grep の終了コードで誤解が生じる可能性があります)。ログファイルに出力された EXIT= の行と、grep の結果の両方を確認することで、スクリプトの成否と内容を総合的に判断できるのです。
自動化されたチェックにおいて、AIなどのツールが提示する「チェックパス」という結果を鵜呑みにせず、常にその判断の根拠を深く理解することが重要です。特に、以下の二点を確認する習慣を持つべきです。一つは「示された終了コードが、実際にチェックを行ったスクリプトのものか、それともパイプラインの最後のコマンドのものか」ということ。もう一つは「エラーメッセージ(NGなど)の確認が、ログ全体に対して行われたのか、それとも一部の行だけなのか」ということです。システム開発の現場では、このような小さな知識の有無が、開発の効率やシステムの品質に大きな影響を与えることを、この事例は強く示唆しています。コマンドの基本的な挙動を正しく理解し、堅牢なスクリプトを記述する力を身につけることが、皆さんにとって重要になります。