目次

Claude Codeと開発していると、「できました」「完了しました」という報告を1日に何度も受け取る。僕はこの数ヶ月、5つのプロジェクトをClaude Codeと作ってきたが、いまはこの言葉だけで完成したとは考えていない。タイトルの通りだ。Claude Codeの「できました」は、たぶんできてない。

先に断っておくと、これはClaude Codeが嘘をつくという話ではない。問題は、「できた」という一語の中に、コードを書き終えた・buildが通った・プロセスが起動した・画面が表示された・見た目として成立した、という別々の種類の完了が混ざっていることだ。どの完了を指しているのかは、確認するまで分からない。そして確認する場所は、種類ごとに違う。

5つのプロジェクトに残っている記録から、そのズレが実際にどう起きたかを順番に振り返る。

Gitに存在しない「完了」

カプログという、デートの記録を投稿・共有するWebサービスを作っている。その開発中、過去のセッションの完了報告に「Phase D 完了 commit 6f9c60c」とあった。Phase Dは投稿詳細から「このプランを参考に作成」で新規作成画面へつなぐ機能で、報告の上では完了済みだった。

2026年7月13日、このhashが実在しないと確定した。branchを全部見てもない。reflogにもない。git cat-file -t 6f9c60c は fatal: Not a valid object name を返す。Gitのobjectとして存在しない。そして実際のPhase Dは、その時点まで未完了・未pushだった。

なぜこのhashが報告に現れたのかは、分かっていない。当時の記録も「実在しない hash(repo の全 branch・reflog に痕跡なし)」という事実だけを書いていて、原因を断定していない。だからここでも断定しない。

同じ7月13日に、Phase Dは本当に完了した。本物のcommitは3つ(335f496 fb4d8d0 6725812)。その約8分後、docsに「履歴訂正」のセクションを追加するだけのcommit(8947c26)を作り、「本セクションの 3 commit が正」と恒久記録にした。実在しないhashの記述は、いまもこの訂正文の中に1箇所だけ残っている。

この一件が教えてくれたのは単純なことだ。報告の中のcommitは、引いてみるまで存在すら分からない。「完了しました」という文と、「Gitに完了が存在する」という事実は、別のものだった。

「起動した」と「使える」は違う

ウラドリという店探しのモバイルアプリでは、Expoの開発サーバーの起動スクリプトをClaude Codeに任せていた。当初のスクリプトは、プロセスの起動に成功したら「Expoを起動しました」と表示して正常終了していた。判定していたのは「プロセスが生成された」ことだけだ。

これを改修したcommit(fb96906)のコメントに、理由がそのまま残っている。

起動できたことを必ず検証してから成功と報告する。 Metroが立ち上がる前・落ちた後にQRを読み込むと、端末側は無限ロードになるため、 「起動した」とだけ報告して終わらない

改修後のスクリプトは、http://127.0.0.1:8081/status が200を返すまで3秒間隔で最大120秒待ち、確認できなければexit 1で失敗する。成功時のメッセージも「起動しました」から「Metroの応答を確認済みです」に変わった。報告の文言そのものが、何を検証したかを言う形になった。

process startedとapplication readyは別の完了だ。プロセスは起動に成功して、その直後に死ぬことができる。「起動しました」は前者しか保証しない。だから成功の定義を、スクリプトの出口で「readinessを確認できた」に差し替えた。AIの報告品質をプロンプトのお願いで上げるのではなく、ツールの出口で保証する方向だった。

機械的にはできている。でも、見たらダメだった

ここまでは機械で検証できる話だ。commitはGitに聞けばいいし、readinessはHTTPで聞けばいい。問題は、それまでの機械的な検証だけでは拾えていない完了が、その先に残ることだった。

カプログで体験談の発見フィードを作ったときのこと。実装はlintもbuildも通り、スモークテストも通り、本番にdeployされて、本番で正常に表示されていた。機械的な検証項目は全部緑だった。その翌日の記録にこうある。

Pixel 9 幅の本番目視で「文章中心カードの連続では続きを見たくならない」ことをオーナーが確認。

少なくとも、それまで確認していた検証では不具合は出ていなかった。それでも翌日、フィードを写真付きの投稿だけに絞り、カード構造も配色も作り直した(21cd0ad)。落ちたのは、それまでの機械的な検証項目には入っていなかった「続きを見たくなるか」という判定だった。

犬アプリでも同じことが起きている(このアプリを最終的に凍結した経緯は以前の記事に書いたので、ここでは触れない)。犬の行動切替にフェードを入れてサイズを調整した変更は、flutter analyze No issues・flutter build web 成功と、機械検証を全部通過した。その上で、docsにはこう記録されている。

M-3-fix-3(行動切替フェード+柴 displayHeight 180+idle scale 撤去)はユーザー目視で「全然ダメ・前の方が良い」と判定。

当時のChatGPTとのチャットには、複数の犬を配置した実機画面を見て「ダメだ」「想像してたのと違う」「こんなの誰が使うんだ」と言った記録が残っている。ただし続きがあって、問題は複数の犬がいることではなく、「しょっぱい感じ」「クオリティの低さ」だと自分で説明している。仕様は満たしている。表示もされている。でも、見たら違った。

この変更は手動で撤回した(799a4ab)。面白いのはその28分後で、フェードは「行動の系統が変わる切替だけ0.5秒」という限定形に再設計されて戻ってきた。目視が却下したのはフェードそのものではなく、「すべての切替に一律にかかるフェード」だった。そしてこの2つを、flutter analyzeは区別できない。build successとvisual successは別の完了で、このとき最後にNOを出したのは人間の目だった。

「完了」を報告ではなくEvidenceで受ける

決め手ログという比較投票アプリでは、ここまでの扱いがルールとして明文化されている。ChatGPTに計画とレビュー、Claude Codeに実装を任せる体制で、ChatGPT側の運用ルール(.ai/CHATGPT_GUARDRAILS.md)にこう書いた。

Claude Codeの「完了」報告だけで実機成功と判定しない。

完了判定が重要な場合は、報告文ではなく実際のcommit / diff /対象ファイルを確認する。

検証ループのrunbook(.ai/LOOP.md)には、もう一段具体的な線が引いてある。

PC側の200を成功扱いにしない — 実機/実環境の観測(実機ログ・サーバーログの裏取り)を成功条件にする

少し変わっているのは、AIの完了報告を別のAIがそのまま信じないよう、人間側でルールを置いていることだ。中身はここまでの話と同じ形をしている。完了は報告という1つの文で受けない。Git・HTTP・サーバーログ・実機ログという別々のEvidenceで受ける。そして、どのEvidenceが必要かは作業によって違う。

「終わったよ!」を出すアプリが、完了を判定できなかった

この流れの終着点が、Tiny Code Petという小さなWindowsアプリだった。デスクトップの隅にひよこが常駐して、Claude CodeやCodexの作業が終わると「終わったよ!」と知らせてくれる。つまり、「できました」を通知すること自体が仕事のアプリだ。

作り始めた日の設計は素朴だった。Claude CodeのStop hook(応答完了イベント)を受けたら、即「終わったよ!」。この完了判定を、3日間で4回作り直すことになった。

まず、Stop即完了は誤報を出す。hookは非同期で届くので、Stopより前に起きたイベントがStopの後に届くことがある。そこでStopの後に2秒の猶予を置き、taskの進行状況(tracker)と突き合わせる方式にした。このとき見つけた罠も強烈で、taskの削除やキャンセルではhookが発火しないため、消されたtaskが「幽霊」として残り続けて完了を塞いでいた。

次に厳格化へ進んだ。taskを観測した依頼では、tracker全件completedを完了の必須条件にした。さらに、statusを解析できないケースで判定が緩む抜け道も塞いだ。

この厳格版は、21時間33分で撤回された。設計文書に理由が残っている。

tracker を完了の証拠に使うと、agent 側の status 運用の質がそのまま通知の正しさになる。status を先に倒せば嘘の完了が出るし、運用を守れなければ完了通知が出ない。どちらも Pet 側では検証できない。

agentが「completedにした」ことを、そのagentの完了の証拠にすると、自己申告で自己を証明する構造になる。この記事でずっと見てきた「報告をそのまま完了にしない」の、いちばん機械的な形がここで出てくる。Petが使っていいのは、報告の外側で観測できる事実だけだった。

そもそもStopすら完了ではなかった。Codex側の実測では、最初のStopの約1.88秒後にcontinuationが始まり、約3.69秒後に2回目のStopが来るケースが確認されている。最終的な完了判定はこうなった。root Stopを受けて、その後20秒間その作業が再開されなかったこと。それだけ。trackerは完了判定から完全に切り離され、判定関数がtrackerを読まないことはテストの不変条件として固定されている。

現行のREADMEには、この通知の意味がこう明記されている。

終わったよ! の意味は「成果物が数学的に 100% 正しい」ではない。(中略)Claude Code / Codex が root Stop を出し、その後 20 秒間その作業を再開しなかった、という事実だけを表す。

初日の「終わったよ!」と最終形の「終わったよ!」は、画面の上ではまったく同じセリフだ。変わったのは、そのセリフが主張する範囲だった。「作業が完了した」から、「作業が終了した事実を観測した」まで縮めた。その結果、「終わったよ!」が何を保証する通知なのかは明確になった。

「できました」は確認開始の合図

5つのプロジェクトに出てきた「完了」を並べてみる。コードを書いた。testが通った。commitがGitにある。pushされた。プロセスが起動した。HTTPが200を返した。実機で動いた。画面が表示された。デザインとして成立した。人間が完成と認めた。——これらは全部別の状態で、前の方は機械的に検証しやすい。一方、「デザインとして成立したか」「人間が完成と認めたか」は、今回の開発では人間側の判断として残った。

毎回すべてを満たせという話ではない。スクリプトの修正に本番の目視は要らないし、配色の変更にreflogは要らない。作業ごとに、どの種類の完了が必要で、それをどのEvidenceで確認するかが違うだけだ。ズレが事故になるのは、必要な種類と報告された種類が食い違ったまま気づかないときで、この記事の各事件はぜんぶそれだった。

変わったのは僕の側の扱いだ。報告にcommitがあれば引く。「起動しました」ならreadinessを見る。buildが通ったUIは目で見る。検証をかなり機械へ渡したあとも、「見た目として成立しているか」「これでいいと思えるか」は、人間側の判断として残った。

Tiny Code Petの現行版でも、画面に出る言葉は最初と同じ「終わったよ!」だ。ただ、その意味は変わった。最初は「作業が終わった」という完成の宣言だった。いま保証しているのは、root Stopのあと20秒間、作業が再開されなかったという観測事実だけだ。Claude Codeの「できました」も、今はそれに近い。完成の宣言ではなく、次に何を確認するかを知らせる合図として受け取っている。