ConfigChange hookで設定変更を検知してブロックする
ConfigChangeはsettings.jsonなど5種類の設定ソースの変更を検知するフックです。監査ログに残すだけでなく、policy_settings以外は適用そのものをブロックできます。
ConfigChangeは、セッション中に設定ファイルが変わった瞬間に発火するフックです。~/.claude/settings.jsonのようなユーザー設定から.claude/skills/のスキルファイルまで、5種類のソースをmatcherで個別に拾えます。監査ログに残すだけでなく、policy_settings以外なら変更の適用そのものを止められます。
ConfigChange hookとは何か — 発火するタイミングと対象外の変更
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が向くのは、複数人が同じリポジトリの.claude/配下を触るチーム開発です。誰かが.claude/settings.jsonにツールの許可設定を書き足しても、通常はコミットログを見返すまで気づけません。監査ログを常時記録しておけば、設定ドリフトの原因調査にかかる時間が大きく減ります。
もう1つの用途は承認制の強制です。セキュリティ要件が厳しいプロジェクトでは、権限周りの設定変更を誰でも即座に反映できてしまうこと自体がリスクになります。project_settingsやlocal_settingsをブロック対象にしておけば、承認済みの変更だけが実際にセッションへ反映される構成を組めます。
skillsのmatcherは、社内で配布しているスキルの改変を監視したいときに使えます。共有スキルリポジトリを複数チームが更新する運用では、想定外のスキルファイル変更を検知できるだけでも運用上の安心材料になります。
入力フィールドで変更元を特定する
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は「optionally」と明記されている任意フィールドです。sourceだけで判定できる処理なら、スクリプト側でfile_pathの有無を分岐条件にする必要はありません。
skillsのように1つのmatcherが複数ファイルを束ねているケースでは、file_pathが実質的な識別子になります。.claude/skills/配下には複数のスキルファイルが並ぶため、sourceだけでは「どのスキルが変わったか」までは分かりません。監査ログにファイル名まで残したいなら、file_pathを欠かさず記録します。
ConfigChangeが受け付ける決定制御
ConfigChangeは、UserPromptSubmit・PostToolUse・Stopなどと同じ「標準decisionモデル」に属するイベントです。トップレベルのdecisionフィールドとreasonフィールドを受け付ける点は共通していますが、StopやSubagentStopが使えるadditionalContextのような会話継続用のフィールドはConfigChangeには存在しません。ConfigChangeが返せるのは「許可する」か「ブロックする」かの二択で、設定の中身を書き換えたりコンテキストを追加したりする出口は用意されていません。
設定手順 — 変更を監査ログに記録する
まず.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 -Iseconds) 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=$(sha256sum "$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 2承認済みハッシュを.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が返り、フックが正しくブロック側の判定を返せているかをセッションを起動せずに確認できます。
policy_settingsとサーバー管理設定はブロックできない
policy_settingsの変更はブロック対象から除外されています。decision: "block"を返してもmanaged-settings.jsonの変更は無視され、そのまま適用されます。組織の管理ポリシーが個々のマシンのフック次第で無効化されないようにするための仕様です。
ただしpolicy_settingsのhook自体は発火します。ブロックはできなくても、誰がいつ管理ポリシーを変更したかのログは取れるということです。
| 設定ソース | hookは発火するか | ブロック可否 |
|---|---|---|
user_settings / project_settings / local_settings | hookは発火するかする | ブロック可否できる |
policy_settings(managed-settings.json等) | hookは発火するかする | ブロック可否できない(常に適用) |
| サーバー管理設定の到着・更新 | hookは発火するかしない | ブロック可否— |
| macOS管理者設定・Windowsレジストリポリシー | hookは発火するかしない | ブロック可否— |
「管理ポリシーの変更は監査できるが止められない」「サーバーから配信される管理設定はそもそも監査対象にも入らない」という2段階の違いを混同すると、セキュリティ要件のカバー範囲を実際より広く見積もってしまいます。
実務では、この2層を組み合わせて設計します。組織全体で必ず適用したい必須ポリシーはpolicy_settings(管理ポリシー)に置き、ブロックできない代わりに常に反映されることを前提にします。一方、プロジェクトごとに裁量が残る設定はproject_settingsやlocal_settingsに置き、ConfigChangeのブロック機能でレビューを強制します。両方を同じuser_settings扱いにしてしまうと、本来は強制したいはずのポリシーまで現場の裁量でブロック・迂回できる構成になりかねません。
ConfigChangeと隣接フックの使い分け
ConfigChangeが扱うのは設定ファイルの変更で、CLAUDE.mdの読み込みを扱うInstructionsLoadedとは別物です。両者は「ファイルを監視する」という点で似ていますが、対象も決定制御の有無も違います。InstructionsLoadedの詳しい挙動はClaude Code Setup/InstructionsLoadedフックの実務設定にまとまっています。
ツール呼び出しをブロックするPreToolUseとも役割が異なります。PreToolUseは「これから実行されるコマンド」を止めるフックで、ConfigChangeは「これから反映される設定」を止めるフックです。判定の書き方自体はどちらもdecisionとexit code 2という同じ枠組みなので、PreToolUseで承認フローを組んだ経験があればConfigChangeにもそのまま応用できます。実装例はPreToolUse hookでツール実行前に許可・拒否・改変するを参照してください。
skillsのmatcherは、hooks・skills・agentsをプラグイン構成へ移すような作業と相性が良い機能です。移行作業中に.claude/skills/を触るたびにログが残るため、誰がどのファイルをいつ動かしたかを後から追えます。移行手順そのものはClaude Codeプラグイン化の移行手順で扱っています。
よくあるつまずき
reasonを書けば承認却下の理由が利用者に伝わると誤解しがちです。実際はどこにも表示されず、デバッグログにも残りません。理由を伝えたいなら、ブロックとは別に通知の仕組みを用意します。
コマンドのパスをsettings.jsonに書き間違えると、フックは静かに無効化されます。存在しないパスを指定すると、シェルはexit code 127のような値で終了し、ほとんどのイベントでは処理がそのまま進行します。ブロック目的で組んだConfigChangeフックがこの状態になると、監査もブロックも一切機能しないまま設定変更が通り続けます。初回実行時にデバッグログを確認する習慣が有効です。
policy_settingsをブロックする設計は最初から成立しません。エンタープライズ管理下の設定を無効化できないための仕様であり、バグではありません。組織のポリシーそのものを制限したいなら、フックではなく管理ポリシー側の設定を見直す話になります。
サーバー管理設定やOS側の管理者設定はConfigChangeの対象外です。「全ての設定変更を監査できている」という前提で運用を組むと、この2つの経路だけ抜け落ちます。
まとめ
ConfigChangeは、user_settings・project_settings・local_settings・policy_settings・skillsという5つのソースの変更を検知するフックです。policy_settings以外はexit code 2またはdecision: "block"で適用そのものを止められますが、ブロックの理由は利用者にもClaudeにも表示されません。承認制の設定管理やスキルファイルの変更監査を組みたいチームに向いた機能で、サーバー管理設定やOS側の管理者設定は監視範囲に入らない点だけ押さえておくと、想定外の抜け漏れを防げます。