ConfigChange hookで設定変更を検知してブロックする
ConfigChangeはsettings.jsonなど5種類の設定ソースの変更を検知するフックです。監査ログに残すだけでなく、policy_settings以外は適用そのものをブロックできます。
ConfigChangeは、セッション中に設定ファイルが変わった瞬間に発火するフックです。~/.claude/settings.jsonのようなユーザー設定から.claude/skills/のスキルファイルまで、5種類のソースをmatcherで個別に拾えます。監査ログに残すだけでなく、policy_settings以外なら変更の適用そのものを止められます。
ConfigChange hookとは何か — 発火するタイミングと対象外の変更
ConfigChangeは、設定ファイル・管理ポリシーファイル・スキルファイルのいずれかが変わったときに発火します。新しい設定が実行中のセッションへ反映される前に割り込めて、反映そのものを止められる点が特徴です。
発火の引き金は、Claude Codeが設定ファイルを監視して自動で再読み込みする仕組みです。permissionsやhooksの編集は再起動なしで実行中のセッションに効きます。Claude Codeは、検知した設定ファイルの変更ごとにConfigChangeを実行します。
ただし全ての設定変更を拾うわけではありません。管理ポリシーについてはmanaged-settings.json本体かmanaged-settings.d/配下のファイルが変わったときだけ発火します。サーバー管理設定(server-managed settings)の到着・更新、macOSの管理者設定、Windowsのレジストリポリシーの変更は、フックを実行せずに直接適用されます。WSL環境でwslInheritsWindowsSettingsを使っている場合も、ポリシーのポーリングで拾ったWindows側の管理設定はフックを経由しません。
matcherは、この5つの設定ソースを個別に狙い撃ちするためのフィルターです。
| matcher | 発火するタイミング |
|---|---|
user_settings | 発火するタイミング~/.claude/settings.jsonの変更 |
project_settings | 発火するタイミング.claude/settings.jsonの変更 |
local_settings | 発火するタイミング.claude/settings.local.jsonの変更 |
policy_settings | 発火するタイミングmanaged-settings.jsonまたはmanaged-settings.d/内のファイルの変更 |
skills | 発火するタイミング.claude/skills/内のスキルファイルの変更 |
このどれにも当てはまらない変更(サーバー管理設定の反映など)はイベント自体が発火しないため、matcherをどう書いても捕まりません。
こんな場面で使う — 監査と承認制の2つの用途
ConfigChangeの2つの使い方
監査ログに残す
誰かが.claude/settings.jsonに許可設定を書き足しても、通常はコミットログを見返すまで気づけません。変更のたびに記録しておけば、設定ドリフトの原因を後から追えます。skillsのmatcherなら、共有スキルの改変だけを拾えます。
承認制で止める
project_settingsやlocal_settingsをブロック対象にし、承認済みの変更だけをセッションへ反映させます。権限周りの設定を誰でも即座に反映できる状態を避けたい、セキュリティ要件の厳しいプロジェクト向けです。
どちらも、複数人が同じリポジトリの.claude/配下を触るチーム開発で効きます。個人利用でも、自分の~/.claude/settings.jsonが意図せず書き換わっていないかを見張る用途には使えます。
入力フィールドで変更元を特定する
ConfigChangeは、session_id・transcript_path・cwd・hook_event_nameという共通フィールドに加えて、sourceとfile_pathを受け取ります。sourceは上の表のmatcher値と同じ文字列で、file_pathは実際に変更されたファイルの絶対パスです。
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"hook_event_name": "ConfigChange",
"source": "project_settings",
"file_path": "/Users/.../my-project/.claude/settings.json"
}file_pathは付かないこともある任意のフィールドです。sourceだけで判定できる処理なら、スクリプト側でfile_pathの有無を分岐条件にする必要はありません。
skillsのように1つのmatcherが複数ファイルを束ねているケースでは、file_pathが実質的な識別子になります。.claude/skills/配下には複数のスキルファイルが並ぶため、sourceだけでは「どのスキルが変わったか」までは分かりません。監査ログにファイル名まで残したいなら、file_pathを欠かさず記録します。
ConfigChangeが受け付ける決定制御
ConfigChangeは、UserPromptSubmit・PostToolUse・Stopなどと同じトップレベルのdecisionとreasonを受け付けるイベントです。StopやSubagentStopが使えるhookSpecificOutput.additionalContextのような会話継続用のフィールドは、ConfigChangeの説明にはありません。返せるのは「許可する」か「ブロックする」かの二択で、設定の中身を書き換えたりコンテキストを足したりする出口は用意されていません。
JSONで返せる値のうち、systemMessageとcontinueは破棄されます。見られるのはdecisionの結果だけです。
設定手順 — 変更を監査ログに記録する
まず.claude/settings.jsonにConfigChangeのhookを登録します。matcherを空にすると5つのソースすべてが対象になります。
{
"hooks": {
"ConfigChange": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/audit-config-change.sh",
"args": []
}
]
}
]
}
}呼び出すスクリプトは、標準入力のJSONからsourceとfile_pathを取り出してログへ追記するだけの単純な形で十分です。
#!/bin/bash
# .claude/hooks/audit-config-change.sh
input=$(cat)
source=$(jq -r '.source' <<<"$input")
file_path=$(jq -r '.file_path // "unknown"' <<<"$input")
echo "$(date +%Y-%m-%dT%H:%M:%S%z) source=$source file=$file_path" >> ~/.claude/config-change-audit.log
exit 0保存したら実行権限を付けます。
chmod +x .claude/hooks/audit-config-change.shskillsをmatcherに指定すれば、この監査ログの対象を「誰かがスキルを追加・変更したとき」だけに絞り込めます。チームで共有しているスキルディレクトリの改変履歴を、Gitのコミット粒度より細かく追いたいときに使える構成です。
変更をブロックする実装 — 承認制にする
ConfigChangeは監査だけでなく、変更の適用そのものを止められます。ブロックするには、exit code 2で終了するか、JSONのdecisionフィールドに"block"を返します。ブロックされた場合、新しい設定は実行中のセッションへ反映されません。
| フィールド | 説明 |
|---|---|
decision | 説明"block"で変更の適用を防ぐ。省略すれば変更を許可 |
reason | 説明JSON上は受理されるが、どこにも表示されない |
{
"decision": "block",
"reason": "Configuration changes to project settings require admin approval"
}project_settingsの変更を一律ブロックし、承認済みの変更だけを個別に許可したいときは、承認済みファイルのハッシュ値をあらかじめ用意しておき、一致しない変更だけをexit code 2で止める構成が実務的です。
#!/bin/bash
# .claude/hooks/approve-project-settings.sh
input=$(cat)
source=$(jq -r '.source' <<<"$input")
file_path=$(jq -r '.file_path' <<<"$input")
if [ "$source" != "project_settings" ]; then
exit 0
fi
current_hash=$(shasum -a 256 "$file_path" | cut -d' ' -f1)
if grep -qx "$current_hash" .claude/hooks/approved-settings.sha256 2>/dev/null; then
exit 0
fi
echo "承認済みハッシュに一致しない project_settings の変更です" >&2
exit 2shasum -a 256はmacOSにも標準で入っているコマンドです。承認済みハッシュを.claude/hooks/approved-settings.sha256に追記する運用にすれば、レビュー済みの変更をマージした直後に承認リストを更新する流れをGitのワークフローにそのまま乗せられます。
デプロイ前にスクリプト単体で動作確認するなら、実際のセッションを介さずに標準入力へJSONを流し込んで実行できます。
echo '{"source":"project_settings","file_path":".claude/settings.json"}' \
| .claude/hooks/approve-project-settings.sh; echo "exit=$?"承認リストが空だと、次のようにexit=2が返ります。settings.jsonのハッシュを承認リストに書き込んで同じ入力を流すとexit=0です。sourceをuser_settingsに変えた入力もexit=0で素通りします。
承認済みハッシュに一致しない project_settings の変更です
exit=2セッションを起動しなくても、ブロック側の判定が返るかどうかはここで見分けられます。
policy_settingsとサーバー管理設定はブロックできない
policy_settingsの変更はブロック対象から除外されています。decision: "block"を返してもmanaged-settings.jsonの変更は無視され、そのまま適用されます。組織の管理ポリシーが個々のマシンのフック次第で無効化されないようにするための仕様です。
ただしpolicy_settingsのhook自体は発火します。ブロックはできなくても、誰がいつ管理ポリシーを変更したかのログは取れるということです。
ソースごとの発火とブロック
user・project・local
user_settings・project_settings・local_settingsはhookが発火し、decision: "block"かexit code 2で適用を止められます。
policy_settings
managed-settings.jsonなどの変更でhookは発火しますが、ブロックの判定は無視され、変更は常に適用されます。
管理ポリシーは監査できても止められず、サーバー配信の管理設定は監査対象にも入りません。組織全体で必ず適用したい必須ポリシーはpolicy_settingsに置き、常に反映される前提で設計します。プロジェクトごとに裁量が残る設定はproject_settingsやlocal_settingsに置き、ConfigChangeのブロックでレビューを強制します。必須ポリシーをproject_settings側に置くと、フックの判定次第で外れうるためです。
ConfigChangeと隣接フックの使い分け
ConfigChangeが扱うのは設定ファイルの変更で、CLAUDE.mdの読み込みを扱うInstructionsLoadedとは別物です。両者は「ファイルを監視する」という点で似ていますが、対象も決定制御の有無も違います。InstructionsLoadedの詳しい挙動はClaude Code Setup/InstructionsLoadedフックの実務設定にまとまっています。
ツール呼び出しをブロックするPreToolUseとも役割が異なります。PreToolUseは「これから実行されるコマンド」を止めるフックで、ConfigChangeは「これから反映される設定」を止めるフックです。判定の返し方は異なります。ConfigChangeはトップレベルのdecision、PreToolUseはhookSpecificOutput.permissionDecisionです。exit code 2でブロックできる点は共通なので、承認フローの考え方は応用できます。実装例はPreToolUse hookでツール実行前に許可・拒否・改変するを参照してください。
skillsのmatcherは、hooks・skills・agentsをプラグイン構成へ移すような作業と相性が良い機能です。移行作業中に.claude/skills/を触るたびにログが残るため、誰がどのファイルをいつ動かしたかを後から追えます。移行手順そのものはClaude Codeプラグイン化の移行手順で扱っています。
止めたいのに止まらない — exit code 1とJSONの崩れ
ブロックに使えるのは、exit code 2か、標準出力に返した正しいJSONのdecisionです。exit code 1は慣用的にはUnixの失敗コードですが、有効なJSONが標準出力に無ければ非ブロッキングのエラーとして扱われ、処理がそのまま進みます。承認制のスクリプトでexit 1と書くと、見かけだけ「拒否」で実際は設定が通ります。
標準出力をJSONとして解釈できなかった場合も、exit code 2以外では非ブロッキングのエラーです。transcriptにhook errorの通知が出て、処理は進みます。jqの出力に余計な行が混ざると、decisionを返したつもりが無視される形です。
承認制スクリプトを入れてから確かめる順番
- 1
スクリプト単体で終了コードを見る
上のJSON流し込みで、
exit=2とexit=0の両方が出ることを確認します。 - 2
パスと実行権限を確かめる
settings.jsonのcommandに書いたパスが実在し、chmod +x済みかを見ます。パスが違うとシェルがexit code 127のような値で終了し、フックは非ブロッキングのエラーに落ちます。 - 3
実セッションの初回実行でデバッグログを見る
ブロックされたかどうかはデバッグログで確かめます。
claude --debug-file <path>で保存先を決めて起動するか、claude --debugで起動して~/.claude/debug/<セッションID>.txtを読みます。
まとめ
承認制を組むなら、終了コードは2、通知は別経路、検証は標準入力へのJSON流し込みで済ませる、の3点が要になります。管理ポリシーの強制はpolicy_settings側に任せ、ConfigChangeはプロジェクト裁量の設定とスキルの監視に使う、という分担が合う構成です。