Blog
Mein Claude-Code-Hook feuerte auf jedes Tool. Der Fehler steckte im Matcher.
Ein Matcher in Claude-Code-Hooks ist ein Regex, kein exakter String. Genau das hatte ich übersehen. Die häufigsten Matcher-Fallen, warum mcp__memory nichts matcht, und wann du if statt matcher brauchst.
24. Juli 2026
Mein PostToolUse-Hook sollte nach jedem Datei-Edit einen Linter starten. Stattdessen feuerte er nach ziemlich allem. Nach jedem Read, nach jedem Bash, nach dem Suchen. Aufgefallen ist es mir erst, weil die statusMessage bei jedem einzelnen Schritt im Terminal aufblitzte, "Linter läuft", obwohl ich seit zehn Minuten nur gelesen und gegrept hatte. Ich hatte als Matcher `Edit.*` eingetragen, in der Annahme, das sei so ein Glob wie in der Shell. Ist es nicht. Ein Matcher in Claude-Code-Hooks ist ein Regex, und das ändert alles.
Hier sind die vier Fallen, in die ich getappt bin, alle aus der offiziellen Hook-Referenz.
## Falle 1: der Matcher ist ein Regex, kein String
Der Matcher wird mit JavaScripts `RegExp.prototype.test` geprüft, und das trifft, sobald das Muster irgendwo im Wert vorkommt. `Edit.*` matcht deshalb nicht nur `Edit`, sondern auch `NotebookEdit`. Und weil `test` unverankert ist, matcht ein zu lockeres Muster auch da, wo du es nie wolltest.
Die Lösung ist ankern. Wenn du wirklich nur das eine Tool willst, schreib `^Edit$`.
```json
"PostToolUse": [
{
"matcher": "^Edit$",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/lint.sh",
"timeout": 10
}
]
}
]
```
Wenn du Edit und Write zusammen willst, nimm `Edit|Write`. Der senkrechte Strich trennt Alternativen, das ist der saubere Weg für "eins von diesen".
## Falle 2: der umgekehrte Fall, mcp__memory matcht gar nichts
Das war die verwirrendste. Ich wollte alle Tools des Memory-Servers matchen und schrieb `mcp__memory`. Der Hook feuerte nie. Kein Fehler, kein Log, einfach Stille. Eine halbe Stunde habe ich am Skript gesucht, dabei lag es am Matcher.
Der Grund: MCP-Tools heißen `mcp__<server>__<tool>`, zum Beispiel `mcp__memory__create_entities`. Ein Matcher wie `mcp__memory` enthält nur exakt-Match-Zeichen und wird deshalb als exakter String verglichen, nicht als Regex. Und kein Tool heißt wortwörtlich `mcp__memory`. Du musst `.*` anhängen, damit es zum Regex wird:
```json
"matcher": "mcp__memory__.*"
```
Das matcht dann jedes Tool vom Memory-Server. Gleiches Spiel für `mcp__github__.*` oder `mcp__.*__write.*`, wenn du jedes write-Tool von jedem Server treffen willst.
## Falle 3: Bindestriche und Versionsnummern
Server mit Bindestrich im Namen sind ein eigenes Thema. `mcp__brave-search__.*` funktioniert auf jeder Version. Ein blankes `mcp__brave-search` ohne `.*` verhält sich je nach Version anders: Bindestriche im exakt-Match-Set brauchen Claude Code 2.1.195 oder neuer. Auf älteren Versionen wird der Bindestrich als unverankerter Regex behandelt und matcht plötzlich zu viel.
Ähnlich beim Komma. Willst du `Edit, Write` mit Komma statt senkrechtem Strich trennen, brauchst du 2.1.191 oder neuer. Wenn du nicht sicher bist auf welcher Version dein Team fährt, bleib beim `|` und beim expliziten `.*`. Beides geht überall und erspart dir die Versions-Raterei.
## Falle 4: manche Events haben gar keinen Matcher
Ich hatte einen Matcher auf UserPromptSubmit gesetzt und mich gewundert, warum er nichts filtert. Er filtert nichts, weil UserPromptSubmit gar keine Matcher unterstützt. Genauso Stop und ein paar andere. Ein Matcher-Feld an diesen Events wird stillschweigend ignoriert, der Hook feuert immer.
Wenn du an so einem Event filtern willst, musst du das im Skript machen. Der Hook liest den Input von stdin, prüft die Bedingung selbst und entscheidet dann, ob er überhaupt was tut. Der Matcher hilft dir da nicht.
## Feiner filtern mit if statt matcher
Der Matcher kann nur den Tool-Namen. Sobald du auf die Argumente schauen willst, gibt es das `if`-Feld auf dem einzelnen Handler. Es nutzt Permission-Rule-Syntax und matcht Tool-Name und Argumente zusammen. So fällt mein Linter-Beispiel endlich sauber:
```json
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/lint.sh",
"if": "Edit(*.ts)",
"timeout": 10
}
]
}
]
```
`Edit(*.ts)` feuert nur bei TypeScript-Dateien. `Bash(git *)` nur bei git-Subkommandos. Der Matcher grenzt grob auf das Tool ein, `if` macht die feine Arbeit auf den Argumenten.
## Was ich mir gemerkt habe
Drei Sätze reichen eigentlich. Der Matcher ist ein Regex, also anker mit `^` und `$` wenn du genau ein Tool willst. MCP-Server brauchen `mcp__server__.*`, das blanke Präfix matcht nichts. Und wo es keinen Matcher gibt oder wo du auf Argumente schauen musst, nimm `if` oder filter im Skript. Diese drei Regeln hätten mir eine gute Stunde Sucherei erspart.
## Weiterlesen
Wenn ein Hook trotzdem partout nicht feuert, geh das Playbook [Hooks debuggen wenn nichts feuert](/playbooks/hooks-debuggen-wenn-nichts-feuert) durch. Die Grundlagen zu Hooks stecken in der Lesson [Hooks und Skills](/levels/4/hooks-und-skills), und für die direkten Tool-Aufrufe aus Events die Lesson zu [mcp_tool-Hooks](/levels/4/mcp-tool-hooks). Die vollständige Matcher- und Event-Referenz liegt bei Anthropic: https://docs.claude.com/en/docs/claude-code/hooks