hookify plugin: rule discovery only globs cwd-relative `./.claude/` — never finds user-scope `~/.claude/` rules
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.mdare not loaded — copy or symlink them into each project where they should apply.
Happy to send a PR.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗