
「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つを確実に守りたいとする。
- ファイルを変更するたびに型チェックとlintが走る
src/config/以下のファイルは変更させない- コンポーネントごとの規約をファイルの種類で自動的に適用する
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の書き方を知る