はじめに
この記事を読むと、以下がわかります。
- Claude Code の hooks で
matcherフィールドがどう評価されるか(exact-match と regex フォールバックの分岐条件) - v2.1.195(2026-06-26)でハイフン付き識別子の扱いが変わった具体的な内容
- 自分のリポジトリの hooks 設定が影響を受けるかどうかの点検方法
前提知識として、Claude Code の hooks(PreToolUse / PostToolUse 等のイベントで外部コマンドを発火させる仕組み)を一度でも設定したことがあると理解が早いです。
TL;DR
- Claude Code v2.1.195(2026-06-26)で、hook の
matcherフィールドの評価ルールが変わった - 変更前は
code-reviewerのようなハイフン付き識別子が 意図せず regex(部分一致)として評価 されていた - 変更後は英数字・
_・-・空白・,・|のみで構成される matcher は 完全一致(exact-match) として評価される - この変更により、
code-reviewerはsenior-code-reviewerに もうマッチしなくなる - エラーは一切出ない。hook が静かに発火しなくなるだけなので、自分で気づかないと放置しがち
公式ドキュメントで matcher の評価ルールを確認する
まず一次情報を確認します。Claude Code 公式 Changelog の該当箇所です。
Fixed hook matchers with hyphenated identifiers (e.g.
code-reviewer,mcp__brave-search) accidentally substring-matching — they now exact-match. Usemcp__brave-search__.*to match all tools from a hyphenated MCP server.
「バグ修正」という体裁ですが、動いていた設定が動かなくなるという意味では実質的に破壊的変更です。
hooks ドキュメントには、matcher の評価ルールが表で整理されています。
| matcher の値 | 評価のされ方 |
|---|---|
"*"、""、または省略 |
全イベントにマッチ |
英数字・_・-・空白・,・| のみで構成 |
完全一致(| または , 区切りで複数指定可) |
| 上記以外の文字を含む | JavaScript の正規表現として評価(アンカーなし) |
そして、ハイフンの扱いについてはこう明記されています。
Hyphens in the exact-match set require Claude Code v2.1.195 or later. On earlier versions a hyphenated name like
code-revieweris evaluated as an unanchored regular expression, so it also fires forsenior-code-reviewer; anchor it as^code-reviewer$on those versions to match only that name.
つまり:
-
v2.1.195より前:
-は「その他の文字」扱いなので、code-reviewerという matcher はアンカーなし正規表現として評価され、senior-code-reviewerにも部分一致でマッチしていた -
v2.1.195以降:
-が完全一致文字集合に追加されたため、code-reviewerは完全一致専用になり、senior-code-reviewerにはマッチしなくなる
ちなみに FileChanged と StopFailure の2イベントだけは完全一致文字集合がさらに狭く、英数字・_・| のみ(ハイフンを含まない)なので、この2イベントに限っては v2.1.195以降もハイフン付き matcher は regex フォールバックのままです。
v2.1.195 前後の挙動差を再現する
公式ドキュメントの記述をもとに、評価ロジックを Python で再現してみます。
import re
def evaluate(matcher: str, tool_name: str, hyphen_in_exact_set: bool) -> bool:
if matcher in ("*", "", None):
return True
allowed = r"a-zA-Z0-9_ ,|"
if hyphen_in_exact_set:
allowed += "-"
if re.fullmatch(f"[{allowed}]*", matcher):
candidates = [c.strip() for c in re.split(r"[|,]", matcher)]
return tool_name in candidates
return re.search(matcher, tool_name) is not None
cases = [
("mcp__kinako-mocchi", "mcp__kinako-mocchi__generate_image"),
("mcp__kinako-mocchi", "mcp__kinako-mocchi__check_status"),
("mcp__kinako-mocchi__.*", "mcp__kinako-mocchi__generate_image"),
("code-reviewer", "code-reviewer"),
("code-reviewer", "senior-code-reviewer"),
]
for matcher, tool in cases:
old = evaluate(matcher, tool, hyphen_in_exact_set=False)
new = evaluate(matcher, tool, hyphen_in_exact_set=True)
print(f"{matcher:<24} {tool:<38} v2.1.195より前={old} v2.1.195以降={new}")
実行結果:
mcp__kinako-mocchi mcp__kinako-mocchi__generate_image v2.1.195より前=True v2.1.195以降=False
mcp__kinako-mocchi mcp__kinako-mocchi__check_status v2.1.195より前=True v2.1.195以降=False
mcp__kinako-mocchi__.* mcp__kinako-mocchi__generate_image v2.1.195より前=True v2.1.195以降=True
code-reviewer code-reviewer v2.1.195より前=True v2.1.195以降=True
code-reviewer senior-code-reviewer v2.1.195より前=True v2.1.195以降=False
注目すべきは1〜2行目です。mcp__kinako-mocchi という matcher(サーバー名だけを書いて、個別ツール名まで書いていない)は、v2.1.195より前なら部分一致で mcp__kinako-mocchi__generate_image 等の実ツール名を拾えていましたが、v2.1.195以降は完全一致専用になるため 一切マッチしなくなります。3行目のように mcp__kinako-mocchi__.* と明示的にワイルドカード化していれば、両バージョンで動作は変わりません。
自分のリポジトリの設定を点検する
このリポジトリの .claude/settings.json に実際に定義されている hook matcher を確認してみます。
{
"PreToolUse": [
{ "matcher": "Bash", "hooks": [ /* ... */ ] },
{ "matcher": "mcp__github__create_pull_request|mcp__github__merge_pull_request", "hooks": [ /* ... */ ] }
],
"PostToolUseFailure": [
{ "matcher": "Bash", "hooks": [ /* ... */ ] }
]
}
Bash や mcp__github__create_pull_request|mcp__github__merge_pull_request はいずれもハイフンを含まないため、今回の変更の影響を受けません。一方で、このリポジトリが接続している MCP サーバーには kinako-mocchi というハイフン付きの名前があります(.mcp.json の mcpServers に定義)。もし今後 mcp__kinako-mocchi という matcher(個別ツール名やワイルドカードなし)を hooks に追加してしまうと、v2.1.195以降ではその hook は一切発火しなくなります。
自分のリポジトリを点検する手順はシンプルです。
-
.claude/settings.jsonのhooks配下にある全てのmatcherを列挙する - ハイフンを含む値だけを抽出する
- その値が「個別ツール名まで完全一致で書かれているか」「ワイルドカード(
.*等)で終わっているか」を確認する - どちらでもない場合(例:
mcp__kinako-mocchiのようにサーバー名だけを書いている)は、mcp__kinako-mocchi__.*のように明示的に正規表現化するか、個別ツール名を,または\|区切りで列挙する
なぜこの変更が地味に危険か
この手の変更が厄介なのは、エラーが一切出ない ことです。matcher の評価に失敗しても Claude Code は何も警告しません。単に「その hook が発火しなくなる」だけなので、CI が落ちるわけでも、ログにワーニングが出るわけでもありません。
特に影響を受けやすいのは以下のパターンです。
- ハイフン付きの MCP サーバー名を、個別ツール名やワイルドカードなしで matcher に直書きしている
- ハイフン付きのサブエージェント名(例:
code-reviewer)を matcher にしていて、実はsenior-code-reviewerのような派生名にも当ててほしかった(元々の部分一致に依存していた) - Changelog を「バグ修正」の一言で読み流し、自分の設定への影響を確認していない
まとめ
- Claude Code v2.1.195(2026-06-26)で、hook matcher の完全一致文字集合にハイフンが追加された
- ハイフン付き matcher は「部分一致(regex フォールバック)」から「完全一致」に評価方法が変わった
- 影響を受けるのは、ハイフン付きの MCP サーバー名・サブエージェント名を matcher に使っていて、個別ツール名やワイルドカードを明示していない設定
- エラーは出ないため、自分の
.claude/settings.jsonを能動的に点検する必要がある
著者視点の発見ポイント
今回、自分のリポジトリで実際に運用している MCP サーバー名(kinako-mocchi)を材料に検証したことで、「ドキュメントを読んだだけでは気づかない罠」が具体的に見えました。mcp__kinako-mocchi のようにサーバー名だけを matcher に書く設定は一見自然に見えますが、v2.1.195以降ではワイルドカードを明示しない限り機能しません。Changelog の1行だけでは「バグ修正」としか読めず、破壊的変更としての重大さが伝わりにくい典型例だと感じました。自分の hooks 設定を棚卸しする良いきっかけになったので、同様にハイフン付きのMCPサーバー名やサブエージェント名を使っている方は、この記事の点検手順を使って一度確認することをおすすめします。