Matchers and Pattern Matching
Complete guide to matching tools with hook matchers.
Overview
Matchers and Pattern Matching
Complete guide to matching tools with hook matchers.
What are matchers?
Matchers are regex patterns that filter which tools trigger a hook. They allow you to:
- Target specific tools (e.g., only
Bash) - Match multiple tools (e.g.,
Write|Edit) - Match tool categories (e.g., all MCP tools)
- Match everything (omit matcher)
Syntax
Matchers use JavaScript regex syntax:
{
"matcher": "pattern"
}
The pattern is tested against the tool name using new RegExp(pattern).test(toolName).
Common Patterns
Exact match
{
"matcher": "Bash"
}
Matches: Bash
Doesn't match: bash, BashOutput
Multiple tools (OR)
{
"matcher": "Write|Edit"
}
Matches: Write, Edit
Doesn't match: Read, Bash
Starts with
{
"matcher": "^Bash"
}
Matches: Bash, BashOutput
Doesn't match: Read
Ends with
{
"matcher": "Output$"
}
Matches: BashOutput
Doesn't match: Bash, Read
Contains
{
"matcher": ".*write.*"
}
Matches: Write, NotebookWrite, TodoWrite
Doesn't match: Read, Edit
Case-sensitive! write won't match Write.
Any tool (no matcher)
{
"hooks": {
"PreToolUse": [
{
"hooks": [...] // No matcher = matches all tools
}
]
}
}
Tool Categories
All file operations
{
"matcher": "Read|Write|Edit|Glob|Grep"
}
All bash tools
{
"matcher": "Bash.*"
}
Matches: Bash, BashOutput, BashKill
All MCP tools
{
"matcher": "mcp__.*"
}
Matches: mcp__memory__store, mcp__filesystem__read, etc.
Specific MCP server
{
"matcher": "mcp__memory__.*"
}
Matches: mcp__memory__store, mcp__memory__retrieve
Doesn't match: mcp__filesystem__read
Specific MCP tool
{
"matcher": "mcp__.*__write.*"
}
Matches: mcp__filesystem__write, mcp__memory__write
Doesn't match: mcp__filesystem__read
MCP Tool Naming
MCP tools follow the pattern: mcp__{server}__{tool}
Examples:
mcp__memory__storemcp__filesystem__readmcp__github__create_issue
Match all tools from a server:
{
"matcher": "mcp__github__.*"
}
Match specific tool across all servers:
{
"matcher": "mcp__.*__read.*"
}
Real-World Examples
Log all bash commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/bash-log.txt"
}
]
}
]
}
}
Format code after any file write
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "prettier --write $CLAUDE_PROJECT_DIR"
}
]
}
]
}
}
Validate all MCP memory writes
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__memory__.*",
"hooks": [
{
"type": "prompt",
"prompt": "Validate this memory operation: $ARGUMENTS\
\
Check if data is appropriate to store.\
\
Return: {\\"decision\\": \\"approve\\" or \\"block\\", \\"reason\\": \\"why\\"}"
}
]
}
]
}
}
Block destructive git commands
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/check-git-safety.sh"
}
]
}
]
}
}
check-git-safety.sh:
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command')
if [[ "$command" == *"git push --force"* ]] || \\
[[ "$command" == *"rm -rf /"* ]] || \\
[[ "$command" == *"git reset --hard"* ]]; then
echo '{"decision": "block", "reason": "Destructive command detected"}'
else
echo '{"decision": "approve", "reason": "Safe"}'
fi
Multiple Matchers
You can have multiple matcher blocks for the same event:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/bash-validator.sh"
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "/path/to/file-validator.sh"
}
]
},
{
"matcher": "mcp__.*",
"hooks": [
{
"type": "command",
"command": "/path/to/mcp-logger.sh"
}
]
}
]
}
}
Each matcher is evaluated independently. A tool can match multiple matchers.
Debugging Matchers
Enable debug mode
claude --debug
Debug output shows:
[DEBUG] Getting matching hook commands for PreToolUse with query: Bash
[DEBUG] Found 3 hook matchers in settings
[DEBUG] Matched 1 hooks for query "Bash"
Test your matcher
Use JavaScript regex to test patterns:
const toolName = "mcp__memory__store";
const pattern = "mcp__memory__.*";
const regex = new RegExp(pattern);
console.log(regex.test(toolName)); // true
Or in Node.js:
node -e "console.log(/mcp__memory__.*/.test('mcp__memory__store'))"
Common mistakes
❌ Case sensitivity
{
"matcher": "bash" // Won't match "Bash"
}
✅ Correct
{
"matcher": "Bash"
}
❌ Missing escape
{
"matcher": "mcp__memory__*" // * is literal, not wildcard
}
✅ Correct
{
"matcher": "mcp__memory__.*" // .* is regex for "any characters"
}
❌ Unintended partial match
{
"matcher": "Write" // Matches "Write", "TodoWrite", "NotebookWrite"
}
✅ Exact match only
{
"matcher": "^Write$"
}
Advanced Patterns
Negative lookahead (exclude tools)
{
"matcher": "^(?!Read).*"
}
Matches: Everything except Read
Match any file operation except Grep
{
"matcher": "^(Read|Write|Edit|Glob)$"
}
Case-insensitive match
{
"matcher": "(?i)bash"
}
Matches: Bash, bash, BASH
(Note: Claude Code tools are PascalCase by convention, so this is rarely needed)
Performance Considerations
Broad matchers (e.g., .*) run on every tool use:
- Simple command hooks: negligible impact
- Prompt hooks: can slow down significantly
Recommendation: Be as specific as possible with matchers to minimize unnecessary hook executions.
Example: Instead of matching all tools and checking inside the hook:
{
"matcher": ".*", // Runs on EVERY tool
"hooks": [
{
"type": "command",
"command": "if [[ $(jq -r '.tool_name') == 'Bash' ]]; then ...; fi"
}
]
}
Do this:
{
"matcher": "Bash", // Only runs on Bash
"hooks": [
{
"type": "command",
"command": "..."
}
]
}
Tool Name Reference
Common Claude Code tool names:
BashBashOutputKillShellReadWriteEditGlobGrepTodoWriteNotebookEditWebFetchWebSearchTaskSkillSlashCommandAskUserQuestionExitPlanMode
MCP tools: mcp__{server}__{tool} (varies by installed servers)
Run claude --debug and watch tool calls to discover available tool names.