DEV Community

aicoding-guide
aicoding-guide

Posted on Originally published at aicoding-guide.com

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

Originally published at https://aicoding-guide.com.

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

Event Matcher filters Example values
PreToolUse / PostToolUse / PostToolUseFailure / PermissionRequest / PermissionDenied Tool name Bash, `Edit\
{% raw %}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\
{% raw %}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.

  • 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"
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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.

Glossary
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>__.*

Top comments (0)