anthropics / anthropics/claude-plugins-official
hookify: not_contains compares literally, but the shipped require-tests example uses a regex alternation — the rule always fires
- Lenguaje dominante
- Python
- Estrellas
- 36.3k
- Forks
- 4.1k
- Merge medio
- 2 d 14 h
- PR fusionados (30 d)
- 539
Descripción
`contains` / `not_contains` compare literally (`pattern in field_value`), but the shipped example rule and the README both write the pattern as a regex alternation. The alternation never occurs verbatim in a transcript, so a `not_contains` condition written that way is **always true** and the rule fires unconditionally — the exact opposite of what the example promises.
**Environment:** hookify from this marketplace (`plugins/hookify`, `plugin.json` carries no version field), engine files `core/rule_engine.py` + `core/config_loader.py`. Same code in the `cache/` copies.
### The shipped example is broken
`plugins/hookify/examples/require-tests-stop.local.md`:
```yaml
event: stop
action: block
conditions:
- field: transcript
operator: not_contains
pattern: npm test|pytest|cargo test
```
`README.md` (~line 198) documents the same shape.
`core/rule_engine.py` implements the operator as a literal substring test:
```python
elif operator == 'not_contains':
return pattern not in field_value
```
So the condition asks "does the transcript contain the literal string `npm test|pytest|cargo test`?". It never does. The rule matches even in a session where `npm test` ran and passed — and since the example uses `action: block`, enabling it as documented makes Stop unblockable except by the block cap.
### Reproduction
```bash
mkdir -p /tmp/hookify-repro/.claude && cd /tmp/hookify-repro
cp /examples/require-tests-stop.local.md .claude/hookify.require-tests.local.md
sed -i 's/enabled: false/enabled: true/; s/action: block/action: warn/' .claude/hookify.require-tests.local.md
echo '{"content":"npm test passed, 42 tests green"}' > transcript.jsonl
echo '{"hook_event_name":"Stop","transcript_path":"/tmp/hookify-repro/transcript.jsonl"}' \
| CLAUDE_PLUGIN_ROOT= python3 /hooks/stop.py
```
Actual: the rule matches and emits its message.
Expected: `{}` — `npm test` is present in the transcript.
### Why there is no clean workaround
- `regex_match` exists, but there is no negated regex operator, so "the transcript contains **none** of N commands" cannot be expressed as one condition.
- Conditions are AND-ed (`rule_engine.py`: `# All conditions must match`), so the working formulation is one `not_contains` per command:
```yaml
conditions:
- field: transcript
operator: not_contains
pattern: npm test
- field: transcript
operator: not_contains
pattern: pytest
- field: transcript
operator: not_contains
pattern: cargo test
```
Verified: with this form the rule stays silent when any of the three appears in the transcript, and still fires when none do.
That works, but it is not what the example or the README teaches, and the failure is silent — a rule that always fires looks like a rule that works, since the warning text itself is plausible.
### Suggested fixes (any one helps, first two are cheap)
1. Fix `examples/require-tests-stop.local.md` and the README snippet to use one condition per pattern.
2. Warn at load time when a `contains` / `not_contains` pattern contains an unescaped `|` — almost certainly a regex written against a literal operator.
3. Add `regex_not_match` (or an `invert: true` flag on conditions) so a multi-alternative negative can be written as one condition, and document which operators are literal and which are regex.
Related but distinct: #5255 covers the event-filter and legacy-`pattern:` bugs; this one is about operator semantics in the documented example.
Guía de contribución
No hay ninguna guía de contribución indexada para este repositorio
Línea de trabajo
Start with core/rule_engine.py to confirm that contains and not_contains use literal substring matching. Update plugins/hookify/examples/require-tests-stop.local.md and the README snippet to use one condition per command, then run the supplied reproduction to verify the rule stays silent when npm test appears and fires when none of the commands appear.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- python
- Área
- devtools, documentation
- Tipo de issue
- Error
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Estado de actividad
- Tranquilo
- Claridad
- Bien especificado
- Aptitud para principiantes
- 78/100