Initial commit
This commit is contained in:
470
skills/create-hooks/references/matchers.md
Normal file
470
skills/create-hooks/references/matchers.md
Normal file
@@ -0,0 +1,470 @@
|
||||
# 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"matcher": "pattern"
|
||||
}
|
||||
```
|
||||
|
||||
The pattern is tested against the tool name using `new RegExp(pattern).test(toolName)`.
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Exact match
|
||||
```json
|
||||
{
|
||||
"matcher": "Bash"
|
||||
}
|
||||
```
|
||||
Matches: `Bash`
|
||||
Doesn't match: `bash`, `BashOutput`
|
||||
|
||||
### Multiple tools (OR)
|
||||
```json
|
||||
{
|
||||
"matcher": "Write|Edit"
|
||||
}
|
||||
```
|
||||
Matches: `Write`, `Edit`
|
||||
Doesn't match: `Read`, `Bash`
|
||||
|
||||
### Starts with
|
||||
```json
|
||||
{
|
||||
"matcher": "^Bash"
|
||||
}
|
||||
```
|
||||
Matches: `Bash`, `BashOutput`
|
||||
Doesn't match: `Read`
|
||||
|
||||
### Ends with
|
||||
```json
|
||||
{
|
||||
"matcher": "Output$"
|
||||
}
|
||||
```
|
||||
Matches: `BashOutput`
|
||||
Doesn't match: `Bash`, `Read`
|
||||
|
||||
### Contains
|
||||
```json
|
||||
{
|
||||
"matcher": ".*write.*"
|
||||
}
|
||||
```
|
||||
Matches: `Write`, `NotebookWrite`, `TodoWrite`
|
||||
Doesn't match: `Read`, `Edit`
|
||||
|
||||
Case-sensitive! `write` won't match `Write`.
|
||||
|
||||
### Any tool (no matcher)
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"hooks": [...] // No matcher = matches all tools
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Categories
|
||||
|
||||
### All file operations
|
||||
```json
|
||||
{
|
||||
"matcher": "Read|Write|Edit|Glob|Grep"
|
||||
}
|
||||
```
|
||||
|
||||
### All bash tools
|
||||
```json
|
||||
{
|
||||
"matcher": "Bash.*"
|
||||
}
|
||||
```
|
||||
Matches: `Bash`, `BashOutput`, `BashKill`
|
||||
|
||||
### All MCP tools
|
||||
```json
|
||||
{
|
||||
"matcher": "mcp__.*"
|
||||
}
|
||||
```
|
||||
Matches: `mcp__memory__store`, `mcp__filesystem__read`, etc.
|
||||
|
||||
### Specific MCP server
|
||||
```json
|
||||
{
|
||||
"matcher": "mcp__memory__.*"
|
||||
}
|
||||
```
|
||||
Matches: `mcp__memory__store`, `mcp__memory__retrieve`
|
||||
Doesn't match: `mcp__filesystem__read`
|
||||
|
||||
### Specific MCP tool
|
||||
```json
|
||||
{
|
||||
"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__store`
|
||||
- `mcp__filesystem__read`
|
||||
- `mcp__github__create_issue`
|
||||
|
||||
**Match all tools from a server**:
|
||||
```json
|
||||
{
|
||||
"matcher": "mcp__github__.*"
|
||||
}
|
||||
```
|
||||
|
||||
**Match specific tool across all servers**:
|
||||
```json
|
||||
{
|
||||
"matcher": "mcp__.*__read.*"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Log all bash commands
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "jq -r '.tool_input.command' >> ~/bash-log.txt"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Format code after any file write
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit|NotebookEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "prettier --write $CLAUDE_PROJECT_DIR"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Validate all MCP memory writes
|
||||
```json
|
||||
{
|
||||
"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
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "/path/to/check-git-safety.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`check-git-safety.sh`:
|
||||
```bash
|
||||
#!/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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
```bash
|
||||
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:
|
||||
|
||||
```javascript
|
||||
const toolName = "mcp__memory__store";
|
||||
const pattern = "mcp__memory__.*";
|
||||
const regex = new RegExp(pattern);
|
||||
console.log(regex.test(toolName)); // true
|
||||
```
|
||||
|
||||
Or in Node.js:
|
||||
```bash
|
||||
node -e "console.log(/mcp__memory__.*/.test('mcp__memory__store'))"
|
||||
```
|
||||
|
||||
### Common mistakes
|
||||
|
||||
❌ **Case sensitivity**
|
||||
```json
|
||||
{
|
||||
"matcher": "bash" // Won't match "Bash"
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Correct**
|
||||
```json
|
||||
{
|
||||
"matcher": "Bash"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
❌ **Missing escape**
|
||||
```json
|
||||
{
|
||||
"matcher": "mcp__memory__*" // * is literal, not wildcard
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Correct**
|
||||
```json
|
||||
{
|
||||
"matcher": "mcp__memory__.*" // .* is regex for "any characters"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
❌ **Unintended partial match**
|
||||
```json
|
||||
{
|
||||
"matcher": "Write" // Matches "Write", "TodoWrite", "NotebookWrite"
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Exact match only**
|
||||
```json
|
||||
{
|
||||
"matcher": "^Write$"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Negative lookahead (exclude tools)
|
||||
```json
|
||||
{
|
||||
"matcher": "^(?!Read).*"
|
||||
}
|
||||
```
|
||||
Matches: Everything except `Read`
|
||||
|
||||
### Match any file operation except Grep
|
||||
```json
|
||||
{
|
||||
"matcher": "^(Read|Write|Edit|Glob)$"
|
||||
}
|
||||
```
|
||||
|
||||
### Case-insensitive match
|
||||
```json
|
||||
{
|
||||
"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:
|
||||
```json
|
||||
{
|
||||
"matcher": ".*", // Runs on EVERY tool
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "if [[ $(jq -r '.tool_name') == 'Bash' ]]; then ...; fi"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Do this:
|
||||
```json
|
||||
{
|
||||
"matcher": "Bash", // Only runs on Bash
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Name Reference
|
||||
|
||||
Common Claude Code tool names:
|
||||
- `Bash`
|
||||
- `BashOutput`
|
||||
- `KillShell`
|
||||
- `Read`
|
||||
- `Write`
|
||||
- `Edit`
|
||||
- `Glob`
|
||||
- `Grep`
|
||||
- `TodoWrite`
|
||||
- `NotebookEdit`
|
||||
- `WebFetch`
|
||||
- `WebSearch`
|
||||
- `Task`
|
||||
- `Skill`
|
||||
- `SlashCommand`
|
||||
- `AskUserQuestion`
|
||||
- `ExitPlanMode`
|
||||
|
||||
MCP tools: `mcp__{server}__{tool}` (varies by installed servers)
|
||||
|
||||
Run `claude --debug` and watch tool calls to discover available tool names.
|
||||
Reference in New Issue
Block a user