1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code に「完成しました」を言わせるな — 公式が教えない3層強制Hook

1
Last updated at Posted at 2026-05-03

「ビルド通ったので完成です」と Claude Code が返してきた瞬間、ブラウザでは何もクリックできなかった。型チェック=完成という嘘を、Hooks/Skill/Playwright の3層で物理的に殴って潰した話。

この記事でわかること

  • Claude Code が「型チェック通った=完成」と嘘をつく構造的な理由
  • メモリ(CLAUDE.md・自動メモリ)にルールを書いても守られない問題と回避策
  • 完成宣言を物理的に止める3層設計(Hooks / Skill / 自動E2E)のコピペで動く完成形
  • メモリ48個運用してきた経験から見える「効くルール」と「効かないルール」の境界

対象読者

  • Claude Code に「動きます」と言われたのに動いてなくてキレた経験がある人
  • AI に作業を任せる頻度が増えてきて、レビュー負荷が上がってる人
  • メモリやルールを書いても AI が守ってくれず諦めかけてる人
  • Claude Code の Hooks / Skills を実用レベルで使いこなしたい人

目次


事件: 「完成しました」と言われた瞬間、ブラウザで何もクリックできなかった

ある日の作業ログ。Claude Code に Web アプリの実装を任せて、しばらく別の作業をしていたら返事が来た。

✅ ビルド成功
✅ 型チェック OK
完成しました。

ブラウザを開いて触る。

クリックが効かない。

「次へ」ボタンが反応しない。フォームに入力しても何も起きない。リンクは飛ぶが、その先のロジックが動かない。

「動かんものを渡すな」と返した。AI 側は「すみません、型チェックだけで完成と言いました」と謝った。

ここまで読んで「あるあるすぎる」と思った人、多いはず。問題はこれが毎回起きること。Claude Code のメモリには「完成宣言の前に E2E 検証」「ビルド通過 ≠ 完成」というルールを既に書いてあった。それでも守られない。

メモリで防げないなら、仕組みで止めるしかない


なぜ Claude Code は「型チェック=完成」と言うのか

これは Claude Code 固有のバグというより、LLM 全般の構造的な性質。理由を3つに分解する。

1. 「完成」の定義が曖昧で、AI は最も近い完了シグナルに飛びつく

人間にとっての「完成」は「ユーザーが意図通りに使える」だが、AI にとっての「完成」はその場で観測できる成功シグナル(=ビルド成功・型チェック成功・テスト pass)になりやすい。

ビルドが通った瞬間、AI はそこで観測した「成功」を「タスク完了の根拠」として使う。観測できないもの(=実際の挙動)は推論の対象にならない

2. 「次に何をすべきか」のチェックリストを内在化していない

人間の熟練エンジニアは「実装 → ビルド → 起動 → ブラウザで触る → 期待通りか確認」という暗黙のチェックリストを持っている。AI はそれを毎回プロンプトとメモリから再構築するので、プロンプトが弱いと最後の「触る」工程が抜ける

3. ツールコールのコストを節約する圧力

LLM はトークンとレイテンシを使う。ある種の「最短経路バイアス」が働く。「ビルド通ったら次は触る」より「ビルド通ったら報告」の方がトークン的に安い。コストを意識すればするほど、検証工程をスキップしたくなる。

💡 Tips: この性質は Claude Code に限らず、Cursor・GitHub Copilot agent・Devin など全 AI コーディングエージェントで観測される。「AI のサボり癖」ではなく LLM の最適化の副作用。Claude Code は特に Hooks があるので、対策できる側に回りやすい。

全Pass で本番で死ぬ典型パターン

ちなみにビルド・型・単体テストが全部 Pass しても、本番で死ぬパターンは大量にある。

  • ✅ 型 OK / ❌ onClickawait を忘れていてボタンが反応しない
  • ✅ ビルド OK / ❌ Next.js の 'use client' 指定漏れでサーバ側で React Hook が落ちる
  • ✅ 単体テスト OK / ❌ CSS で pointer-events: none がかかっててクリックが届かない
  • ✅ E2E のセレクタ OK / ❌ 環境変数 NEXT_PUBLIC_API_URL が空で API が呼べない

ユーザーが触る場所で確認しないと、これら全部素通りする。


メモリにルールを書いても守られない理由

「だったら CLAUDE.md か自動メモリに『完成と言う前に E2E 検証する』と書けばいいのでは?」と思うはず。書いた。守られなかった。

自分の場合、Claude Code の自動メモリには48個のルールが入っている。その中には:

  • feedback_e2e_before_done.md — 「ビルド通過≠完成。ユーザー動線を1回通してから完成と言う」
  • feedback_no_confirm_progress.md — 「これやっていい?と聞かない。代替手段を自分で選んで完遂する」

両方ともはっきり書いてある。なのに守られない瞬間がある。理由はメモリは「読み込まれる情報」であって「実行される命令」ではないから。

LLM のコンテキストに入った時点で、無数のシステムプロンプト・過去の会話・ツール結果と並列に評価される。優先度の競合に負けると、ルールは無視される。

メモリは「思い出すための材料」であって、「強制力のある命令」ではない。命令にしたいなら実行ルートそのものを書き換える必要がある


解決策: 3層で完成宣言を物理的に止める

メモリで止まらないなら、3つのレイヤーで段階的に止める。

各層の役割は以下の通り。

レイヤー 強制力 目的
L1: Hooks 強(コマンド単位) ビルド系コマンド成功時に「完成じゃない」と物理的に警告
L2: Skill 中(タスク単位) 「完成宣言を出す前に E2E チェックリストを通る」スキルを定義
L3: 自動E2E 強(動線単位) dev server 起動を検知して Playwright で自動アクセス

L1 が一番手っ取り早く、L3 が一番強い。順に説明する。


L1: Hooks で「ビルド成功=完成」を物理的に否定する

Claude Code の Hooks は、ツールコールの前後に任意のシェルコマンドを差し込める仕組み。PostToolUsenpm run build 系コマンドの直後に警告を流し込む。

これがコピペで動く完成形。

~/.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$CLAUDE_TOOL_INPUT\" | grep -qE '(npm|pnpm|bun|yarn) (run )?(build|type-check|tsc|lint)'; then echo '⚠ ビルド/型チェック/lint 成功は完成ではない。dev server を起動して実画面を踏むまで「完成」と言うな。' >&2; fi"
          }
        ]
      }
    ]
  }
}

このフックが入ると、AI が npm run build を実行した直後に標準エラー出力に警告が流れる。Claude Code はツール結果と一緒にこの警告を読むので、「あ、ビルド通っただけじゃダメだった」と気付くチャンスが生まれる

完璧ではない(無視されることもある)が、メモリだけよりは2〜3倍効く体感。

💡 Tips: >&2 で標準エラーに出すのがポイント。Claude Code はツールの stderr もコンテキストに入れる。stdout と分けることで「これは警告だ」と認識しやすくなる。

Hooks のもう一段強い使い方

「警告」では弱い、「強制ブロック」したい場合は exit code を 2 で返す。

if [ "$BUILD_SUCCEEDED" = "true" ] && [ -z "$E2E_DONE" ]; then
  echo "E2E未実施。dev server起動とブラウザ確認を完了してから再試行してください。" >&2
  exit 2
fi

exit code 2 は Claude Code に「このアクションは止めるべき」と伝えるシグナル。AI は中断して再考する。


L2: Skill で完成宣言の前に E2E を強制起動する

Hooks はコマンド単位。「タスクの完了報告」というイベントを直接掴むのは難しい。そこで Skill(Claude Code の ~/.claude/skills/ 配下に置く手順書)を作る。

~/.claude/skills/done-check/SKILL.md
---
description: 「完成」「完了」「動きます」と報告する前に必ず通す検証チェックリスト。タスクが終わったタイミングで自動起動する。
---

# done-check: 完成宣言前の必須チェック

完成・完了の報告を出す前に、以下を全て実施する。1つでも未実施なら報告しない。

## チェックリスト

1. [ ] ビルドが通る (`npm run build` 等)
2. [ ] 型チェックが通る (`tsc --noEmit` 等)
3. [ ] dev server を起動した (`npm run dev`)
4. [ ] dev server が HTTP 200 を返す (`curl http://localhost:PORT`)
5. [ ] **Playwright で実画面を1動線踏んだ**
6. [ ] エラーが console に出ていないことを確認した

## 失敗時の対応

5番が実施できない場合(MCPブラウザが他セッションで占有中など):
- 「Playwrightで踏めなかった」と明示する
- 「ブラウザで自分で確認してください」と頼む
- **絶対に「完成しました」と言わない**

このスキルを定義しておくと、/done-check で明示起動できる。さらに Claude Code のメモリに「タスク完了直前に done-check スキルを必ず起動」と書けば、自動起動の確率が上がる。

スキル化のメリットはチェックリストが明文化されること。AI は曖昧なルールより構造化されたチェックリストの方が守りやすい。


L3: Playwright で自動踏み込みする

ここが本丸。Hooks と Skill は「AI に思い出させる」止まりだが、L3 は実際にブラウザを開いて踏むところまで自動化する。

Claude Code には Playwright MCP が標準で使える(mcp__playwright__browser_navigate 等のツールが提供される)。これを使った最低限の自動 E2E は以下のような形。

e2e_smoke.py
"""
dev server 起動後の最小スモークテスト。
- トップページにアクセスして HTTP 200 を確認
- メインCTAをクリックして遷移先のレスポンスを確認
- console エラーが出ていないか確認
"""
import asyncio
from playwright.async_api import async_playwright

async def smoke_test(url: str = "http://localhost:3000") -> dict:
    errors = []
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()
        page.on("console", lambda msg: errors.append(msg.text) if msg.type == "error" else None)
        page.on("pageerror", lambda exc: errors.append(str(exc)))

        # トップ表示確認
        response = await page.goto(url, wait_until="networkidle")
        if response.status != 200:
            return {"ok": False, "reason": f"HTTP {response.status}"}

        # メインCTAをクリックして次画面が出るか確認
        cta = await page.query_selector('button:has-text("次へ"), a:has-text("始める")')
        if cta:
            await cta.click()
            await page.wait_for_load_state("networkidle", timeout=5000)

        await browser.close()
    return {"ok": len(errors) == 0, "console_errors": errors}

if __name__ == "__main__":
    result = asyncio.run(smoke_test())
    print(result)
    exit(0 if result["ok"] else 1)

これを Hooks の PostToolUse(npm run dev 起動後) で叩けば、dev server が立った瞬間に自動でブラウザ踏み込み + console エラー検知まで走る。

運用上の注意点

L3 を Hooks に組み込むと、npm run dev のたびにテストが走って若干重い。実運用では:

  • 「完成」「完了」を AI が言いそうなタイミング(タスクの最終ターン)だけ走らせる
  • スキル done-check の中から呼ぶ
  • 失敗したら exit 2 で AI を止める

の組み合わせが現実的。

Playwright MCP は1つのブラウザインスタンスを使うので、複数 Claude Code セッションを並列で動かしてると「Browser is already in use」エラーが出る。--isolated フラグで分離するか、E2E はメインセッションだけで走らせる。


メモリ48個を運用して分かった「効くルール」と「効かないルール」

Claude Code の自動メモリ機能を1ヶ月以上、48個まで育ててきた経験で言うと、行動矯正系のルールには効くやつと効かないやつがある。違いは「実行ルートに割り込めるか」。

効くルール

  • ツールコールのタイミングで割り込めるもの — Hooks 経由で物理的に warn/block できる
  • チェックリスト形式に分解できるもの — Skill にして明文化すると守られる
  • 失敗のコストが目に見えるもの — exit code 2 で止まる、テスト落ちる、CI が赤くなる

効かないルール

  • 「これはしない方がいい」系の禁則 — メモリに書いても無視されがち
  • 抽象的な行動指針 — 「丁寧に」「慎重に」では行動が変わらない
  • 守るタイミングが曖昧なもの — 「いつ」発動すべきかが不明瞭だとスキップされる

💡 Tips: メモリは「思い出させる装置」、Hooks は「物理的に止める装置」、Skill は「手順を強制する装置」。役割を切り分けると、ルールが増えても破綻しない。


まとめ

「型チェック通ったから完成です」は Claude Code のサボりではなく、LLM の構造的なバイアス。メモリにルールを書くだけでは止まらない。止めるには Hooks / Skill / 自動E2E の3層で実行ルートそのものを書き換える必要がある。

今日からやること

  1. 今日(5分): ~/.claude/settings.json の Hooks に npm run build 後の警告だけ追加する。コピペで動く。
  2. 今週(1時間): ~/.claude/skills/done-check/SKILL.md を作って完成チェックリストを明文化する。Playwright を入れていないプロジェクトなら npm i -D @playwright/test && npx playwright install だけ走らせる。
  3. 今月(半日): Playwright スモークテストを書いて、dev server 起動時に自動実行する仕組みを組み込む。最小1動線(ログイン→ダッシュボード等)から始める。

「動かんものを渡すな」と Claude Code に毎回言う代わりに、動かないものを渡せない仕組みを1日で作れる。


参考ソース:

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?