Every value you can put in a Claude Code hook matcher, by event
What a hook matcher compares against depends on the event. The exact-match, regex and wildcard rules, the events that silently ignore a matcher, and how to target MCP tools.
Contents
When you write hooks in settings.json, it is not obvious what belongs in matcher. You can tell that a tool name like Bash works, but whether regex is allowed, and what SessionStart compares against, is something you normally discover by trial and error.
The short version: what a matcher compares against depends on the event, and the way you write the value silently decides whether it is treated as an exact match or a regex. This article lays out both rules.
KEY POINT
What you will learn
- How Claude Code decides between exact match, regex and wildcard
- What the matcher is compared against for each event
- How to target MCP tools, and which events ignore a matcher entirely
How the value decides the matching mode
A matcher is evaluated in one of three modes, and the characters you type decide which one.
| Value | Evaluated as | Example |
|---|---|---|
"*", "", or omitted | Match all | Fires on every occurrence |
Only letters, digits, _, -, spaces, ,, | | Exact string, or a list of exact strings | Bash, Edit|Write, Edit, Write |
| Contains any other character | JavaScript regex, unanchored | ^Notebook, mcp__memory__.* |
This is the easiest part to get wrong. Bash is an exact match, but ^Bash$ contains anchors, so it is treated as a regex. Both happen to give the same result here, but if a name you write happens to contain . or *, it silently becomes a pattern.
Regexes are tested with RegExp.prototype.test(), which means matching anywhere in the value. A regex Edit also matches NotebookEdit. Write ^Edit$ when you want the whole string.
Matchers are case-sensitive
bash does not match the Bash tool. Use the exact spelling from the tools reference.
Two behaviors depend on your version:
- Exact matching for names containing hyphens requires Claude Code v2.1.195 or later. On earlier versions a name like
code-revieweris evaluated as an unanchored regex, so write^code-reviewer$ - Comma-separated alternatives (
Edit, Write) require v2.1.191 or later
What each event matches against
The matcher is not a tool-name field. Each event compares it against something different.
| Event | Matcher filters | Example values |
|---|---|---|
PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDenied | Tool name | Bash, Edit|Write, mcp__.* |
SessionStart | Session start reason | startup, resume, clear, compact, fork |
Setup | CLI flag that triggered setup | init, maintenance |
SessionEnd | Session end reason | clear, resume, logout, prompt_input_exit, other |
Notification | Notification type | permission_prompt, idle_prompt, auth_success |
SubagentStart / SubagentStop | Agent type | general-purpose, Explore, Plan, custom names |
PreCompact / PostCompact | What triggered compaction | manual, auto |
PreModelSwitch / PostModelSwitch | Canonical model name | claude-opus-5 |
ConfigChange | Configuration source | user_settings, project_settings, local_settings, policy_settings, skills |
DirectoryAdded | How the directory was added | slash_command, register_repo_root |
FileChanged | Literal filenames to watch | .envrc|.env |
StopFailure | Error type | rate_limit, overloaded, authentication_failed, server_error |
InstructionsLoaded | Load reason | session_start, nested_traversal, path_glob_match, include, compact |
UserPromptExpansion | Command name | Your skill or command names |
Elicitation / ElicitationResult | MCP server name | Your configured MCP server names |
For a worked example on the last-but-two row, see Log which CLAUDE.md files Claude Code loads.
Events that ignore the matcher
These events do not support a matcher. Adding one is not an error — it is silently ignored.
UserPromptSubmitPostToolBatchStopCwdChangedTeammateIdleTaskCreatedTaskCompletedWorktreeCreateWorktreeRemoveMessageDisplay
If a hook seems to fire every time no matter what you narrow it to, check this list first. Because nothing errors, this misconfiguration is easy to miss.
A working example
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-bash.sh"
}
]
},
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "/path/to/lint-check.sh"
}
]
}
],
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "echo started >> ~/session.log"
}
]
}
]
}
}
Check tool-name spellings in the official tools reference. The ones you will reach for most are Bash, Read, Edit, Write, Glob, Grep, WebFetch, WebSearch, NotebookEdit, TodoWrite, Agent and Skill.
Targeting MCP tools
MCP server tools are named mcp__<server>__<tool>.
| Goal | Matcher |
|---|---|
| Every tool from one server | mcp__memory__.* |
| A server name containing a hyphen | mcp__brave-search__.* |
| One tool name across all servers | mcp__.*__write.* |
| A plugin-bundled server | mcp__plugin_my-plugin_db__.* |
Don't drop the trailing .*
mcp__memory on its own is an exact match and will not match mcp__memory__create_entities. Append .* when you want everything under a server.
When the matcher isn't specific enough
A matcher only narrows as far as the tool name. To filter on the tool's arguments — "only TypeScript file edits", say — use the if field on the hook itself.
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"if": "Edit(*.ts)",
"command": "./check-typescript.sh"
}
]
}
if uses the same rule syntax as permissions, covered in Claude Code permissions in settings.json. It is evaluated only on tool events and ignored everywhere else.
用語解説
Unanchored matching: a regex succeeds if it matches anywhere inside the value. Add ^ and $ when you need the whole string to match.
For writing the hook script itself, see Run lint and format automatically after every edit.
Summary
- The characters in the value decide between exact match, regex and match-all
- Any symbol turns the value into an unanchored regex, so
Editalso matchesNotebookEdit - Matchers are case-sensitive; hyphen exact-matching needs v2.1.195+, commas need v2.1.191+
- What the matcher compares against varies by event and is often not a tool name
- Ten events, including
StopandUserPromptSubmit, ignore the matcher silently - For MCP tools, append
.*as inmcp__<server>__.*
FAQ
- What happens if I omit the matcher?
- It matches everything. Omitting it, an empty string, and "*" all mean the same thing: the hook fires on every occurrence of that event.
- Can I use a regex in a matcher?
- Yes, but the value is not always treated as one. A value made only of letters, digits, underscores, hyphens, spaces, commas and pipes is an exact match; a single other character turns it into a regex.
- Are matchers case-sensitive?
- Yes. Writing bash will not match the Bash tool.
- What does the matcher compare against for non-tool events like SessionStart?
- It differs per event. SessionStart matches the start reason (startup, resume, clear and so on), while PreCompact matches what triggered compaction (manual or auto).
Primary sources
This article was drafted by AI from official documentation and reviewed by the site operator before publishing. Found a mistake? Let us know via the contact page.