Every value you can put in a Claude Code hook matcher, by event

Claude Code Published:

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.

Verified on Sep 15, 2026 These tools change quickly. Please also check the latest official documentation.
Contents
  1. How the value decides the matching mode
  2. What each event matches against
  3. Events that ignore the matcher
  4. A working example
  5. Targeting MCP tools
  6. When the matcher isn't specific enough
  7. Summary

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.

ValueEvaluated asExample
"*", "", or omittedMatch allFires on every occurrence
Only letters, digits, _, -, spaces, ,, |Exact string, or a list of exact stringsBash, Edit|Write, Edit, Write
Contains any other characterJavaScript 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-reviewer is 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.

EventMatcher filtersExample values
PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDeniedTool nameBash, Edit|Write, mcp__.*
SessionStartSession start reasonstartup, resume, clear, compact, fork
SetupCLI flag that triggered setupinit, maintenance
SessionEndSession end reasonclear, resume, logout, prompt_input_exit, other
NotificationNotification typepermission_prompt, idle_prompt, auth_success
SubagentStart / SubagentStopAgent typegeneral-purpose, Explore, Plan, custom names
PreCompact / PostCompactWhat triggered compactionmanual, auto
PreModelSwitch / PostModelSwitchCanonical model nameclaude-opus-5
ConfigChangeConfiguration sourceuser_settings, project_settings, local_settings, policy_settings, skills
DirectoryAddedHow the directory was addedslash_command, register_repo_root
FileChangedLiteral filenames to watch.envrc|.env
StopFailureError typerate_limit, overloaded, authentication_failed, server_error
InstructionsLoadedLoad reasonsession_start, nested_traversal, path_glob_match, include, compact
UserPromptExpansionCommand nameYour skill or command names
Elicitation / ElicitationResultMCP server nameYour 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.

  • UserPromptSubmit
  • PostToolBatch
  • Stop
  • CwdChanged
  • TeammateIdle
  • TaskCreated
  • TaskCompleted
  • WorktreeCreate
  • WorktreeRemove
  • MessageDisplay

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>.

GoalMatcher
Every tool from one servermcp__memory__.*
A server name containing a hyphenmcp__brave-search__.*
One tool name across all serversmcp__.*__write.*
A plugin-bundled servermcp__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 Edit also matches NotebookEdit
  • 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 Stop and UserPromptSubmit, ignore the matcher silently
  • For MCP tools, append .* as in mcp__<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.