K1.5.2 Task 1.5

HookMatcher: Targeted Matchers Save 40% Hook Processing Time

HookMatcher connects hook functions to tool calls via regex patterns. Getting matchers right means hooks fire only for the tools that need them — precise targeting, no wasted processing, no unexpected matches.

The registration pattern

HookMatcher(
    matcher='process_refund',      # regex matching tool name
    hooks=[enforce_refund_limit]    # hook function(s) to run
)

This hook fires only for process_refund. Other tools don’t trigger it.

Regex means substring matching

matcher='update' matches ANY tool containing “update”: update_account, get_status_update, update_settings. This catches unintended tools.

Fix: anchor the pattern. matcher='^update_account$' matches ONLY update_account — start (^) and end ($) anchors ensure exact matching.

For enforcement hooks (blocking, denying), use anchored patterns. A refund limit hook accidentally firing on check_refund_status is a misfire.

No matcher = fires on every tool call

Omitting the matcher field means the hook runs for ALL tool calls. This is correct for universal concerns (audit logging), but wasteful for targeted enforcement.

One system had a single no-matcher hook running security checks + change logging on all 8 tools. Profiling showed 40% of hook processing time wasted on Read, Grep, Glob, Task, and WebSearch — none of which needed enforcement.

Fix: split into targeted matchers. SDK-level filtering is more efficient than early-return logic inside the hook function.

Layered enforcement with separate HookMatchers

Three enforcement layers, three separate registrations:

# Layer 1: audit logging for all tools
HookMatcher(hooks=[audit_log])  # no matcher → all tools

# Layer 2: path validation for file modification tools
HookMatcher(matcher='Edit|Write', hooks=[validate_path])

# Layer 3: command blocklist for shell execution
HookMatcher(matcher='Bash', hooks=[block_dangerous])

Each HookMatcher targets exactly the tools that need its enforcement. Read/Grep/Glob get only audit logging. Edit/Write get audit + path validation. Bash gets audit + security. Clean separation of concerns.

Anti-pattern: one HookMatcher with all three checks in a single function using if-else logic. This mixes concerns, increases maintenance complexity, and doesn’t benefit from SDK-level matcher filtering.

The audit gap diagnostic

Requirements: “log all tool calls, validate Write paths, block Bash entirely.”

Registered:

  • HookMatcher(matcher='Write', hooks=[validate_path])
  • HookMatcher(matcher='Bash', hooks=[block_bash])

Missing: a matcher-less HookMatcher for universal audit logging. Read, Grep, and Glob calls are not logged. Fix: add HookMatcher(hooks=[audit_log]).

Consolidation anti-pattern

A team proposes reducing 12 HookMatchers to 2 (one PreToolUse, one PostToolUse, both without matchers, all routing logic inside). “Fewer registrations = simpler configuration.”

Reject. Consolidation removes matcher-based filtering:

  • Both hooks fire on every tool call even when only specific tools need processing → increased latency
  • Unrelated enforcement concerns mixed in single functions → reduced maintainability
  • Changing one tool’s logic risks breaking another’s → fragile code

The HookMatcher pattern is designed for separation of concerns at the registration level. More registrations with targeted matchers is simpler to maintain than fewer registrations with complex conditional logic.

Comprehensive registration example

A CI/CD agent with 4 needs:

NeedHook typeMatcherWhy
Audit all callsPreToolUse(none)Universal logging
Security enforcementPreToolUse'Bash|Write'Only dangerous tools
Output normalizationPostToolUse'Read|Grep'Only tools with raw output
Deployment gatePreToolUse'deploy'Only deploy tool

Four targeted entries, each firing only where needed. Clean, efficient, maintainable.

Multiple matchers for the same hook type

Registering multiple HookMatcher entries under the same hook type (e.g., three PreToolUse entries) is the intended pattern. They operate independently — each fires for its own matched tools and runs its own hook functions.


One-liner: Use targeted matchers with regex anchoring ('^tool_name$') for enforcement, matcher-less entries for universal logging, and separate HookMatcher entries per concern — consolidation into fewer catch-all hooks wastes 40% processing and mixes unrelated concerns.