CLAUDE LABJP
SCIENCE — Claude Science launches in beta, a workbench that unifies research tools and produces auditable artifactsMODEL — Fast mode for Claude Opus 4.7 retires on July 24; migrate to Opus 4.8 fast modeCODE — Claude Code v2.1.195 adds a toggle to disable mouse clicks in fullscreen modeCODE — Hyphenated hook matchers now match exactly instead of substring-matchingAGENT — Claude Science pairs a coordinating agent with specialists and a reviewer that checks citations and mathCLOUD — Claude is generally available in Microsoft Foundry on Azure with Azure-native accessSCIENCE — Claude Science launches in beta, a workbench that unifies research tools and produces auditable artifactsMODEL — Fast mode for Claude Opus 4.7 retires on July 24; migrate to Opus 4.8 fast modeCODE — Claude Code v2.1.195 adds a toggle to disable mouse clicks in fullscreen modeCODE — Hyphenated hook matchers now match exactly instead of substring-matchingAGENT — Claude Science pairs a coordinating agent with specialists and a reviewer that checks citations and mathCLOUD — Claude is generally available in Microsoft Foundry on Azure with Azure-native access
Articles/Claude Code
Claude Code/2026-07-01Intermediate

My Claude Code Hooks Stopped Firing After an Update — the Hyphenated Matcher Exact-Match Change in v2.1.195

In Claude Code v2.1.195, hook matchers containing a hyphen switched from partial match to exact match, silently disabling an existing PreToolUse hook. Here is how I isolated the cause and how to write matchers that won't break.

Claude Code174hooks13troubleshooting87matcherautomation80

One morning I was reviewing a diff, and my hand froze.

The SwiftLint check that always runs before a commit hadn't run. It hadn't failed with an error — the hook had simply passed through, quietly, with no sign it had ever fired.

I had upgraded Claude Code to the latest version the night before. Nothing had broken loudly. Instead, "nothing happened." This kind of silent failure is the sort that quietly eats your afternoon.

The cause turned out to be a behavior change in the hook matcher introduced in v2.1.195. If you, like me, were writing matchers that contain a hyphen, you may have fallen into the same trap. Let me leave the whole isolation process here as a record.

What was actually happening

In my setup, I inject hooks only for specific tool calls. The relevant part of settings.json looks roughly like this.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/pre-edit-check.sh" }
        ]
      }
    ]
  }
}

This part worked fine. The problem was a different matcher, in another project, that targeted a custom tool name.

Tools exposed through MCP get names like mcp__myserver__run-lint, which contain hyphens. I had targeted one of them like this.

{ "matcher": "run-lint" }

Up through v2.1.194, this run-lint fired via a partial match against mcp__myserver__run-lint. But in v2.1.195, matchers containing a hyphen were changed to be treated as exact matches.

run-lint does not exactly equal mcp__myserver__run-lint. So, without emitting any error, it simply stopped matching.

What I suspected first

I suspected the wrong things, in order. For what it's worth, here is the path I took.

First I suspected the shell script's permissions. I checked chmod +x and ran it directly — the script itself worked correctly. That ruled out a fault in the hook body.

Next I suspected broken JSON. I validated settings.json, and the syntax was clean.

Third, I launched a session with claude --debug and followed the hook evaluation log. This gave me the decisive clue. PreToolUse evaluation was running, but the number of hooks that matched my matcher was logged as "0."

The script was fine, the JSON was fine, evaluation was running. All that remained was the matcher's matching logic. Only then did my mind turn back to the previous night's update.

Confirming the cause

Checking the changelog, v2.1.195 included a fix stating that hook matchers containing a hyphen would be treated as exact matches rather than partial matches.

In other words, matchers that had implicitly been picked up by partial matching stop matching the moment they cross a hyphen boundary. My run-lint was a textbook case.

The tricky part is that this is a "bug fix." The old partial matching had a side effect of catching unintended tools too, so tightening it is the correct direction. But for existing configurations it is a breaking behavior change, and because it emits no error, it takes a while to notice.

The conclusion was simple: my configuration had been relying on the old, implicit behavior.

The fix

The remedy is straightforward. Rewrite the matcher so it correctly matches the actual tool name. There are three main options.

The first is to spell out the tool name exactly. This is the safest and makes the intent clearest.

{ "matcher": "mcp__myserver__run-lint" }

The second is to target it with a regular expression. This suits cases where you want to catch MCP tools per server.

{ "matcher": "mcp__myserver__.*" }

The third is to use a wildcard for all MCP tools. The scope is broad, so it assumes the hook itself decides whether it should really act.

{ "matcher": "mcp__.*" }

I went with the first. When you are targeting a single tool, spelling out the name is both misfire-free and easy to read. After rewriting, I ran the tool with claude --debug and confirmed the evaluation log showed the hook count as "1." Only after seeing that could I relax.

For reference, here is the before-and-after behavior.

MatcherUp to v2.1.194v2.1.195 and later
run-lintPartial match on mcp__myserver__run-lint (fires)No match (does not fire)
mcp__myserver__run-lintMatch (fires)Match (fires)
mcp__myserver__.*Match (fires)Match (fires)

Standard tool names without a hyphen, such as Edit or Write, are unaffected by this change. The only matchers that break are the ones that relied on partial matching across a hyphen.

Preventing a recurrence

To avoid stepping on the same silent failure twice, I put a few operational safeguards in place.

First, I made it a rule that matchers "spell out the tool name exactly, rather than relying on partial matching." Abbreviated forms look convenient, but the more you lean on implicit matching semantics, the more an update can pull the rug out from under you.

Second, I made hooks log whether they actually fired. Even a single line written to standard error at the top of the script lets you tell afterward whether it passed through or ran.

#!/usr/bin/env bash
echo "[pre-edit-check] fired at $(date -u +%FT%TZ)" >&2
# the real work

The greatest enemy of a silent failure is that the failure leaves no record. Keeping the mere fact that a hook fired gives you a starting point for isolation the next time an update changes behavior.

Finally, I added a small ritual: after updating Claude Code, run the key hook-related operations by hand once to confirm them. It isn't flashy, but the more unattended a pipeline is, the more this check pays off.

As an indie developer juggling several projects, I have entrusted a fair amount of work to automation hooks myself. That is exactly why this incident stayed with me as a lesson: lean on implicit behavior for the sake of convenience, and it will break quietly.

If you've ever struggled with reading hook evaluation logs in the first place, Claude Code hooks production debugging methodology may help as well. If you want to get matcher syntax right from the ground up, see the complete guide to Claude Code hooks, and for a different update-triggered problem, see handling Claude Code auto-update failures.

What to check next: open your own settings.json and review, just once, whether any matcher uses a hyphenated string as a partial match. If it does, replace it with an exact tool name or a regular expression to be safe.

If even one person is spared from losing time to this silent failure, I'll be glad. Thank you for reading.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-06-18
Moving Cleanup and Logging into a SessionEnd Hook
How to use Claude Code's new post-session hook to automate temp-file cleanup and log writing after a session ends, with real examples from a pipeline that processes several repositories in sequence.
Claude Code2026-04-25
When Claude Code Hooks Loop Forever — Stopping Self-Triggering PostToolUse Hooks
You wired up Claude Code hooks and now your terminal is a waterfall of tool calls that won't stop. Here's why PostToolUse hooks loop on themselves, and the patterns I use in real projects to make sure they never do again.
Claude Code2026-06-24
I Watched an Agent Try to Fix a File It Had Already Fixed — Stale Shallow Clones and Refreshing Before You Decide
An unattended agent tried to re-fix a file it had already fixed. The cause was a days-old shallow clone it kept reading. Here is how to detect that staleness numerically and re-clone only before decisions.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →