Claude Media
FileChanged hookでディスク上のファイル変更を検知する

FileChanged hookでディスク上のファイル変更を検知する

FileChanged hookはBashコマンドや外部プロセスによる書き換えにも反応し、ファイルシステムの変更をディスク上で直接検知します。設定方法とwatchPathsによる動的な監視対象の追加、よくあるつまずきを解説します。

Claude CodeのHooksには、ツール呼び出しをきっかけに動くもの以外に、ディスク上の変化そのものをきっかけに動く種類があります。FileChanged hookはその代表で、監視対象に指定したファイルが変わった瞬間に発火します。ClaudeのEditツールで書き換えたのか、Bashで実行したスクリプトが書き換えたのか、Claude Codeの外で動く別プロセスが書き換えたのかを問いません。ファイルシステムの監視機能で変化そのものを見ているため、書き換えの手段を選ばずに拾えるのが最大の特徴です。

FileChanged hookで何ができるか

FileChanged hookは、監視リストに登録したファイルがディスク上で変わった瞬間に発火します。ツール呼び出しを検査するのではなく、ファイルシステムウォッチャーで変化を検知する仕組みです。だから、EditやWriteツールの呼び出しだけでなく、Claudeが実行したBashスクリプトや、Claude Codeとは無関係な外部プロセスによる書き換えでも同じように反応します。

典型的な使い方は、プロジェクトの設定ファイルが変わったときに環境変数を再読み込みする処理です。.env.envrcのようなファイルを監視しておき、変化を検知したらその内容を読み直してセッションに反映する、という運用がそのままあてはまります。

FileChangedは他の多くのイベントと違い、決定制御(decision control)を持ちません。ツール呼び出し前に発火するPreToolUseのように許可・拒否を返す仕組みがなく、変化を止めることはできません。役割はあくまで「変化への反応」であって「変化のゲート」ではない、と割り切って設計されています。

導入前に確認する3つの前提

設定を書く前に、他のフックとは異なる制約を押さえておきます。

1. matcherは正規表現ではなく、リテラルなファイル名のリストとして扱われる。 matcherの値は|で分割され、各セグメントがそのまま監視対象のファイル名として登録されます。^\.envのような正規表現っぽい値を書いても、それは「^\.envという名前のファイル」として文字どおり監視されるだけです。他のイベントで使える正規表現の感覚を持ち込むと、意図した監視が始まりません。

2. マッチ判定の文字種がさらに狭い。FileChangedとStopFailureの2つだけは、英数字・アンダースコア・|だけを使った完全一致の対象になります。ハイフンやスペース、カンマを含めると正規表現パスに落ち、区切りとして機能するのは|だけになります。他の多くのイベントで使えるカンマ区切りやハイフンを含む名前は、この2つのイベントでは扱いが変わります。

3. ウォッチャーは「誰かが監視ファイルを名指ししたとき」だけ起動する。 matcherに何も書かない、あるいは監視対象を1つも指定していない状態では、Claude Codeはファイルシステムの監視自体を開始しません。少なくとも1つのファイル名を持つFileChangedグループを設定するか、SessionStartかCwdChangedのフックからwatchPathsを返す必要があります。

設定の書き方 — matcherで2つの役割を兼ねる

FileChangedのmatcherは、他のイベントには無い二重の役割を持ちます。1つは監視リストを組み立てること、もう1つは発火時にどのフックグループを実行するかを絞り込むことです。

次の設定は、data.csvが変わるたびに改行コードを正規化するフックです。

{
  "hooks": {
    "FileChanged": [
      {
        "matcher": "data.csv",
        "hooks": [
          {
            "type": "command",
            "command": "/path/to/normalize-line-endings.sh"
          }
        ]
      }
    ]
  }
}

matcherの値は|で分割され、各セグメントが作業ディレクトリ内の監視対象として登録されます。同じ値は、変化が起きたときにどのフックグループを動かすかのフィルターとしても再利用されます。matcherを省略したグループは、監視リストには何も追加しませんが、他の設定で監視対象になっているすべてのファイルの変化に反応します。動的に追加された監視対象をまとめて処理したいグループでは、この省略形が使えます。

入力と出力のフィールド

FileChanged hookは、共通の入力フィールドに加えてfile_patheventを受け取ります。

フィールド内容
file_path内容変化したファイルの絶対パス
event内容change(変更)、add(作成)、unlink(削除)のいずれか
{
  "session_id": "abc123",
  "cwd": "/Users/my-project",
  "hook_event_name": "FileChanged",
  "file_path": "/Users/my-project/.envrc",
  "event": "change"
}

出力側では、watchPathsを返すことで監視リストを動的に更新できます。決定制御は無いため、continueフィールドを返しても無視されます。systemMessageはインタラクティブなセッションでは短い通知として画面に表示されますが、SDKのメッセージストリームには届きません。終了コード2でエラーを返しても、変化そのものを止めることはできず、標準エラー出力はユーザーにだけ表示されます。

もう1つ重要なのがCLAUDE_ENV_FILEです。FileChanged hookは、SessionStart・Setup・CwdChangedと並んでこの環境変数へのアクセスを持ちます。ここにexport文を書き込むと、以降のBashコマンドにその変数が引き継がれます。ただし永続的ではなく、次のCwdChangedイベント(作業ディレクトリの変更)が起きた時点でClaude Codeがクリアします。設定ファイルの変更を検知して環境変数を書き換える運用では、この失効タイミングを前提に設計する必要があります。

動的にwatchPathsを追加して監視対象を広げる

起動時にすべての監視対象を列挙できない場合は、watchPathsを使って後から追加します。返せるのはFileChangedだけでなく、SessionStartやCwdChangedのフックからも同じフィールドを返せます。

起動の順序には注意が必要です。ウォッチャー自体は「誰かがファイルを名指ししたとき」にしか立ち上がらないため、最低でも1つの静的なファイル名を持つFileChangedグループを用意するか、SessionStartかCwdChangedのフックがwatchPathsを返す形で監視リストの種をまいておきます。

この仕組みは、direnvのようにディレクトリごとに環境を切り替えるツールと相性がよく、CwdChangedとペアで使う設計が公式ドキュメントでも紹介されています。ディレクトリを移動するたびにCwdChangedが発火し、そのディレクトリ固有の設定ファイルをwatchPathsで監視対象に加える、という流れです。

PreToolUse・PostToolUseとの使い分け

同じ「ファイルの変更」を扱うフックでも、発火のタイミングと検知できる範囲がまったく違います。

イベント発火タイミング検知できる変更ブロック可否
PreToolUse hook発火タイミングツール実行の直前検知できる変更Claude自身のツール呼び出しのみブロック可否可(許可・拒否・改変)
PostToolUse hook発火タイミングツール実行の直後検知できる変更Claude自身のツール呼び出しのみブロック可否不可
FileChanged発火タイミングディスク上の変化を検知した時点検知できる変更ツール呼び出し・Bashスクリプト・外部プロセスすべてブロック可否不可

ここで見落としやすいのが、PostToolUseの守備範囲です。BashコマンドやClaude Code外のプロセスが同じファイルを書き換えても、Edit|WriteにマッチさせたPostToolUseフックは発火しません。あくまでツール呼び出しそのものを見ているためです。書き換えた手段を問わずに反応させたいなら、PostToolUseの拡張ではなくFileChangedを選ぶ場面になります。

逆に、書き込みそのものを止めたい、あるいは内容を書き換えてから実行させたいなら、FileChangedでは実現できません。変化はすでに完了した後に通知されるだけなので、そこは決定制御を持つPreToolUse hookの役割です。

シェルループで複数ファイルを一括変更する運用のように、Claude Codeの外で動くスクリプトが対象ファイルを直接書き換えるケースでも、FileChangedなら等しく検知できます。ツール名のマッチングに依存しない分、監視の抜け漏れを心配しなくて済みます。Editツールがタブインデントのファイルで書き換えに失敗を繰り返す場合も、自分のエディタで直接編集しFileChangedに検知させる運用が使えます。詳しくはClaude Codeでタブ編集できないエラーの原因と対処法で扱っています。

よくあるつまずき

正規表現を書いても効かない。 matcher^\.envのような値を書くと、他のイベントの感覚では「.envで始まるファイル」を意図しますが、FileChangedでは文字どおり^\.envという名前のファイルとして登録されるだけです。ワイルドカードや前方一致が必要に見える場面でも、実際に指定できるのはリテラルなファイル名の並びに限られます。

ガードのないスクリプトが無限ループする。フックがファイルを書き換えると、その書き換え自体がまた新しい変化として検知され、同じフックが再び走ります。改行コードの正規化スクリプトを例にすると、変換対象の有無を確認せずに毎回書き込むガードは無限ループの原因になります。

#!/bin/bash
FILE=$(jq -r .file_path)
if grep -q $'\r$' "$FILE"; then
  perl -pi -e 's/\r$//' "$FILE"
fi

grepで対象があるかを確認してからperl -piで書き換えるこの形なら、正規化が終わった後の実行は何も変更せずに終わります。動作確認は、対象ファイルに実際に変更を加えて発火するかを見るのが確実です。

printf "a,b,c\r\n" >> data.csv

"*"を指定しても「すべて監視」にはならない。 matcher"*"を書くと、発火フィルタとしてはすべてのファイルにマッチしますが、監視リストへの登録としては*という名前の1ファイルがそのまま追加されるだけです。「ワイルドカードで全ファイルを監視対象にする」という発想は通用しません。

ウォッチャーがそもそも起動していない。 matcherを省略したFileChangedグループだけを設定し、監視対象を1つも静的に指定していない場合、Claude Codeはファイルシステムの監視自体を始めません。動的なwatchPathsだけに頼る設計では、種となる1件をどこかで必ず名指ししておきます。

環境変数がいつの間にか消える。 CLAUDE_ENV_FILEに書き込んだ変数は、次のCwdChangedイベントでクリアされます。ディレクトリを移動する運用を挟むと、FileChangedで設定したはずの変数が失われていた、という状況が起こり得ます。

まとめ

FileChanged hookは、Edit・Writeツールの呼び出しだけでなく、Bashスクリプトや外部プロセスによる書き換えまで含めて、ディスク上の変化を等しく拾える点に価値があります。設定ファイルの変更に応じて環境変数を再読み込みする運用や、Claude Codeの外で動く別プロセスがファイルを更新する構成では、PostToolUseの守備範囲では拾いきれない変化をFileChangedが補います。

一方で、決定制御を持たないという制約は変わりません。変化を止めたい、内容を検査してから書き込ませたい場合は、引き続きPreToolUse hookの役割です。ファイル変更を検知してから元に戻したいという用途であれば、Agent SDK File Checkpointingのようにセッション内の変更を巻き戻す仕組みの方が目的に合います。監視対象の広さと、決定制御の有無。この2つの軸でどのフックを選ぶかを決めると、設定で迷う場面は減ります。

この記事を共有:XはてブLinkedIn