Claude CodeでCLAUDE.mdやプロジェクト設定の動的読み込み・変更を監視したい人向け
チーム共通CLAUDE.mdの読み込み状況をSlackに飛ばして肥大化を可視化したい場面、監査要件でAIに渡したコンテキストをログ取りしたい場面、project_settingsの書き換えはPRレビュー必須にして勝手な書き換えを防ぎたい場面で、それぞれsettings.jsonのhooks配列にこの2つを書く
Claude Code のフックには「会話の流れに介入するもの」と「会話の外で起きる出来事を観測するもの」があって、InstructionsLoaded と ConfigChange は後者寄りの兄弟フックです。
前者は CLAUDE.md や .claude/rules/*.md が読み込まれた瞬間に発火し、後者は settings.json 系が書き換えられた瞬間に発火します。一見似ていますが、性格は正反対です。片方は「観測専用で止められない」、もう片方は「条件次第で変更そのものを止められる」。この違いを知らずに同じ感覚で書くと、止めたいものが止まらない、または通したいものまで詰まる、のどちらかが起きます。
噛み砕くと
InstructionsLoaded は、会議室に貼ってあるルール紙を Claude が読んだ瞬間に「いま読みました」と社外のホワイトボードに勝手にメモする係です。読むのを止めることはできません。ただ「読みましたよ」と記録するだけです。
一方 ConfigChange は、会議室の運用ルール集を誰かが書き換えようとした瞬間に立ちはだかる門番です。「その書き換えは PR レビュー必須なので通せません」と返せば、書き換え自体が無効になります。観測係と門番、と覚えると役割を取り違えません。
大事な前提:この2つは性格が真逆。片方は止められて、片方は止められない
公式 docs は InstructionsLoaded について「decision control を持たず、observability 目的で非同期に走る」と明言しています。ConfigChange 側は逆に、policy_settings という特殊な設定源を除けば、設定変更そのものを止められると明記されています。
この性格差を踏まえないと、CLAUDE.md の読み込み自体を条件で止めたいのに InstructionsLoaded を書いてしまう、みたいなズレが起きます。読み込みを制御したいなら settings.json の additionalDirectories の側で絞るのが正しい入り口です。
「複数の CLAUDE.md ロードを Slack に通知」を例に、実際の手順を見る
ステップ1: settings.json にフックを登録する
シナリオAは「セッション開始時に読み込まれた CLAUDE.md / .claude/rules/*.md の絶対 path と memory_type を Slack に飛ばす」です。プロジェクト直下の .claude/settings.json を開いて、hooks.InstructionsLoaded 配列を追加します。
{
"hooks": {
"InstructionsLoaded": [
{
"matcher": "session_start",
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/notify-claude-md-load.sh"
}
]
}
]
}
}
matcher を session_start に絞っているのが肝です。これを外すと nested_traversal や path_glob_match の lazy load も全部拾ってしまい、1セッション中に何十回も Slack 通知が飛びます。
ステップ2: 通知スクリプトを書く
~/.claude/scripts/notify-claude-md-load.sh を作って、標準入力で渡される JSON をパースして Slack に POST します。
#!/bin/bash
input=$(cat)
file=$(echo "$input" | jq -r '.file_path')
mtype=$(echo "$input" | jq -r '.memory_type')
curl -s -X POST -H 'Content-Type: application/json' \
-d "{\"text\":\"[$mtype] CLAUDE.md loaded: $file\"}" \
"$SLACK_WEBHOOK_URL"
exit 0
chmod +x で実行権限を付けるのを忘れない。
ステップ3: 入力 JSON の形を確認する
このフックが受け取る JSON は公式サンプル通りで、こんな形です。
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../transcript.jsonl",
"cwd": "~/my-project",
"hook_event_name": "InstructionsLoaded",
"file_path": "~/my-project/CLAUDE.md",
"memory_type": "Project",
"load_reason": "session_start"
}
共通フィールドに加えて、file_path / memory_type / load_reason の3つがこのフック専用に渡ります。memory_type は "User" / "Project" / "Local" / "Managed" の4種。lazy load の場合は globs や trigger_file_path も追加で入ってきます。共通フィールド側は session_id / transcript_path / cwd / hook_event_name の4つです。
ステップ4: ここで初心者がやりがちな勘違い
「読み込みが多すぎる CLAUDE.md は止めたい」と考えて、スクリプトの中で exit 2 や {"decision":"block"} を返したくなる場面が来ます。やってもムダです。公式 docs に「Exit code is ignored」「no decision control」と明記されている通り、InstructionsLoaded は何を返しても読み込みは止まりません。
止めたいなら settings.json 側の additionalDirectories や、そもそも該当 CLAUDE.md を消す、で対応します。
ステップ5: 3つ以上ロードされたら肥大化警告も出す
同じセッションで session_start が3回以上飛んできたら「CLAUDE.md が分散しすぎ」のサインです。スクリプト側でカウンタファイルを ~/.claude/state/ 配下に持って、3を超えたら別チャネルに警告を飛ばす、みたいな運用が組めます。
ステップ6: 確認
新しいセッションを開いて、Slack に「[Project] CLAUDE.md loaded: ...」が飛んでくれば成功です。lazy load 系まで観測したい場合は、フック定義を2本に分け、matcher を path_glob_match|nested_traversal に変えた別フックとして書いて、Slack の宛先チャネルも分けるのが運用上ラクです。
「project_settings の変更だけ block」を例に、ConfigChange の手順を見る
ステップ1: 何を止めて何を通すかを決める
シナリオBは「.claude/settings.json の書き換えだけは PR レビュー必須にして、ローカルの .claude/settings.local.json はそのまま通す」です。policy_settings は仕様上 block 不可なので触りません。
ステップ2: settings.json にフックを登録する
{
"hooks": {
"ConfigChange": [
{
"matcher": "project_settings",
"hooks": [
{
"type": "command",
"command": "~/.claude/scripts/block-project-settings.sh"
}
]
}
]
}
}
matcher は config_source の値で絞ります。user_settings / project_settings / local_settings / policy_settings / skills の5択。
ステップ3: block を返すスクリプトを書く
標準出力に {"decision":"block","reason":"..."} を出すと、設定変更が無効化されます。
#!/bin/bash
input=$(cat)
src=$(echo "$input" | jq -r '.config_source')
if [ "$src" = "project_settings" ]; then
echo '{"decision":"block","reason":"project settings の変更は PR レビュー必須です"}'
exit 0
fi
exit 0
あるいは exit 2 と stderr 出力でも block できます。どっちでもいい。
ステップ4: 入力 JSON の中身
このフックには共通フィールドに加えて、config_source / config_path / change_type の3つが入ります。
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../transcript.jsonl",
"cwd": "~/my-project",
"hook_event_name": "ConfigChange",
"config_source": "project_settings",
"config_path": "~/my-project/.claude/settings.json",
"change_type": "modified"
}
変更の出元を示す config_source、書き換わったファイル位置を示す config_path、操作種別を示す change_type の3軸で粗めに分岐できます。change_type は "modified" / "created" / "deleted" の3値。注意点として、現時点の公式 docs には「変更後/変更前の設定値オブジェクト」に当たるフィールドは記載されていません。disableAllHooks のような特定キーの値だけを見て分岐する書き方は docs 上に根拠がないので、できるのはあくまで「どのファイルがどう書き換わったか」までです。
ステップ5: local_settings はそのまま通す
個人ローカルの .claude/settings.local.json は手元の調整なので、わざわざ block しません。フック定義に local_settings 用の matcher を書かない、これだけで素通りします。
ステップ6: 確認
試しに .claude/settings.json を文章を書き換えるアプリで開いて適当な値を書き換えて保存してみると、Claude Code 側で「project settings の変更は PR レビュー必須です」と通知が出て、変更が反映されません。逆に .claude/settings.local.json を書き換えると、何の抵抗もなくそのまま反映されます。
つまり InstructionsLoaded / ConfigChange は何をしてくれるのか
- やってくれる: CLAUDE.md がロードされた瞬間の通知・監査ログ取り、
settings.json系の変更の事前 block。block 対象はpolicy_settings以外の出元です - やってくれない:
InstructionsLoadedでの読み込みキャンセル、ConfigChangeでのpolicy_settingsの block、CLAUDE.md の中身そのものの書き換え - 意味が薄い場面: 個人だけが触る小規模プロジェクトで Slack 連携も監査も不要なケース、
settings.jsonをほぼ触らない読者、組織配布のpolicy_settingsしか存在しないチーム
使いどころ3シナリオ
シナリオ1: チーム共通 CLAUDE.md が肥大化してきたチームの可視化
5人チームで CLAUDE.md と .claude/rules/*.md を3年運用していて、ファイル数が15個を超えた頃の話です。誰がいつどのルールファイルを足したかが追えなくなり、Claude の応答もブレ始める。ここで InstructionsLoaded の session_start マッチャーでロード数と path を Slack に飛ばしておけば「今日のセッションは .claude/rules/legacy-style.md まで読み込んでた、これ消していい?」みたいな会話が回せます。
シナリオ2: 監査要件のあるプロジェクトでの compliance ログ
金融系・医療系で「AI に何のコンテキストを渡したか」を記録する義務がある場合、InstructionsLoaded の matcher を session_start|nested_traversal|path_glob_match|include|compact の全種類を OR 連結した1本のフックで拾って、~/audit/ 配下にタイムスタンプ付きで JSONL 追記する運用が組めます。load_reason: compact も拾えるので /compact 後の再読み込みも漏れません。
シナリオ3: 「project の settings.json はそもそも自由に書き換えさせない」運用
誰かが Slack で「フック止める追加スイッチがあるらしいよ」と聞いて、軽い気持ちで .claude/settings.json を書き換えて保存し、セキュリティ系フックを丸ごと止めてしまうリスクがあります。値レベルの差分検査は公式 docs の Input スキーマで保証されていないので、ここは「config_source: project_settings かつ config_path がチーム共有の .claude/settings.json なら、change_type を問わず一律 block」という粗めの運用にします。書き換えたい場合は PR レビューで通す形に揃える、というガード。local_settings(個人ローカル)は別フックを書かないことで素通りさせます。
初心者が踏みやすい落とし穴
InstructionsLoadedで読み込みは止められない。公式 docs が「no decision control」「Exit code is ignored」と明言。exit 2や{"decision":"block"}を返してもログに残るだけで、CLAUDE.md は通常通り読み込まれます。- lazy load を絞らないと Slack 通知が地獄になる。matcher を指定せず雑に書くと
nested_traversalpath_glob_matchincludecompactまで全部拾って、1セッション中に数十回飛びます。観測目的なら matcher をsession_startに絞るのが定石。 compactload_reason の存在を見落とす。/compactの後に CLAUDE.md が静かに再ロードされる挙動があり、これもInstructionsLoadedで発火します。監査ログ目的ならこの値も対象に入れる。逆に「セッション開始時だけでいい」なら明示的に除外する。ConfigChangeのpolicy_settingsだけは block 不可。公式 docs に "(exceptpolicy_settings)" と明記。組織管理者が配布した設定を現場フックで握り潰す抜け道は塞がれている、というセキュリティ設計です。matcher: "policy_settings"で block を返しても無視されます。- v2.1.140 より古い Claude Code は symlinked settings バグ持ち。changelog (May 12, 2026) で「symlinked settings files が誤検知 + spurious
ConfigChangehook を発生させていた」と明記。古いバージョンで symlink 運用してる人はまずアップデート。 ConfigChangeの Input はconfig_source+config_path+change_typeの3フィールドで来る。updatedConfigやoldConfigのような変更前後の値オブジェクトは現時点で公式 docs に記載なし。「disableAllHooksが true になる時だけ block」のような値レベルの判定は docs 上に根拠が無いので避け、config_pathの場所とchange_typeの種類で粗めに絞る運用が安全です。ConfigChangeの exit code 2 も block 相当になる。公式 table verbatim「Yes / Blocks the configuration change from taking effect (except policy_settings)」。スクリプトのjqエラー・コマンド未インストール・ヒアドキュメント文法ミスで exit 2 終了すると、何もしていないのに「設定変更が常に block される」状態になり原因追跡が地獄。デバッグで詰まったらecho $?で終了コードを確認する癖を。- 2つを混同して同じ感覚で書く。
InstructionsLoadedは非同期・観測専用、ConfigChangeは同期・block 可能。性格が真逆。最初に「これは観測か、介入か」を1秒考えてからフックを書く癖をつけると事故が減ります。
書き方
settings.json の hooks.InstructionsLoaded[].matcher で load_reason を絞る / hooks.ConfigChange[].matcher で config_source を絞る
やってみるとこうなる
入力
{
"hooks": {
"InstructionsLoaded": [
{
"matcher": "session_start",
"hooks": [
{ "type": "command", "command": "~/.claude/scripts/notify-claude-md-load.sh" }
]
}
],
"ConfigChange": [
{
"matcher": "project_settings",
"hooks": [
{ "type": "command", "command": "~/.claude/scripts/block-project-settings.sh" }
]
}
]
}
}
出力例
InstructionsLoaded のスクリプトに渡る JSON:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../transcript.jsonl",
"cwd": "~/my-project",
"hook_event_name": "InstructionsLoaded",
"file_path": "~/my-project/CLAUDE.md",
"memory_type": "Project",
"load_reason": "session_start"
}
ConfigChange のスクリプトに渡る JSON:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../transcript.jsonl",
"cwd": "~/my-project",
"hook_event_name": "ConfigChange",
"config_source": "project_settings",
"config_path": "~/my-project/.claude/settings.json",
"change_type": "modified"
}
block を返したいときの標準出力:
{ "decision": "block", "reason": "project settings の変更は PR レビュー必須です" }
このページに出てきた言葉
- InstructionsLoaded
- CLAUDE.md / <code>.claude/rules/*.md</code> が読み込まれた瞬間に発火するフック。観測専用で、読み込みを止めることはできない
- ConfigChange
- <code>settings.json</code> 系の設定変更時に発火するフック。<code>policy_settings</code> 以外は変更そのものを block できる
- load_reason
- CLAUDE.md がなぜ読まれたかを示す値。<code>session_start</code> <code>nested_traversal</code> <code>path_glob_match</code> <code>include</code> <code>compact</code> の5種
- memory_type
- CLAUDE.md のスコープ。<code>User</code> <code>Project</code> <code>Local</code> <code>Managed</code> の4種
- config_source
- 設定変更の出元。<code>user_settings</code> <code>project_settings</code> <code>local_settings</code> <code>policy_settings</code> <code>skills</code> の5種。matcher で絞る対象
- config_path
- 書き換わった設定ファイルの絶対 path。どのファイルが対象かをスクリプト側で特定するのに使う
- change_type
- <code>"modified"</code> / <code>"created"</code> / <code>"deleted"</code> の3値。操作種別を区別する
- matcher
- フックを「どの条件のときだけ走らせるか」を絞る指定。パイプ <code>|</code> で複数値を OR 連結できる
- decision: block
- フックが返す指示。設定変更や処理を止める
- policy_settings
- 組織管理者が配布する設定。セキュリティ設計上、フックで block 不可
- lazy load
- セッション開始時ではなく、必要になった時点で後追いで読み込まれる挙動