From ddd802f13a4e05e7518d26625805e5df78c7549a Mon Sep 17 00:00:00 2001 From: Andrii Drozdenko Date: Thu, 15 Jan 2026 22:06:43 +0200 Subject: [PATCH] feat(hookify): add negation operator and value key support --- plugins/hookify/README.md | 16 +++++++++---- plugins/hookify/core/config_loader.py | 10 ++++++-- plugins/hookify/core/rule_engine.py | 2 ++ .../examples/exclude-test-files.local.md | 24 +++++++++++++++++++ plugins/hookify/skills/writing-rules/SKILL.md | 7 ++++-- 5 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 plugins/hookify/examples/exclude-test-files.local.md diff --git a/plugins/hookify/README.md b/plugins/hookify/README.md index 1aca6cdfb7..993924a4d8 100644 --- a/plugins/hookify/README.md +++ b/plugins/hookify/README.md @@ -235,11 +235,19 @@ Use environment variables instead of hardcoded values. ### Operators Reference - `regex_match`: Pattern must match (most common) -- `contains`: String must contain pattern +- `not_regex_match`: Pattern must NOT match (for exclusion patterns) +- `contains`: String must contain value - `equals`: Exact string match -- `not_contains`: String must NOT contain pattern -- `starts_with`: String starts with pattern -- `ends_with`: String ends with pattern +- `not_contains`: String must NOT contain value +- `starts_with`: String starts with value +- `ends_with`: String ends with value + +**Note:** For non-regex operators, you can use `value` instead of `pattern` for clarity: +```yaml +- field: command + operator: contains + value: --force # More intuitive than "pattern" for literal strings +``` ### Field Reference diff --git a/plugins/hookify/core/config_loader.py b/plugins/hookify/core/config_loader.py index fa2fc3e36f..592d53c055 100644 --- a/plugins/hookify/core/config_loader.py +++ b/plugins/hookify/core/config_loader.py @@ -21,11 +21,17 @@ class Condition: @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Condition': - """Create Condition from dict.""" + """Create Condition from dict. + + Supports both 'pattern' and 'value' keys for flexibility. + The 'value' key is more intuitive for non-regex operators. + """ + # Support both 'pattern' and 'value' keys + pattern = data.get('pattern') or data.get('value', '') return cls( field=data.get('field', ''), operator=data.get('operator', 'regex_match'), - pattern=data.get('pattern', '') + pattern=pattern ) diff --git a/plugins/hookify/core/rule_engine.py b/plugins/hookify/core/rule_engine.py index 8244c00591..2ee792441f 100644 --- a/plugins/hookify/core/rule_engine.py +++ b/plugins/hookify/core/rule_engine.py @@ -165,6 +165,8 @@ def _check_condition(self, condition: Condition, tool_name: str, if operator == 'regex_match': return self._regex_match(pattern, field_value) + elif operator == 'not_regex_match': + return not self._regex_match(pattern, field_value) elif operator == 'contains': return pattern in field_value elif operator == 'equals': diff --git a/plugins/hookify/examples/exclude-test-files.local.md b/plugins/hookify/examples/exclude-test-files.local.md new file mode 100644 index 0000000000..33cd2f5c4c --- /dev/null +++ b/plugins/hookify/examples/exclude-test-files.local.md @@ -0,0 +1,24 @@ +--- +# Example: Warn on console.log, but exclude test/story files +name: console-log-prod-only +enabled: true +event: file +action: warn +conditions: + - field: file_path + operator: regex_match + pattern: \.(tsx?|jsx?)$ + - field: file_path + operator: not_regex_match + pattern: (\.test\.|\.spec\.|\.stories\.|__mocks__|__tests__) + - field: new_text + operator: regex_match + pattern: console\.(log|debug|info)\( +--- + +**Console statement in production code!** + +Remove `console.log/debug/info` before committing. + +These are allowed in test and story files, which is why this rule uses +`not_regex_match` to exclude them. diff --git a/plugins/hookify/skills/writing-rules/SKILL.md b/plugins/hookify/skills/writing-rules/SKILL.md index 008168a4c9..9ef93e31b8 100644 --- a/plugins/hookify/skills/writing-rules/SKILL.md +++ b/plugins/hookify/skills/writing-rules/SKILL.md @@ -88,12 +88,13 @@ You're adding an API key to a .env file. Ensure this file is in .gitignore! - For file: `file_path`, `new_text`, `old_text`, `content` - `operator`: How to match - `regex_match`: Regex pattern matching + - `not_regex_match`: Regex must NOT match (for exclusion patterns) - `contains`: Substring check - `equals`: Exact match - `not_contains`: Substring must NOT be present - `starts_with`: Prefix check - `ends_with`: Suffix check -- `pattern`: Pattern or string to match +- `pattern` or `value`: Pattern or string to match (use `value` for non-regex operators for clarity) **All conditions must match for rule to trigger.** @@ -371,4 +372,6 @@ Warning message - Prompt: `user_prompt` **Operators:** -- `regex_match`, `contains`, `equals`, `not_contains`, `starts_with`, `ends_with` +- `regex_match`, `not_regex_match`, `contains`, `equals`, `not_contains`, `starts_with`, `ends_with` + +**Tip:** Use `value` instead of `pattern` for non-regex operators for better readability.