朝、いつものように差分を確認していて、手が止まりました。
コミット前に必ず走るはずの SwiftLint チェックが、通っていないのです。エラーで止まったのではありません。フックそのものが、一度も動いた形跡なく静かに素通りしていました。
前夜、Claude Code を最新へ上げたばかりでした。動かなくなったのではなく、「何も起きなくなった」。この種の無音の失敗が、いちばん時間を溶かします。
原因は、v2.1.195 で入った hook matcher の挙動変更でした。私と同じように、ハイフンを含む matcher を書いていた方は同じ落とし穴にはまっているかもしれません。切り分けの過程ごと、記録として残しておきます。
何が起きていたか
私の設定では、特定のツール実行に対してだけフックを差し込んでいました。settings.json の該当箇所は、おおよそこういう形です。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit",
"hooks": [
{ "type": "command", "command": ".claude/hooks/pre-edit-check.sh" }
]
}
]
}
}これ自体は問題なく動いていました。問題は、別のプロジェクトで使っていた「独自のツール名」を狙った matcher のほうでした。
MCP 経由のツールには mcp__myserver__run-lint のような、ハイフンを含む名前が付きます。私はそれを狙って、次のように書いていました。
{ "matcher": "run-lint" }v2.1.194 までは、この run-lint が mcp__myserver__run-lint に「部分一致」して発火していました。ところが v2.1.195 で、ハイフンを含む matcher は「厳密一致」として扱われるよう修正されました。
run-lint は mcp__myserver__run-lint と完全一致しません。だから、何のエラーも出さずに、ただ一致しなくなったのです。
最初に疑ったこと
順番に、間違った方向を疑いました。参考までに、私がたどった順路を残します。
最初はシェルスクリプトの権限を疑いました。chmod +x を確認し、直接叩いてみると、スクリプト自体は正しく動きます。ここでフック本体の不具合は消えました。
次に疑ったのは JSON の壊れです。settings.json を検証にかけても、構文は正常でした。
三番目に、claude --debug でセッションを起動し、フックの評価ログを追いました。ここで決定的な手がかりが出ます。PreToolUse の評価自体は走っているのに、私の matcher にマッチした hook が「0件」と記録されていたのです。
スクリプトは無事、JSON も無事、評価も走っている。残るのは matcher の一致判定だけ。ここでようやく、前夜のアップデートに意識が向きました。
原因の確定
変更履歴を確認すると、v2.1.195 に「ハイフンを含む hook matcher を部分一致ではなく厳密一致にする」旨の修正が入っていました。
つまり、これまで暗黙に部分一致で拾えていた matcher は、ハイフンをまたぐ瞬間に一致しなくなります。私の run-lint はその典型でした。
厄介なのは、これが「バグ修正」だという点です。以前の部分一致は、意図しないツールまで巻き込む副作用がありました。厳密化は正しい方向の変更です。ただ、既存設定にとっては挙動が変わる破壊的変更であり、しかもエラーを出さないため、気づくのが遅れます。
自分の設定が古い暗黙の挙動に依存していた、というのが結論でした。
解決策
対処は明快です。matcher を、実際のツール名に対して正しくマッチする形へ書き直します。選択肢は主に三つあります。
一つ目は、ツール名を厳密に書ききる方法です。もっとも安全で、意図も明確になります。
{ "matcher": "mcp__myserver__run-lint" }二つ目は、正規表現で狙う方法です。MCP ツールをサーバー単位でまとめて拾いたいときに向きます。
{ "matcher": "mcp__myserver__.*" }三つ目は、ワイルドカードで MCP ツール全体を対象にする方法です。範囲は広くなるので、フック側で本当に処理すべきか判定するのが前提になります。
{ "matcher": "mcp__.*" }私は一つ目を採りました。狙ったツールが一つだけなら、名前を書ききるのが誤爆もなく読みやすいためです。書き換えた後、claude --debug で該当ツールを実行し、評価ログに hook が「1件」と出ることを確認しました。ここまで見て、ようやく安心できました。
念のため、修正前後の挙動を整理します。
| matcher の記述 | v2.1.194 まで | v2.1.195 以降 |
|---|---|---|
run-lint | mcp__myserver__run-lint に部分一致(発火) | 一致せず(発火しない) |
mcp__myserver__run-lint | 一致(発火) | 一致(発火) |
mcp__myserver__.* | 一致(発火) | 一致(発火) |
ハイフンを含まない Edit や Write のような標準ツール名は、今回の変更の影響を受けません。影響が出るのは、ハイフンをまたいで部分一致に頼っていた matcher だけです。
再発を防ぐために
同じ無音の失敗を二度踏まないよう、運用側でいくつか手を打ちました。
まず、matcher は「部分一致に頼らず、ツール名を厳密に書く」ことを原則にしました。省略形は一見便利ですが、暗黙の一致仕様に寄りかかる分だけ、アップデートで足元をすくわれやすくなります。
次に、フックが「本当に発火したか」をログに残すようにしました。スクリプトの冒頭で標準エラーへ一行書き出すだけでも、素通りしたのか実行されたのかが後から判別できます。
#!/usr/bin/env bash
echo "[pre-edit-check] fired at $(date -u +%FT%TZ)" >&2
# 本来の処理無音の失敗のいちばんの敵は、失敗が記録に残らないことです。フックが動いた事実そのものを残しておくと、次にアップデートで挙動が変わっても、切り分けの起点になります。
最後に、Claude Code をアップデートしたら、フックが絡む主要な操作を一度だけ手で動かして確認する、という小さな儀式を挟むことにしました。派手ではありませんが、無人で回すパイプラインほど、この確認が効きます。
個人開発で複数のプロジェクトを回している私自身、多くの作業を自動化のフックに預けています。それだけに今回の一件は「便利さのために暗黙仕様へ寄りかかると、静かに壊れる」という教訓として、胸に残りました。
フックの評価ログの読み方そのものに手こずった経験がある方は、Claude Code フックの本番デバッグ手法も合わせて参考になるかもしれません。matcher の書き方から丁寧に押さえたい場合はClaude Code フック完全ガイドを、アップデート起因の別の不具合についてはClaude Code の自動アップデート失敗の対処をご覧ください。
次に確認すべきこと。お使いの settings.json を開き、matcher にハイフンを含む文字列を部分一致で使っていないか、一度だけ見直してみてください。該当があれば、厳密なツール名か正規表現に置き換えておくと安心です。
同じ無音の失敗で時間を溶かす方が一人でも減れば嬉しいです。お読みいただきありがとうございました。