Hook event 30 of 33

FileChanged

Runs when Claude Code's filesystem watcher detects a change to a watched file, regardless of which tool or external process caused it.

Exit code veto
Exit code cannot block
Matchers
filenames split on the pipe character, built from and matched against a changed file's basename
if filter
Does not apply
Docs checked
2026-08-29

Disk, not tool calls

PreToolUse and PostToolUse see a tool call. They tell you what Claude tried to do. FileChanged sees a change on disk, whoever made it. An edit by hand in another window, a script writing to the file, and Claude’s own Write tool all reach this event the same way.

One matcher does two jobs

Split on |, the matcher text becomes a set of literal filenames in the working directory, and Claude Code adds each one to the watch list. The same text then filters which handler groups run, by checking a changed file’s basename against that set. Text that looks like a regular expression is registered as a literal filename. A matcher of "*" watches a file named *. On a group that handles paths supplied at runtime, omit the matcher. Module 09 covers the narrower character set this event’s matcher accepts.

Three event values, and what the reference leaves out

The payload carries an absolute file_path and event: change for a modification, add for a new file, unlink for a deletion. The hooks reference does not document watcher debounce, event coalescing, delivery order across near-simultaneous changes, or how a platform rename maps onto those three values.

Growing the watch list from the hook

watchPaths in the JSON output, an array of absolute paths, replaces the dynamic watch list while the matcher-configured paths stay watched. The reference does not say what happens when two hooks return different arrays for the same change. The hook can also write variables to the file named by CLAUDE_ENV_FILE for later Bash commands. systemMessage appears as a brief notification in the terminal and does not enter the message stream an SDK program reads. continue is discarded.

The change stands

Exit 0 cannot undo a change already on disk, and the JSON applies. Exit 1 leaves the change too, and adds a hook error to the transcript. Exit 2 cannot block, and its stderr is shown to you only.

The mistake most people make first

You watch package.json and run a formatter on it from the hook. The formatter writes the file. The watcher sees a change, from whichever process, and runs the hook again.

The sign is the hook firing more than once for one edit you made. Nothing on this event tells a hook that its own write is different from any other, so the loop is yours to break.

Skip the write when the formatted output equals what is already on disk, or write the result to a different file.