hookify plugin: rule discovery only globs cwd-relative `./.claude/` — never finds user-scope `~/.claude/` rules

Resolved 💬 2 comments Opened May 27, 2026 by saxster Closed May 30, 2026

Plugin: plugins/hookify (v0.1.0, author: @daisy)

Related: #62679 (packaging bug — fix the packaging first; this issue surfaces only after that one is resolved or worked around locally)

Summary

hookify discovers rules by globbing ./.claude/hookify.*.local.md relative to the current working directory. It never searches user-scope ~/.claude/hookify.*.local.md. Users who put rules in user-scope (the natural location for cross-project rules — same place CLAUDE.md, settings.json, and hooks live) get zero enforcement and zero error: the hook runs, finds zero matching files, returns {}, and looks healthy.

Where the bug is

core/config_loader.py:198-211:

def load_rules(event: Optional[str] = None) -> List[Rule]:
    ...
    # Find all hookify.*.local.md files
    pattern = os.path.join('.claude', 'hookify.*.local.md')
    files = glob.glob(pattern)
    ...

glob.glob with a relative pattern is cwd-relative. No fallback to ~/.claude/. No fallback to CLAUDE_PROJECT_DIR (the project root, which can differ from cwd when running from a subdirectory).

Reproduce

# Setup: user-scope rule
cat > ~/.claude/hookify.repro-test.local.md <<'RULE'
---
name: repro-test
enabled: true
event: file
conditions:
  - field: new_text
    operator: contains
    pattern: REPRO_SENTINEL
---
**Should fire on any file Write/Edit containing REPRO_SENTINEL.**
RULE

# Run from a project that has no project-local .claude/hookify.*
cd /tmp && mkdir empty_proj && cd empty_proj
# In Claude Code: Edit any file to contain "REPRO_SENTINEL"
# Expected: rule fires, systemMessage with the rule body
# Actual:   silent {}, rule never seen

Why this is high-impact

Users naturally configure rules at user-scope — the same place every other Claude Code artifact lives:

  • ~/.claude/CLAUDE.md
  • ~/.claude/settings.json
  • ~/.claude/hooks/
  • ~/.claude/skills/
  • ~/.claude/agents/

~/.claude/hookify.*.local.md follows that pattern. The README doesn't flag that hookify is project-only. Users build out dozens of rules at user-scope, see no errors, assume the system is enforcing them — and ship with zero enforcement.

On my machine: 56 rule files in ~/.claude/, 11 marked enabled: true (including sentinel-hardcoded-secret, sentinel-sql-interpolation, tdd-enforcement, swift safety nets). None of them have ever fired in any project. Only the 5 rules I happened to put in ~/Desktop/MyCode/OffloadProject/.claude/ work — and only when running Claude Code from inside that project.

Recommended fix

Add user-scope as a fallback search location, and respect CLAUDE_PROJECT_DIR:

def load_rules(event: Optional[str] = None) -> List[Rule]:
    rules = []
    search_roots = []

    # Project-scope (existing behavior, prefer CLAUDE_PROJECT_DIR over cwd)
    project_root = os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd())
    search_roots.append(os.path.join(project_root, '.claude'))

    # User-scope fallback
    search_roots.append(os.path.expanduser('~/.claude'))

    seen = set()
    for root in search_roots:
        pattern = os.path.join(root, 'hookify.*.local.md')
        for file_path in glob.glob(pattern):
            real = os.path.realpath(file_path)
            if real in seen:
                continue
            seen.add(real)
            # ... existing per-file load logic

Precedence: project-scope wins for same-named rules (matches how settings.json/CLAUDE.md merge today). Dedupe by realpath so symlinks don't double-fire.

Suggested doc update

Until/unless this is fixed, the README should explicitly say:

Rules live at <project>/.claude/hookify.<rule-name>.local.md. User-scope rules at ~/.claude/hookify.*.local.md are not loaded — copy or symlink them into each project where they should apply.

Happy to send a PR.

View original on GitHub ↗

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