🥼

【Claude Code】Stop hook × claude-doctorでCLAUDE.mdを自動育成する仕組みを作った話

に公開

はじめに

Claude Code を使っていると、「また同じミスをしてしまった」と気づく瞬間があります。

その場合、同じミスを繰り返さないように rules にルール化してもらったり、CLAUDE.md に追記してもらうのが一般的だと思います。

ただ、これが地味に面倒です。毎回手動で「このルール、書いておいて」と指示するのは認知コストが高く、結局やらずに同じ失敗を繰り返すループに陥りがちです。

そこで skill 化や hook 化が選択肢に上がります。しかし、skill は発火しないケースがあります。

hook はさらに根本的な問題があって、「どんなルールを書くべきか」を事前に知っていないと hook 自体を設計できません。

つまり hook は既知のルールを自動適用する仕組みであり、過去のセッションを遡って改善余地を発見する一般的な仕組みとは言い切れません。
※ ただし、スクリプトを作成するなどして、「過去のセッションを分析してrulesを作る」ことはできますが、このように、ひと工夫する必要があります。

そこで今回、claude-doctorというツールを活用し、この問題の解決をしようと考えました。

https://github.com/millionco/claude-doctor

具体的には、ターンが終わるたびに過去の会話ログを自動診断し、CLAUDE.md に書くべきルール候補を勝手に蓄積してくれる仕組みを作成しました。

そこで、本記事では、Stop hook + claude-doctor の継続解析パイプラインを自環境に導入し、CLAUDE.md をデータドリブンに育てる方法を紹介しようと思います。

前提

こんな課題を感じている方向けの内容となります。

  • 🔁 似たような失敗(ファイルを全読せず編集、連続ツール失敗でも無理やり進む等)を何度も見かける
  • 📝 CLAUDE.md のルール追加が「なんとなく経験則」で、網羅性や効果が測れない
  • 🔍 claude-doctor を手動で叩くのが面倒
  • 💭 失敗ターンだけでなく「冗長な成功ターン」の改善余地も拾いたい

モチベーション

CLAUDE.md の運用を続けていると、ルール追加が「勘と気合」になっていきます。

失敗に気づいた瞬間はコーディング中で、CLAUDE.md を編集する余裕はない。後から思い出して書こうとしても、その頃には何を書くべきか曖昧になっている。など、原因は複数ありますが、結果、同じ失敗を繰り返しながら CLAUDE.md少しずつ陳腐化していきます。

さらに根本的な問題があります。自分が気づいていないパターンは、ルール化できないということです。

例えば、セッション中の失敗は目に入っても、過去何百件というセッションを横断して「3回に1回、ファイルを全読せずに編集している」といった傾向は人間には見えないなどが挙げられます。

一方で、~/.claude/projects/*/には自分の開発セッションの会話ログが大量に眠っています。

つまり、データはすでにあり、足りないのは、そのデータを自動的にルール候補へ変換する仕組みだけということになります。

このようなモチベーションから仕組み化を試みました。
ここからは実際の仕組み化の過程の解説となります。

実装と設計判断

ディレクトリ構成

スクリプトと出力レポートは以下のパスに配置します。

~/.claude/
├── hooks/
│   └── claude-doctor-continuous.sh   # Stop hook 本体
├── .cache/
│   └── claude-doctor-last-run        # スロットル用 mtime ファイル(自動生成)
├── claude-doctor-reports/            # レポート出力先(自動生成)
│   ├── 20250420-093012.json          # 生成ルール候補(JSON)
│   └── 20250420-093012.log           # stderr ログ
└── settings.json                     # hook 登録

スクリプト本体

まずスクリプト全体を示します。以降の設計説明ではこのコードの該当箇所を参照しながら各判断の理由を述べます。

~/.claude/hooks/claude-doctor-continuous.sh を作成します。

#!/bin/bash
# claude-doctor-continuous.sh
#
# Stop hook: At end of each turn, run claude-doctor to detect anti-patterns and emit rule candidates.
# Throttling (10 minutes) and background execution avoid impacting Claude Code UX.

# Discard stdin so the hook runtime does not wait for the pipe to close
cat > /dev/null

LAST_RUN_FILE="$HOME/.claude/.cache/claude-doctor-last-run"
REPORTS_DIR="$HOME/.claude/claude-doctor-reports"

# Ensure directories exist
mkdir -p "$(dirname "$LAST_RUN_FILE")" "$REPORTS_DIR" || true

# Skip if we already ran within the last 10 minutes
if [ -f "$LAST_RUN_FILE" ] && find "$LAST_RUN_FILE" -mmin -10 2>/dev/null | grep -q .; then
  exit 0
fi

# Record run time for the next throttle check
touch "$LAST_RUN_FILE" || true

# Output paths with timestamp
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
REPORT_JSON="$REPORTS_DIR/${TIMESTAMP}.json"
REPORT_LOG="$REPORTS_DIR/${TIMESTAMP}.log"

# Run claude-doctor in the background (close stdin for full detachment)
# Use perl alarm as a macOS-compatible timeout (no GNU coreutils required)
{
  perl -e 'alarm 120; exec @ARGV' -- \
    npx -y claude-doctor@latest --rules --save --json \
    > "$REPORT_JSON" \
    2> "$REPORT_LOG" \
    || true
} </dev/null &
disown

exit 0

作成したら実行権限を付与します。

chmod +x ~/.claude/hooks/claude-doctor-continuous.sh

設計判断① — なぜ Stop hook か

Claude Code の hook event は用途ごとに発火タイミングが異なりますが、今回は以下のhook eventを候補にし、Stopを採用することにしました。

hook event 発火タイミング 今回の用途への適性
PostToolUseFailure ツール失敗時のみ ❌ 成功ターンに動かない。失敗の粒度が小さすぎる
StopFailure API エラー終了時 △ 成功ターンの学習機会を取りこぼす
Stop ターン終了時(Claude の応答完了後) ✅ 成功・失敗問わず、1ターン1回、確定済みの作業を診断できる

Stop を選んだ理由は、ターン終了時点でそのターンのすべての操作が確定しているからです。

claude-doctor は「1 つのターン全体を見てパターンを抽出する」ツールなので、失敗ターンだけを対象にすると成功ターンに潜む冗長なパターンを見落とします。

設計判断② — スロットル(10 分制限)

Stop フックはターンが完了するたびに呼び出されます。セッションが活発な場合、20 回ターンを重ねれば claude-doctor も 20 回起動することになります。

これは無駄な処理であり、診断に数秒かかるとしたら体験も損ないます。そこで「直前の起動から 10 分以内であれば、今回の診断はスキップする」という制限を設けています。

仕組みとしてはタイムスタンプファイルで「最後に起動した時刻」を記録するという方法を採用しています。

スクリプトは起動のたびに $LAST_RUN_FILE というファイルを touch コマンドで更新します
これにより「最終更新時刻」を現在時刻に書き換えることができます。

そのため、次回呼び出された際は、このファイルの更新時刻を確認し、10 分以内であればすぐ終了します。

Stop フック呼び出し


タイムスタンプファイルが存在するか?

   Yes ─── 最終更新が 10 分以内か? ─── Yes ──→ exit 0(スキップ)
       │           │
   No  │           No
       │           │
       └───────────┘

touch でタイムスタンプ更新 → claude-doctor 起動

実際にこの制御を担っているのが以下のコードです。

if [ -f "$LAST_RUN_FILE" ] && find "$LAST_RUN_FILE" -mmin -10 2>/dev/null | grep -q .; then
  exit 0
fi

各部分の意味を順に説明します。

  • -f "$LAST_RUN_FILE" — タイムスタンプファイルが存在するかを確認します。初回起動時はファイルがないため、このブロックはスキップされます
  • find "$LAST_RUN_FILE" -mmin -10-mmin -10 は「最終更新が 10 分未満のファイルを探す」という意味です。mmin は "modified minutes"(更新からの経過分数)の略で、-10 は「10 分より小さい=10 分以内」を表します
  • grep -q .find の結果が 1 行以上あれば(ファイルが見つかれば)真と判定します
  • exit 0 — 診断を実行せず、正常終了します

条件を満たした場合は即座に exit 0 するため、touch によるタイムスタンプ更新は診断起動の直前に行います。

これにより「診断の完了を待たず、起動した瞬間からインターバルのカウントが始まる」動作になります。結果として、20 回ターンを重ねても最初の診断から 10 分が経過するまで claude-doctor は 1 回しか起動しません。

設計判断③ — バックグラウンド detach

まず前提として、Claude Code のフックは同期実行です。フックスクリプトが終了するまで Claude Code は次の応答を返しません。つまり何も工夫しなければ、ターンが終わるたびに npx のダウンロード+スキャンの数秒間、画面がフリーズし続けます。

この問題を解決するために、スクリプト冒頭では Claude Code がフックに渡す JSON ペイロードを読み捨てています。

cat > /dev/null

これは定型句です。読み飛ばすと標準入力のパイプが詰まり、フック全体が応答しないまま固まってしまいます(いわゆるフリーズ状態です)。

本題の非同期化は、スクリプト末尾の以下の部分で実現しています。

{
  perl -e 'alarm 120; exec @ARGV' -- \
    npx -y claude-doctor@latest --rules --save --json \
    > "$REPORT_JSON" 2> "$REPORT_LOG" \
    || true
} </dev/null &
disown

一見複雑に見えますが、それぞれが独立した 1 つの問題を解決するパーツです。

  • & : コマンドをバックグラウンドジョブとして起動します。これだけで Claude Code はスキャンの完了を待たずに応答を返せるようになります
  • disown : バックグラウンドジョブをシェルのジョブテーブルから切り離します。これがないと、Claude Code のシェルセッションが終了したときに SIGHUP がバックグラウンドプロセスに送られ、スキャンが中断されることがあります
  • </dev/null : バックグラウンドプロセスの標準入力をクローズします。ターミナルから切り離した後もプロセスが端末入力を待ち続けるのを防ぎます
  • perl -e 'alarm 120' : 120 秒のタイムアウトを設けます。npx の取得に失敗したり、スキャンが応答不能になったりした場合に、プロセスがゾンビとして残り続けるのを防ぐための保険です

設計判断④ — CLAUDE.md に自動追記しない理由

claude-doctor の出力をそのまま CLAUDE.md に書き込む自動化は技術的には可能ですが、3 つの具体的なリスクから採用しませんでした。

  1. 矛盾の混入: たとえば既存の CLAUDE.md に「コメントは英語で書く」と定義されているプロジェクトで、診断ツールが「日本語コメントを推奨」という提案を自動追記したとします。Claude Code は相反する指示を同時に受け取ることになり、どちらに従うかが不定になります。

  2. 過剰汎化: 特定のファイルでのみ有効な命名パターンを診断ツールが検出し「プロジェクト全体のルール」として書き込んだ場合、そのルールが本来関係ないコンポーネントにも適用され、誤った制約として機能します。

  3. 幻覚の固定化: 診断ツール自体が誤検知した内容をCLAUDE.md に書き込むと、以降の全セッションでその誤ったルールが有効になります。手動で発見して巻き戻すまでの間、静かに悪影響を与え続けます。

そこでこの仕組みでは、レポートをファイルに書き出すだけに留めています。

意図したワークフローは「生成 → 人間がレビュー → 取り込む項目だけを選んで CLAUDE.md に貼り付ける / rules/に切り出す」という 3 ステップです。自動化を諦めたのではなく、「採否の判断を人間が持つ」こと自体を設計の核心として位置づけています。

レポートをファイルに書き出すだけに留め、CLAUDE.md/rules/ への反映は開発者が内容を確認した上で判断する設計としています。この割り切りがこの仕組みで最も大切な設計判断と私は考えています。

settings.json への登録

~/.claude/settings.jsonhooks.Stop 配列に以下を追加します。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/claude-doctor-continuous.sh"
          }
        ]
      }
    ]
  }
}

既存の Stop エントリがある場合は配列にマージしてください。

実際の動作

実際に動かしてみたところ、206 sessions / 13 projects が解析対象となり、9 件のルール候補が生成されました。
※ Claude Codeのグローバルhookとして実施したため、読み込むセッションとプロジェクトが多くなっています。

claude-doctor が出力するレポートの中身は以下のようなテキストです(~/.claude/claude-doctor-reports/*.json に保存されます)。

Model saved to .../.claude-doctor/ (206 sessions, 13 projects)

## Rules (auto-generated by claude-doctor)

Based on analysis of 206 sessions. Paste into your CLAUDE.md or AGENTS.md.

- Double-check your output before presenting it. Verify that your changes actually address what the user asked for.
- When the user corrects you, stop and re-read their message. Quote back what they asked for and confirm before proceeding.
- Re-read the user's last message before responding. Follow through on every instruction completely.
- Read the full file before editing. Plan all changes, then make ONE complete edit. If you've edited a file 3+ times, stop and re-read the user's requirements.
- When stuck, summarize what you've tried and ask the user for guidance instead of retrying the same approach.
- Every few turns, re-read the original request to make sure you haven't drifted from the goal.
- Act sooner. Don't read more than 3-5 files before making a change. Get a basic understanding, make the change, then iterate.
- After 2 consecutive tool failures, stop and change your approach entirely. Explain what failed and try a different strategy.
- Break work into small, verifiable steps. Confirm your approach with the user before making large changes.

いずれのパターンも思い当たる節が私にもあったため、今回は全部採用として良さそうです。
CLAUDE.mdrulesへの追加についてもClaude Codeに行わせる想定です。

だからこそ、人間の目でどれが必要かを判断するフェーズが必要です。
なんでもとりあえず追加してしまえば、コンテキストウィンドウの圧迫にもつながりかねないためです。

今後の展望

あまりにも自動生成のrules候補が多い場合の対策をしていこうと考えています。

例えば、Claude Codeにレビューさせて効果が高いもののみ取り入れるなどをSkillやSubagentsなどで仕組み化すると半自動的に人間を介することなく運用することもできそうです。

おわりに

CLAUDE.mdやrulesといった基本ハーネスを育てる作業は、結局のところ自分の開発スタイルを言語化する作業だと感じました。

Stop hook + claude-doctor の組み合わせは、そのための「自動メモ取り係」として予想以上に効いています。

気になった方はぜひ手元で試してみてください。

参考文献

https://docs.anthropic.com/en/docs/claude-code/hooks

https://github.com/millionco/claude-doctor

Discussion