A hook without a matcher fires for everything. That is rarely what you want. The matcher is how you scope a tool-based hook to just the calls you care about.
Matcher patterns
For PreToolUse and PostToolUse, the matcher names the tool that triggers the hook. Three forms cover most needs:
"Bash"— a single tool by name."Edit|Write"— several tools, separated by a pipe."*"— every tool.
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "./.claude/on-edit.sh" }] }
]
}
}
That fires the script after any Edit or Write, but not after a Bash call.
Conditions inside the command
The matcher only filters by tool. For anything finer, put the condition inside your command. Since the full event arrives as JSON on stdin, your script can inspect it and decide whether to act:
#!/usr/bin/env bash
path=$(jq -r '.tool_input.file_path')
if [[ "$path" == *.py ]]; then
black "$path"
fi
Here the matcher catches all edits, but the if narrows it to Python files. Everything else passes through untouched.
Two layers of filtering
Think of it as a funnel. The matcher is the coarse filter, cheap and declarative, choosing which tool wakes the hook. The if inside your command is the fine filter, choosing whether this particular call deserves action.
Splitting the work this way keeps your settings.json readable and pushes the messy logic into a script where you can test it. Reach for a broad matcher plus a script condition whenever "which tool" is not specific enough on its own.
Comments
Be the first to comment.