7.3 KiB
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\n\nCheck if data is appropriate to store.\n\nReturn: {\"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.