ankuro.dev
← ブログ一覧に戻る
「CLAUDE.mdだけでは足りない」——Claude Codeをハーネスで制御する
2026-04-03#Claude Code#AI#Hooks#CLAUDE.md#自動化#アーキテクチャ

「CLAUDE.mdだけでは足りない」——Claude Codeをハーネスで制御する

CLAUDE.mdを書いた人なら一度は経験する。

「lintを必ず実行してください」と書いたのに、Claudeがlintを実行せずにコードを渡してくる。「このディレクトリは変更しないでください」と書いたのに、いつの間にか変更されている。

もっと丁寧に書けば解決するのか? 強調すれば? セッションの冒頭で念押しすれば?

答えはほぼNOだ。問題はCLAUDE.mdの書き方ではなく、CLAUDE.mdがそもそも「お願い」でしかないという本質的な限界にある。


CLAUDE.mdが「お願い」である理由

CLAUDE.mdはClaude Codeが起動するたびに自動で読み込まれる、優れた仕組みだ。プロジェクトのコンテキスト・技術スタック・コーディング規約を伝えるには十分機能する。

ただし、CLAUDE.mdの内容はあくまでClaudeへのインプットだ。Claudeはそれを読んで理解し、回答の参考にする。必ず従うわけではない。

Anthropicのドキュメントにはこう書かれている。

When deterministic compliance is required, prompt instructions alone have a non-zero failure rate.

「確実な遵守が必要な場面では、プロンプトの指示だけでは失敗率がゼロにならない」

CLAUDE.mdに「lintを必ず実行して」と書いても、それはClaudeへのお願いだ。会話が長くなれば忘れることもある。急いでいるときはスキップすることもある。確率的な動作をするモデルに対して、確実な動作を「文章で」求めることには限界がある。


ハーネスエンジニアリングという考え方

この限界を突破するアプローチがハーネスエンジニアリングだ。

「ハーネス(harness)」は馬具を指す言葉で、馬の力を制御・方向づける道具のこと。AIに当てはめると、モデルの周囲に配置する制御の仕組み全体を指す。

Agent = Model + Harness

ハーネスエンジニアリングとは、Claudeへの「お願い」に頼るのではなく、仕組みとして確実に起きる動作を設計することだ。

Claude Codeで使える主なハーネスの構成要素は3つある。

構成要素 役割 強制力
.claude/rules/ 条件付きでルールをロードする お願い(ただし構造化)
Hooks ツール実行の前後にコマンドを差し込む 確実に実行される
フィードバックループ 型チェック・lint・テストを自動で回す 確実に実行される

CLAUDE.mdが「常時読み込まれるお願い」なのに対し、HooksとフィードバックループはClaudeの判断を介さずに必ず実行される。ここが決定的な違いだ。


実装してみる

TypeScriptのプロジェクトで、以下の3つを確実に守りたいとする。

  1. ファイルを変更するたびに型チェックとlintが走る
  2. src/config/以下のファイルは変更させない
  3. コンポーネントごとの規約をファイルの種類で自動的に適用する

CLAUDE.mdにこれらを書くだけでは不十分だ。ハーネスを使って実装する。


① .claude/rules/ でルールを構造化する

CLAUDE.mdに全ルールを書くと肥大化する。.claude/rules/以下にファイルを分けると、glob パターンで特定のファイルを編集するときだけ読み込まれる条件付きルールが作れる。

.claude/
  rules/
    components.md    ← src/components/** を編集するときだけロード
    api.md           ← src/api/** を編集するときだけロード

components.mdの例:

---
paths: ["src/components/**/*"]
---

# コンポーネント規約

- named export を使う(default export 禁止)
- props の型定義は同ファイルの先頭に書く
- スタイルは CSS Modules を使う(インラインスタイル禁止)
- コンポーネントは 1 ファイル 1 コンポーネント

CLAUDE.mdに全部書くのとの違いは、APIのコードを書いているときにコンポーネントの規約が読み込まれないこと。不要なコンテキストを渡さないため、Claudeの判断精度が上がる。

ただしこれもまだ「お願い」であることは変わらない。次のHooksで初めて「強制」になる。


② Hooks でツール実行を制御する

HooksはClaude Codeがツールを実行する前後にシェルコマンドを差し込む仕組みだ。Claude Codeが何かをしようとするタイミングを捕まえて、割り込める。

設定は.claude/settings.jsonに書く。

PreToolUse:ツール実行の直前に走る。exitコードが0以外だとツールの実行がキャンセルされる。

PostToolUse:ツール実行の直後に走る。後処理・検証・通知に使う。

src/config/ の保護

src/config/への書き込みを物理的にブロックする:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$CLAUDE_TOOL_INPUT\" | python3 -c \"import sys,json; p=json.load(sys.stdin).get('path',''); exit(1 if 'src/config/' in p else 0)\" 2>/dev/null; then :; else echo 'src/config/ は変更禁止です' >&2; exit 1; fi"
          }
        ]
      }
    ]
  }
}

$CLAUDE_TOOL_INPUTにはClaudeがツールに渡そうとしているJSONが入っている。src/config/へのパスが含まれていたらexit 1を返し、ツールの実行をキャンセルする。

Claudeがどう判断しようとも、このフックが動く限りsrc/config/のファイルは変更されない。プロンプトで「変更しないでください」と書くのとは根本的に違う。


③ フィードバックループで自動検証する

PostToolUseフックで型チェックとlintをファイル保存のたびに自動実行する:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(echo \"$CLAUDE_TOOL_INPUT\" | python3 -c \"import sys,json; print(json.load(sys.stdin).get('path',''))\" 2>/dev/null); if [[ \"$FILE\" == *.ts || \"$FILE\" == *.tsx ]]; then echo '--- 型チェック ---' && npx tsc --noEmit && echo '--- ESLint ---' && npx eslint \"$FILE\" --max-warnings 0; fi"
          }
        ]
      }
    ]
  }
}

.tsまたは.tsxファイルが変更されるたびに型チェックとESLintが走る。エラーがあればClaude Codeのターミナルに出力されるので、Claudeがエラーを読んで次のアクションを修正できる。

「lintを実行してください」と毎回頼む必要がなくなる。ファイルが変わった瞬間に自動で実行される。


完成形の settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "if echo \"$CLAUDE_TOOL_INPUT\" | python3 -c \"import sys,json; p=json.load(sys.stdin).get('path',''); exit(1 if 'src/config/' in p else 0)\" 2>/dev/null; then :; else echo 'src/config/ は変更禁止です' >&2; exit 1; fi"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "FILE=$(echo \"$CLAUDE_TOOL_INPUT\" | python3 -c \"import sys,json; print(json.load(sys.stdin).get('path',''))\" 2>/dev/null); if [[ \"$FILE\" == *.ts || \"$FILE\" == *.tsx ]]; then npx tsc --noEmit && npx eslint \"$FILE\" --max-warnings 0; fi"
          }
        ]
      }
    ]
  }
}

これで「Claudeに頼まなくても自動で起きること」が3つできた。

  • src/config/への変更は物理的にブロックされる
  • .ts/.tsx`ファイルの変更後は型チェックとlintが自動で走る
  • コンポーネントを編集するときはコンポーネント固有の規約が自動でロードされる

プロンプト vs ハーネスの判断軸

全部をハーネスで制御しようとすると複雑になりすぎる。使い分けの基準はシンプルだ。

ハーネスで制御すべきもの:

  • 繰り返し発生する・毎回確実に起きてほしい動作
  • 失敗したときのコストが高い操作(本番ファイルの変更・重要なコマンド)
  • チーム全員に一律で適用したいルール

プロンプトで十分なもの:

  • 一時的な指示(「今回だけこの形式で出して」)
  • 文脈に応じてClaudeに判断してほしい部分
  • まだルールが固まっていない実験段階

一言で言うと「繰り返し発生する・確実に守られる必要がある」はハーネス、それ以外はプロンプト。


段階的に始める

いきなり全部入りを目指すと複雑になって失敗しやすい。段階的に育てるのが現実的だ。

Day 1:CLAUDE.mdを整理する
まずCLAUDE.mdをプロジェクト固有の情報だけに絞る。一般的なベストプラクティスは書かず、「このプロジェクト特有のルール」だけを60行以内にまとめる。

Week 1:よく使う規約をrulesに分ける
CLAUDE.mdが肥大化してきたら.claude/rules/に分割する。どのファイルを編集するときにどのルールが必要かを考えながらglobパターンを設定する。

Week 2:フィードバックループを追加する
「毎回手動でlintを実行している」「型エラーを後から気づく」という問題が出てきたらPostToolUseフックを追加する。まず1つのコマンドから始める。

Week 3以降:保護が必要なパスにPreToolUseを追加する
「絶対に触ってほしくないファイル」が出てきたらPreToolUseでブロックする。設定ファイル・環境変数ファイル・デプロイスクリプトなどが候補になる。


CLAUDE.mdは入り口、ハーネスは仕組み

CLAUDE.mdはClaude Codeをうまく使うための入り口として今も重要だ。ハーネスはCLAUDE.mdを否定するものではなく、その先にある。

CLAUDE.mdで「何をしてほしいか」を伝え、HooksとフィードバックループでClaudeの判断に依存しない部分を仕組みとして担保する。この組み合わせが、プロダクション品質のClaude Code環境を作る。


関連記事

Hooksをもっと詳しく知る

CLAUDE.mdの書き方を知る