[BUG] Hookify plugin: Multiple foot guns causing rules to fail silently or trigger unexpectedly
[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 Bash→bash and Edit/Write/MultiEdit→file. 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?
- Rules should only load for their declared event type
- File event rules should match both Edit and Write operations
- Shipped examples should work as documented
event: allshould work with simple patterns- Case sensitivity should be consistent across operators
- 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 usesnew_string) - Operator semantics (literal substring vs regex)
- The
not_containsfootgun 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:
- Default to empty list when
event=None, or map all tools - Check both
new_stringandcontentfor file events - Fix or remove the broken example
- Document
event: allrequires explicit conditions - Make case sensitivity consistent or document the difference
- Use proper parsing for inline conditions
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗