はじめに
Claude Code を半年くらい使ってきて、正直しんどいなと思うことがあった。
そして、僕は気付いた「Aiがより人間らしくなっている」と
どうしたAIに気持ちよく仕事してもらえるか、
どうしたらAIをハンドリングできるかと考えてみた
AIが同じミスを何度も繰り返す。
「Co-Authored-By つけないで」と言っても次のセッションでまたつける。「スコープ外のファイル触るな」と言っても lint エラーを見つけると勝手に直す。セッションをまたぐと記憶がリセットされるから、毎回同じ注意をしないといけない。
まるで小さい子に何かを教える間隔を覚えた。小さい子にもの教える時、ほとんどの場合1回では学習できない。そんな時にどうするか?を考えた。
例えば、お菓子は1日に1つしか食べてはいけない約束でも小さい子は食べたいと思えば2,3と食べてしまう…
こんな自分が時親ならどうするかを考えて欲しい。
お菓子を子供の手の届かないところに置いたり、そもそもお菓子を1日1つしか買わな買ったりするだろう
つまりお菓子が1つしか渡されない(物理的に不可能)という状況を作り上げている。要するに「仕組み化」している。
ここで僕はAIも同じように考えられないか?失敗してしまう理由を考えた
これってプロンプトで解決する問題じゃないのでは?と過程してその他の方法で「仕組み化」できないか?
を考えて試行錯誤したので共有します。
きっとこれからのAi時代を生き抜くうえで必要になる考え方になると思う。
「お願い」だけの世界:
人間「やるな」→ AI → ミス → 人間「だからやるなって」→ AI → ミス → ... (無限ループ)
「仕組み化」した世界:
+---------------------------+
| ハーネス(Hook) |
| |
人間 --> AI ----> | NG なこと --> ブロック |----> 安全な出力
| 毎回やること --> 自動実行 |
| 品質チェック --> 失敗なら戻す |
+---------------------------+
ハーネス = AI を囲む「柵」。お菓子を手の届かない場所に置くのと同じ発想で、AIが間違えられない状況を仕組みで作る。
この記事では、実際に Claude Code の Hook を使ってハーネスを構築した過程を、ハマりどころも含めて記録する。
まず、AIのミスを数えてみた
「なんか同じミス多いな」と感覚で思っていたけど、実際にどれくらいなのか気になったので、過去のセッション履歴(JSONL)を grep して調べてみた。
| ミスパターン | 発生回数 |
|---|---|
| Co-Authored-By を勝手につける | 22回以上 |
| スコープ外の lint エラーを勝手に直す | 15件以上 |
| PR概要にファイル変更一覧を盛りすぎ | 32件 |
| テスト全 SKIP を「テスト通過」と報告 | 3件 |
| --no-verify を自分で提案してゲート迂回 | 2〜3回 |
| デバッグでユーザーに「確認して」ループ | 5件以上 |
| Dockerfile でバージョン固定 → 後でビルド壊れる | 1件 |
| これは半年程度、現場で使用し間違えを指摘した回数。 | |
| 22回って。22回「いらない」って言ったのか、自分... |
根本原因を整理するとこう:
プロンプトで「やるな」と言う
|
v
AI は理解する(そのセッション内では)
|
v
セッション終了 --> 記憶リセット
|
v
次のセッションでまた同じミス <-- ここが問題
今回の修正をするまではCLAUDE.mdに私が指摘した場合はそのフローを見直せ、また強い口調で指摘した場合は是正しろって書いて運用していた。
しかし、それだけでは一つも良くならず、日頃のイライラが溜まっていく一方…
要するに親が子供に何度も同じことを言っている状態と同じ。
AIはCLAUDE.md に書いておけば毎回読むけど、それでも「例外」を作り出す。lint エラーが出ると「スコープ外だけど簡単な修正なのでやっておきました」みたいなことを言い出す。
まるで、決まり事を書いている紙を子供に渡してその決まり事を破ってしまうのと同じ。
お願いだけでなく、「仕組み化」する。方法は冒頭で話したように物理的に不可能(強制)な状況を作り出す必要がある。
これをAIに置き換えると…
プロンプトは「お願い」、Hook は「強制」。 お願いで22回ダメだったんだから、強制するしかない。
作ったもの全体像
最終的にこういう構成になった。
AI が何かしようとする
|
v
[Gate 1] ファイル書く前 (PreToolUse)
- .env 書き換え --> BLOCK
- リンター設定変更 --> BLOCK
- プラン未承認で実装開始 --> BLOCK
|
v
[Gate 2] git 操作前 (PreToolUse)
- --no-verify --> BLOCK
- main に直コミット --> BLOCK
- レビュー未完了でコミット --> BLOCK
|
v
(実行される)
|
v
[Gate 3] ファイル書いた後 (PostToolUse)
- 自動フォーマット (goimports / oxlint)
- 自動リント --> エラーに WHY/FIX/REF ヒント注入
- 対応テスト自動実行
|
v
[Gate 4] 会話が長くなったら (PreCompact)
- git 状態を JSON で自動保存 (記憶喪失対策)
|
v
[Gate 5]「できました」(Stop)
- ユニットテスト自動実行
- E2E テスト自動実行 (サーバー起動中のみ)
- テスト全 SKIP --> BLOCK (SKIP は PASS じゃない)
- テスト失敗 --> BLOCK, 修正に戻される
|
v
ユーザーに返答 (ここまで来たら安全)
全部 Python で書いた。1ファイル = 1 Hook で、~/.claude/hooks/ に置いてある。
Week 1: 基盤を作る
CLAUDE.md は「ポインター」にする
最初にやりがちな失敗は、CLAUDE.md にルールを全部書くこと。150行を超えると AI が先頭のルールばかり重視する「primacy bias」が発生するらしい。
150行以上書くとAIは馬鹿になる
自分の CLAUDE.md は50行以内に抑えている。詳細は ADR(Architecture Decision Records)に書いて、CLAUDE.md からは参照するだけ。
CLAUDE.md (50行以内)
- "ブロックされたら WHY/FIX/REF メッセージに従え"
- "詳細は ~/.claude/adr/ を参照"
- --> ルールの「何」だけ書く
|
| 参照
v
ADR (1ルール = 1ファイル)
- ADR-001: エラーは WHY/FIX/REF
- ADR-002: Step0 強制
- ADR-003: main コミット禁止
- ADR-004: archgate パターン
- --> ルールの「なぜ」を書く
Hook のエラーメッセージは WHY/FIX/REF 形式
ブロックするだけだと AI が「なぜダメなのか」分からず推測で対処しようとする。エラーメッセージに3つの情報を入れる:
WHY: なぜブロックされたか
FIX: 何をすれば解除されるか
REF: 根拠となる ADR やドキュメント
これだけで AI の自律回復率がかなり上がった。「ブロックされた → ADR 読む → 正しい対処をする」のループが回る。
Weeks 2-4: 品質ゲートを強化する
Stop Hook — 「できました」を信用しない
一番効果があったのがこれ。AI が「完了しました」と言おうとした瞬間にテストを強制実行する。
AI「実装完了しました!」
|
v
Stop Hook 発火
1. git status で変更ファイルを取得
2. ファイルパスから go.mod / package.json を遡ってプロジェクト特定
3. go test ./... or pnpm test を実行
4. 全 SKIP 検知 (SKIP は PASS じゃない!)
5. dev サーバー起動中なら E2E テストも実行
|
+-- テスト全パス --> 「完了しました」がユーザーに届く
+-- テスト失敗 --> BLOCK, AI は修正に戻される
テスト全 SKIP の罠
Go では全テストが SKIP でもサマリー行に PASS が出力される。これを見て AI が「テスト通過」と判断したことが3回あった。
判定には --- PASS(テスト関数ごとの結果行)の有無を使う。--- SKIP があって --- PASS がなければ「全 SKIP」。
E2E テスト — サーバーが動いてるときだけ
E2E テストはインフラが必要なので、無条件に実行すると壊れる。localhost:3000 に HTTP GET(2秒タイムアウト)してみて、応答があればテスト実行、なければスキップ。
E2E 判定フロー:
変更ファイルが src/features/ src/components/ 等にマッチ?
+-- No --> E2E スキップ
+-- Yes --> localhost:3000 が応答する?
+-- No --> E2E スキップ (エラーにはしない)
+-- Yes --> npx playwright test 実行
--no-verify ブロック — セキュリティゲートを守る
これは地味だけど大事。--no-verify は git commit の pre-commit hook をスキップするオプション。つまり、せっかく作った品質ゲートを全部素通りさせるスイッチ。
AI が「hook が失敗するので --no-verify で通しましょうか?」と提案してきたことが複数回あった。それ、ダメなやつ。
PreToolUse Hook で Bash コマンドの中身を検査して、--no-verify か --no-gpg-sign が含まれていたら即ブロック。
コンテキスト圧縮対策 — PreCompact Hook
Claude Code は会話が長くなると古いメッセージを圧縮する。このとき「今どこまでやったか」の詳細が消える。
PreCompact Hook は圧縮が始まる直前に発火して、git の状態(ブランチ、最終コミット、未コミット変更)を JSON で自動保存する。
セッション開始
│
▼
作業中... 作業中... 作業中...
│
▼
コンテキスト上限に近づく
│
▼
PreCompact Hook 発火
--> session-handoff.json に以下を保存:
{
"saved_at": "2026-03-27T...",
"branch": "feat/new-feature",
"last_commit": "a7f3e91 ...",
"uncommitted_changes": [...]
}
--> サマリーを content で返す
│
▼
圧縮実行(古いメッセージが要約に置き換わる)
│
▼
作業継続(状態は保存済みなので見失わない)
なぜ JSON?
最初は Markdown で保存していたけど、AI が Markdown を書くと微妙にフォーマットが崩れることがある。JSON にしたら読み書きが安定した。
参考にした記事でも「JSON over Markdown」が推奨されている。
Months 2-3: archgate パターン
ここまでの仕組みで「ブロック」はできるようになった。でも AI がブロックされたとき、「なぜダメなのか」を理解しないと場当たり的な修正になる。
archgate は、リンターのエラーメッセージに ADR への参照を自動注入 する仕組み。
普通のリンターエラー:
service.go:12: import of 'infra/repo' not allowed
AI「じゃあ import 消すか...」 <-- 間違った対処
archgate 注入後:
service.go:12: import of 'infra/repo' not allowed
HINT: infra を直接 import するな。
domain の repository インターフェース経由でアクセスせよ。
WHY: Clean Architecture レイヤー境界違反
REF: ADR-004
AI「ADR-004 を読もう --> インターフェースを定義するのか」 <-- 正しい対処
実装は PostToolUse Hook の中で、リンター出力に正規表現マッチして HINT を追加するだけ。大したコード量じゃない。
# パターンにマッチしたら ADR 参照付きヒントを注入
ARCHGATE_HINTS = [
(r"infra.*not allowed",
"infra を直接 import するな。repository インターフェース経由でアクセスせよ。"
" WHY: Clean Architecture レイヤー境界違反 REF: ADR-004"),
(r"gorm.*not allowed",
"gorm を直接使うな。repository インターフェース経由。"
" WHY: DB 実装の詳細を隠蔽 REF: ADR-004"),
]
なぜこのアプローチか
| アプローチ | ブロック力 | AI の理解度 |
|---|---|---|
| プロンプトだけ | ☆☆☆☆☆ | ★★★★☆(その場限り) |
| Hook だけ | ★★★★★ | ☆☆☆☆☆(理由不明) |
| Hook + ADR | ★★★★★ | ★★★★★ |
| CI/CD だけ | ★★★☆☆ | ☆☆☆☆☆(事後) |
- Hook = 強制。絶対に通さない
- ADR = 説得。なぜダメかを理解させる
- CLAUDE.md = リマインダー。毎セッション読まれる
この三層で補完し合う。どれか一つだと穴がある。
ハマりどころ
Hook の出力プロトコルを間違えると何も起きない
最初 sys.exit(1) でブロックしようとして、何も起きなくてしばらく悩んだ。正しくは {"decision": "block", "reason": "..."} を stdout に出力して exit(0) する。exit code じゃなくて JSON の中身で制御する。
PreCompact の content を返さないと圧縮後に記憶が飛ぶ
PreCompact Hook が {} を返すと、圧縮後のサマリーに何も残らない。{"content": "ブランチ: feat/xxx, 最終コミット: ..."} のように、圧縮後も残したい情報を content フィールドで返す。
f-string の閉じ忘れ
f"FIX: {flag} を外せ" と書くべきところを "FIX: {flag} を外せ" にしてしまい、実際のエラーメッセージに {flag} がそのまま表示された。テスト書いてて気づいたけど、テストがなかったら本番で恥ずかしいことになっていた。
settings.json の登録方法
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "python3 ~/.claude/hooks/pre-commit-check.py" }]
},
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "python3 ~/.claude/hooks/pre-write-guard.py" }]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "python3 ~/.claude/hooks/post-write-quality.py" }]
}
],
"PreCompact": [
{
"hooks": [{ "type": "command", "command": "python3 ~/.claude/hooks/pre-compact-save.py" }]
}
],
"Stop": [
{
"hooks": [{ "type": "command", "command": "python3 ~/.claude/hooks/stop-quality-gate.py" }]
}
]
}
}
やってみて思ったこと
一番大切なことはAIに気持ちよく働いてもらえるように、危険なところにはガードレールを設置して、
致命的なミスを最小限にすること。人の手の中でハンドリングすること。
正直、ここまでやる必要あるのか?と途中で思った。Hook を書いてる時間でコード書けるじゃん、と。
でも振り返ると、22回「Co-Authored-By いらない」と言う時間 のほうがもったいなかった。1回 Hook を書けば永久に解決する。
あと、AIのミスパターンを数値で見たのが良かった。「なんか多いな」と「22回」ではインパクトが違う。セッション履歴を grep するだけでできるので、ハーネスを作る前にまずミスを数えてみるのをおすすめする。
まだ完璧じゃない。「スコープ逸脱」は Hook では防げない(何がスコープ内かは文脈次第だから)。「cherry-pick のコンフリクト解消ミス」も意味論的な判断が必要で自動化等難しいところはたくさんある。でも、自動化できるところから潰していけば、人間が注意すべきことが減って、もっと大事なことに集中できる。
まとめ
- プロンプトで22回注意するくらいなら Hook を1本書け
- ブロックするだけじゃ足りない。WHY/FIX/REF で理由と対処を示す — AI の自律回復率が上がる
- ADR でルールの「なぜ」を残す — 6ヶ月後の自分にも、AIにも伝わる
- セッション管理は JSON — Markdown より壊れにくい。PreCompact で自動保存すれば記憶喪失を防げる
- まずミスを数えろ — 感覚じゃなくデータで優先度を決める
おわりに
今回AIが失敗から学びそして自立的に学習する仕組みを構築して思った。
すでにAIは人間に、人間はAIにお互いが近づいていると、
今回の仕組み化なんて良い例で、冒頭でも伝えたように小さい子にもの教えるのと同じ原理だった。
子供を育てるようにAIを育てると愛着も湧き、生産性も上がると。
昨今のAIの成長は早いので、今回紹介した方法は1ヶ月後いや、2週間後、もしかしたらほんの数日後にはゴミ同様の情報になっているかも知れない…
今回の方法自体はゴミになる可能性はある。しかし、
考え方、思考プロセスは必ずこれからの時代を生き抜く糧になると思う