Skip to content

fix(hookify): pretooluse.py leaks stop-event rules into PreToolUse evaluation#1355

Closed
adgdei wants to merge 1 commit intoanthropics:mainfrom
adgdei:fix/hookify-pretooluse-event-filter
Closed

fix(hookify): pretooluse.py leaks stop-event rules into PreToolUse evaluation#1355
adgdei wants to merge 1 commit intoanthropics:mainfrom
adgdei:fix/hookify-pretooluse-event-filter

Conversation

@adgdei
Copy link
Copy Markdown

@adgdei adgdei commented Apr 11, 2026

PR title

fix(hookify): pretooluse.py leaks stop-event rules into PreToolUse evaluation

Branch suggestion

fix/hookify-pretooluse-event-filter

PR body

Bug

plugins/hookify/hooks/pretooluse.py defaults event = None for any tool other than Bash, Edit, Write, or MultiEdit. It then calls load_rules(event=None).

In plugins/hookify/core/config_loader.py, the filter logic is:

if event:
    if rule.event != 'all' and rule.event != event:
        continue

When event is None, the if event: guard short-circuits — no filtering happens. Every rule from every ~/.claude/hookify.*.local.md file is loaded into the PreToolUse evaluation, including rules declared with event: stop, event: bash, or event: file.

Impact

A stop-event rule whose conditions can match the PreToolUse input (e.g. a transcript-scanning regex condition) will fire on every PreToolUse call for any tool other than Bash/Edit/Write/MultiEdit. If the rule's action is block, this denies tools like ToolSearch, WebFetch, Glob, Grep, Read, etc. — which can create unrecoverable deadlocks if the user's only path to satisfying the stop rule requires one of those tools (e.g. a stop rule that requires a specific WebFetch to occur).

Fix

Default event to a sentinel string 'pretooluse'. The filter in load_rules then runs as intended:

  • rule.event == 'bash' → skipped
  • rule.event == 'file' → skipped
  • rule.event == 'stop'skipped (the bug fix)
  • rule.event == 'all' → kept (preserved by the existing rule.event != 'all' short-circuit)

Existing event: all catch-all behavior is preserved. Bash/Edit/Write/MultiEdit filtering is unchanged.

Reproduction

  1. Create ~/.claude/hookify.test.local.md with event: stop, action: block, and any condition that matches an arbitrary PreToolUse input (e.g. a transcript regex condition that already matches).
  2. Trigger any tool that isn't Bash/Edit/Write/MultiEdit (e.g. ToolSearch, WebFetch, Read).
  3. Observe: blocked, with the stop-rule message.
  4. Apply patch.
  5. Observe: tool runs normally; the stop rule still fires correctly on actual Stop events.

Verification

echo '{"tool_name":"ToolSearch","tool_input":{},"transcript_path":"/tmp/fake.jsonl","cwd":"/tmp","session_id":"test"}' \
  | CLAUDE_PLUGIN_ROOT=/path/to/hookify python3 plugins/hookify/hooks/pretooluse.py
# Before patch (with a matching event:stop rule present): blocked
# After patch: {} exit 0

…aluation

pretooluse.py defaults event=None for any tool other than Bash, Edit,
Write, or MultiEdit, then calls load_rules(event=None). The filter in
core/config_loader.py is:

    if event:
        if rule.event != 'all' and rule.event != event:
            continue

When event is None, the 'if event:' guard short-circuits and no
filtering happens. Every rule from every ~/.claude/hookify.*.local.md
file is loaded into the PreToolUse evaluation, including rules declared
with event: stop, event: bash, or event: file.

A stop-event rule whose conditions can match the PreToolUse input
(e.g. a transcript-scanning regex condition) will fire on every
PreToolUse call for tools other than Bash/Edit/Write/MultiEdit. If
the rule's action is block, this denies tools like ToolSearch,
WebFetch, Glob, Grep, Read, etc. — which can create unrecoverable
deadlocks if the user's only path to satisfying the stop rule
requires one of those tools.

Fix: default event to a sentinel string 'pretooluse' so the filter
in load_rules runs as intended:

  - rule.event == 'bash'  -> skipped
  - rule.event == 'file'  -> skipped
  - rule.event == 'stop'  -> skipped (the bug fix)
  - rule.event == 'all'   -> kept (preserved by 'rule.event != all')

Existing event: all catch-all behavior is preserved. Bash/Edit/Write/
MultiEdit filtering is unchanged.
@github-actions
Copy link
Copy Markdown

Thanks for your interest! This repo only accepts contributions from Anthropic team members. If you'd like to submit a plugin to the marketplace, please submit your plugin here.

@github-actions github-actions bot closed this Apr 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant