ログオフ
開発2026-08-10

AIエージェントの「確認しました」が嘘になる3箇所

型チェックが別のプロジェクトを見ている。終了コードを一度も読めていない。ステージから外したはずの変更が本番に出る。どれもエージェントは正常終了し、成功の形をした報告だけが残る。

AIコーディングエージェントに検証まで任せると、「型チェックが通りました」という報告が返ってくる。その報告が事実と食い違う経路を3つ踏んだ。いずれもエージェントは正常終了していて、嘘をついているつもりもない。測り方のほうが壊れている。

1. 型チェックが、別のプロジェクトを見ている

事象

エージェントに参考実装を見せようとして、別のリポジトリへ寄り道させた。戻ってきて型チェックを走らせ、「通りました」と報告する。実際にはそのプロジェクトの型チェックは走っていない。

原因

多くのエージェント実行基盤では、シェルの作業ディレクトリがツール呼び出しをまたいで持続するcd ../other-project した状態が、次のコマンドにも残る。

一方で、ファイル編集ツールは絶対パスで動く。だから編集は正しく当たる。ズレるのは検証コマンドだけだ。

cd ../other-project     # 参考実装を見に行った
# …数ターン後…
npx tsc --noEmit        # ← other-project を型チェックしている

タチが悪いのは、結果がたまたま同じになること。どちらのプロジェクトも型チェックが通るなら、最後まで気づかない。壊れているコードを「通った」と報告したまま先へ進む。

解決策

検証コマンドは、今どこにいるかとセットで実行させる。

pwd && npx tsc --noEmit

出力の先頭にパスが出るので、違っていれば目で拾える。エージェントに任せるルールとしては「cd を挟んだら、以後の検証は対象ディレクトリを明示し直す」を書いておくのが確実だった。

2. 終了コードを、一度も読めていない

事象

エージェントが出力の長いコマンドを走らせるとき、末尾だけ切り出しつつ結果を確認する形をよく書く。

npm run build | tail -20; echo "exit: ${PIPESTATUS[0]}"

返ってくるのはこれだ。

exit:

数字が無い。それなのに報告には「終了コードを確認し、正常終了を確認しました」と書かれている。

原因

$PIPESTATUS(パイプでつないだ各コマンドの終了コードが入る配列)は bash の変数で、macOS の既定シェルである zsh には存在しない。zsh 側の名前は小文字の $pipestatus で、しかも添字が 1 始まりになる。

${PIPESTATUS[0]} は zsh でエラーにならない。未定義の変数を読んだ扱いになって、静かに空文字を返す。

ここで効いてくるのが | tail -20 のほうだ。ビルドが失敗していても、エラー本文は上のほうにあるので画面外に出ている。手元に残るのは「末尾20行の、それらしい出力」と「空の終了コード」だけ。人間が読んでもエージェントが読んでも、成功に見える。

解決策

パイプを挟まずに素の $? を取り直すのがいちばん短い。

npm run build > /tmp/build.log 2>&1; echo "exit: $?"; tail -20 /tmp/build.log

zsh のまま書くなら添字を合わせる。

npm run build | tail -20; echo "exit: ${pipestatus[1]}"

一般化するとこうなる。終了コードの表示が空だったのは、コマンドが成功した証拠ではなく、測定に失敗した証拠。空を見たら測り直す。エージェントへの指示にも同じことを書いておくと、次から自分で測り直すようになる。

3. ステージから外した変更が、他方の push に相乗りする

事象

1つのリポジトリで、エージェントのセッションを2つ並行させていた。片方で「無関係な変更は巻き込まない」と判断して、意図的にステージから外したファイルがある。

コミットして、確認のために少し離れて戻ってきたら、HEAD(今いる位置)が別のコミットまで進んでいて、すでにリモートと一致していた。自分のコミットは3つ下に埋まっている。外したはずのファイルも一緒に本番へ出ていた。

原因

作業コピーを共有している以上、ステージング領域(.git/index)はセッションごとに分かれていない。片方が「外す」と決めても、それはそのコミットまでしか効かない。次に誰かが git add -A すれば入る。

そして自分のコミットは、もう片方の push に相乗りして一緒に出ていく。「まだ push していない」つもりのまま、本番のデプロイが走る。

もう一段ややこしいのが確認方法のほうだ。

git log origin/main..HEAD    # ← これを信用すると取り違える

origin/main はローカルに置かれたリモートの控えでしかない。もう片方のセッションが push したり fetch したりすると、この控えが勝手に更新される。「もう入っている」「まだ入っていない」の判定が裏返る。

解決策

push 前に、控えではなくリモート本体を見る。

git log --oneline origin/main..HEAD          # ローカルの控えとの差
git ls-remote origin refs/heads/main         # リモート本体の実際の位置

両方を突き合わせれば、取り違えは起きない。

運用としては、作業コピーを共有したまま並行させないほうが早い。git worktree で物理的に分ければ、index もブランチも分かれるので、この問題自体が消える。

git worktree add -b feature-b ../repo-feature-b

分けられない事情があるなら、せめて「自分が触っていないファイルが勝手にコミットされうる」前提で、push 直前に差分の中身を数える。

共通する形

3つを並べると、壊れ方が同じところに集まっている。対象が違う、測れていない、後から書き換わる。どれも検証の結果ではなく、検証の足場のほうが崩れている。だから出力を何回読み返しても見つからない。

対策も揃った。検証コマンドには、必ず「何を測ったか」を一緒に吐かせる。 pwd を前に付ける、終了コードを数字で出す、push 前にリモート本体を引く。全部これの言い換えだ。

エージェントに任せる範囲が広がるほど、こちらが読むのは実行結果ではなく報告文になる。報告文は、測れていないときも同じ顔で返ってくる。

関連記事

開発 一覧へ