[BUG] Hookify plugin: Multiple foot guns causing rules to fail silently or trigger unexpectedly

Resolved 💬 4 comments Opened Dec 9, 2025 by Moredread Closed Feb 28, 2026

[BUG] Hookify plugin: Multiple foot guns causing rules to fail silently or trigger unexpectedly

Preflight

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (consolidated report of related issues in same component)
  • [x] I am using the latest version of Claude Code

What's Wrong?

The hookify plugin has several logic issues that cause rules to behave differently than documented/expected, often failing silently or triggering on wrong events.

Critical Issues

1. Rules load for ALL tools when tool isn't mapped (pretooluse.py:46-49)

pretooluse.py only maps Bashbash and Edit/Write/MultiEditfile. For other tools (Read, Glob, TodoWrite, etc.), event=None is passed to load_rules(), which skips filtering at config_loader.py:220-221 and loads ALL rules regardless of event type.

Impact: Rules unnecessarily load and evaluate for unmapped tools. More critically, rules using not_contains or similar "negative" operators can unexpectedly match because missing fields return empty strings (see rule_engine.py:206) rather than causing the condition to fail. For example, an event: stop rule with field: reason, operator: not_contains will match on PreToolUse because reason returns '' and "pattern" not in "" is always True.

2. Simple pattern with event: file doesn't match Write tool

config_loader.py:64-65 converts simple pattern to field: new_text. In rule_engine.py:239-240, new_text maps to new_string (Edit's field), but Write uses content.

Impact: Users think they're protecting all file operations but Write operations never match.

3. Example file require-tests-stop.local.md is broken

Uses operator: not_contains with pattern: npm test|pytest|cargo test. The not_contains operator does substring matching (pattern not in field_value), not regex. It checks for the literal string "npm test|pytest|cargo test" which never appears.

Medium Issues

4. event: all with simple pattern never matches

Simple pattern defaults to field: content for non-bash/file events (config_loader.py:67), but most tools don't have a content field.

5. Case sensitivity inconsistency

regex_match uses re.IGNORECASE (rule_engine.py:24), but contains/equals/not_contains/etc are case-sensitive (rule_engine.py:168-177).

6. Commas in inline condition patterns break parsing

config_loader.py:167 splits on , before extracting pattern, so pattern: foo,bar becomes pattern: foo.

What Should Happen?

  1. Rules should only load for their declared event type
  2. File event rules should match both Edit and Write operations
  3. Shipped examples should work as documented
  4. event: all should work with simple patterns
  5. Case sensitivity should be consistent across operators
  6. Patterns containing commas should parse correctly

Steps to Reproduce

For issue #1 (most critical):

# .claude/hookify.test-stop.local.md
---
name: test-stop-rule
enabled: true
event: stop
action: block
conditions:
  - field: reason
    operator: contains
    pattern: NEVER_MATCHES
---
This blocks everything!

Then use any unmapped tool (Read, Glob, TodoWrite) - the rule loads and evaluates.

For issue #2:

# .claude/hookify.test-file.local.md
---
name: test-file
enabled: true
event: file
pattern: SECRET
---
Warning!
  • Edit adding SECRET → triggers
  • Write with SECRET → doesn't trigger

For issue #3:
Just enable the shipped examples/require-tests-stop.local.md - it never works as documented.

Is this a regression?

I don't know

Claude Code Version

2.0.56 (Claude Code)

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

Other

SKILL.md Actively Leads Users Into These Bugs

The skills/writing-rules/SKILL.md documentation doesn't just fail to warn about these issues—it actively encourages patterns that trigger them:

| Issue | What SKILL.md Says | The Trap |
|-------|-------------------|----------|
| #2 (Write vs Edit) | Line 55: "Matches against command (bash) or new_text (file)" | Users follow this guidance, use new_text for file events, and it silently fails for Write tool |
| #3 (not_contains is literal) | Lines 93-94: Lists not_contains as operator with no semantics | Users naturally assume operators work like regex_match. The shipped example require-tests-stop.local.md uses not_contains with npm test\|pytest\|cargo test—a regex OR pattern that never matches |
| #4 (event: all broken) | Line 46: "all: All events" | No warning that simple pattern defaults to field: content which most tools don't have |
| #5 (case sensitivity) | Line 89: "regex_match: Regex pattern matching" | No mention that regex is case-insensitive while contains/equals/etc are case-sensitive |
| #6 (commas in patterns) | Lines 280-281 show unquoted patterns | No warning that inline condition syntax splits on commas, so pattern: foo,bar becomes pattern: foo |

What's missing from the skill:

  • Tool→event mapping (only Bash→bash and Edit/Write/MultiEdit→file are mapped; other tools pass event=None)
  • What happens with unmapped tools (all rules load and evaluate)
  • Field availability per tool (Write uses content, Edit uses new_string)
  • Operator semantics (literal substring vs regex)
  • The not_contains footgun with empty fields returning ''

The skill documents the happy path but actively guides users toward edge cases that fail silently.

Additional Information

All issues were confirmed with test cases. The code paths are clearly identifiable in the Python source files.

This issue was prepared with Claude Code (Opus 4.5).

Suggested fixes:

  1. Default to empty list when event=None, or map all tools
  2. Check both new_string and content for file events
  3. Fix or remove the broken example
  4. Document event: all requires explicit conditions
  5. Make case sensitivity consistent or document the difference
  6. Use proper parsing for inline conditions

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗