FileChanged
Runs when a watched file changes on disk. Claude Code detects changes with a filesystem watcher, not by inspecting tool calls, so it runs the hook no matter what changed the file: an Edit or Write tool call, a script Claude runs with Bash, or a process outside Claude Code entirely. A common use is reloading environment variables when project configuration files change.
The matcher for this event serves two roles:
- Build the watch list: the value is split on
|and each segment is registered as a literal filename in the working directory, so".envrc|.env"watches exactly those two files. Regex patterns are not useful here: a value like^\.envwould watch a file literally named^\.env. - Filter which hooks run: when a watched file changes, the same value filters which hook groups run using the standard matcher rules against the changed file's basename.
This example normalizes line endings in data.csv after any change, including a Bash command or an external script rewriting the file:
{
"hooks": {
"FileChanged": [
{
"matcher": "data.csv",
"hooks": [
{
"type": "command",
"command": "/path/to/normalize-line-endings.sh"
}
]
}
]
}
}The hook reads the changed file's absolute path from the file_path field of the JSON input on stdin. Its grep guard tests for the same thing perl removes, a CR at the end of a line, so the run after a normalization exits without touching the file. A looser guard loops forever, because perl -i rewrites the file even when it substitutes nothing and Claude Code runs the hook again after every rewrite. Save this script at /path/to/normalize-line-endings.sh and make it executable:
#!/bin/bash
FILE=$(jq -r .file_path)
if grep -q $'\r$' "$FILE"; then
perl -pi -e 's/\r$//' "$FILE"
fiTo confirm the hook works, ask Claude to append a CRLF line to data.csv with a Bash command. Claude Code runs the hook and the file ends up with LF endings.
To watch files you can't name up front, return watchPaths from a hook to update the watch list dynamically. Claude Code starts the watcher only when something names a file to watch, so seed the list with a FileChanged group whose matcher names at least one file, or with a SessionStart or CwdChanged hook that returns watchPaths. The matcher still filters which hook groups run when a watched file changes, so give the group that handles dynamic paths an omitted matcher, which matches every watched file and adds nothing to the watch list. A "*" matcher also matches every file, but Claude Code registers it in the watch list like any other value, as a literal file named *.
FileChanged hooks have access to CLAUDE_ENV_FILE. Variables written to that file persist into subsequent Bash commands until the next CwdChanged event, when Claude Code clears them.
FileChanged input
In addition to the common input fields, FileChanged hooks receive file_path and event.
| Field | Description |
|---|---|
file_path | Absolute path to the file that changed |
event | What happened: "change" for a modified file, "add" for a created file, or "unlink" for a deleted file |
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../transcript.jsonl",
"cwd": "/Users/my-project",
"hook_event_name": "FileChanged",
"file_path": "/Users/my-project/.envrc",
"event": "change"
}FileChanged output
In addition to the JSON output fields available to all hooks, FileChanged hooks can return watchPaths to dynamically update which file paths are watched:
| Field | Description |
|---|---|
watchPaths | Array of absolute paths. Replaces the current dynamic watch list. Paths from your matcher configuration are always watched. Use this when your hook script discovers additional files to watch based on the changed file |
FileChanged hooks have no decision control. They can't block the file change from occurring.
Claude Code reads watchPaths and systemMessage from their JSON output and discards continue. In interactive sessions, it shows the systemMessage as a brief terminal notification. The message doesn't reach the SDK message stream.