Correction, 2026-10-03: An earlier version of this post said matcher must be a valid regex and that an empty string does not work, and its fixed example put a matcher on a Stop hook. The current Claude Code hooks reference (checked 2026-10-03) says `"", "" or an omitted matcher all match every occurrence, and that Stop` does not support matchers: a matcher there is silently ignored. The matcher rule and the example below have been corrected.*
Claude Code recently changed their hooks format, and if you haven't updated your project settings, you'll see this cryptic error:
hooks: Expected array, but received undefined
Files with errors are skipped entirely, not just the invalid settings.
The Problem
The old hook format put command at the top level:
{
"hooks": {
"Stop": [
{
"matcher": "",
"command": "bash .claude/hooks/my-script.sh"
}
]
}
}
This silently breaks - your entire settings file gets skipped.
The Fix
The new format requires a nested hooks array with explicit type:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/my-script.sh"
}
]
}
]
}
}
Key changes:
-
matcher- Filters when the hook fires."*",""or leaving it out matches everything. A plain tool name such asBash, or a list such asEdit|Write, matches exactly. Anything with other characters is treated as a regex.Stopand several other events ignore the matcher, so the example above leaves it out; it matters on tool events such asPreToolUseandPostToolUse. -
hooks- Now an array inside each matcher group (the object that can hold amatcher) -
type- Required field, set to"command"for shell commands
Quick Migration
Find all your .claude/settings.json files and update them:
# Find all Claude settings files
find ~/projects -name "settings.json" -path "*/.claude/*" 2>/dev/null
Then update each one to the new format.
Top comments (0)