From 7af972ea789b1ed31cedb3642266352d7237dbc3 Mon Sep 17 00:00:00 2001 From: Aleksandr Date: Tue, 10 Mar 2026 23:44:40 +0500 Subject: [PATCH] fix: resolve hookify rules from the project root --- plugins/hookify/core/config_loader.py | 22 +++++- plugins/hookify/tests/test_config_loader.py | 83 +++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 plugins/hookify/tests/test_config_loader.py diff --git a/plugins/hookify/core/config_loader.py b/plugins/hookify/core/config_loader.py index fa2fc3e36f..98d01b8656 100644 --- a/plugins/hookify/core/config_loader.py +++ b/plugins/hookify/core/config_loader.py @@ -195,6 +195,26 @@ def extract_frontmatter(content: str) -> tuple[Dict[str, Any], str]: return frontmatter, message +def resolve_project_claude_dir() -> str: + """Resolve the project's .claude directory for hookify rule discovery.""" + project_dir = os.environ.get('CLAUDE_PROJECT_DIR') + if project_dir: + return os.path.join(project_dir, '.claude') + + current_dir = os.getcwd() + while True: + candidate = os.path.join(current_dir, '.claude') + if os.path.isdir(candidate): + return candidate + + parent_dir = os.path.dirname(current_dir) + if parent_dir == current_dir: + break + current_dir = parent_dir + + return os.path.join(os.getcwd(), '.claude') + + def load_rules(event: Optional[str] = None) -> List[Rule]: """Load all hookify rules from .claude directory. @@ -207,7 +227,7 @@ def load_rules(event: Optional[str] = None) -> List[Rule]: rules = [] # Find all hookify.*.local.md files - pattern = os.path.join('.claude', 'hookify.*.local.md') + pattern = os.path.join(resolve_project_claude_dir(), 'hookify.*.local.md') files = glob.glob(pattern) for file_path in files: diff --git a/plugins/hookify/tests/test_config_loader.py b/plugins/hookify/tests/test_config_loader.py new file mode 100644 index 0000000000..48f0e60ada --- /dev/null +++ b/plugins/hookify/tests/test_config_loader.py @@ -0,0 +1,83 @@ +import os +import sys +import tempfile +import unittest +from pathlib import Path + + +sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + +from hookify.core.config_loader import load_rules, resolve_project_claude_dir + + +RULE_MARKDOWN = """--- +name: project-rule +enabled: true +event: bash +conditions: + - field: command + operator: regex_match + pattern: "rm" +action: warn +--- +Warn about rm usage. +""" + + +class ConfigLoaderTests(unittest.TestCase): + def test_load_rules_uses_claude_project_dir_when_available(self): + with tempfile.TemporaryDirectory() as td: + project_root = Path(td) + rules_dir = project_root / ".claude" + rules_dir.mkdir() + (rules_dir / "hookify.project.local.md").write_text(RULE_MARKDOWN) + subdir = project_root / "subdir" + subdir.mkdir() + + original_cwd = Path.cwd() + original_project_dir = os.environ.get("CLAUDE_PROJECT_DIR") + try: + os.environ["CLAUDE_PROJECT_DIR"] = str(project_root) + os.chdir(subdir) + self.assertEqual( + Path(resolve_project_claude_dir()).resolve(), + rules_dir.resolve(), + ) + self.assertEqual(len(load_rules("bash")), 1) + finally: + os.chdir(original_cwd) + if original_project_dir is None: + os.environ.pop("CLAUDE_PROJECT_DIR", None) + else: + os.environ["CLAUDE_PROJECT_DIR"] = original_project_dir + + def test_load_rules_finds_project_root_from_nested_directory(self): + with tempfile.TemporaryDirectory() as td: + project_root = Path(td) + rules_dir = project_root / ".claude" + rules_dir.mkdir() + (rules_dir / "hookify.project.local.md").write_text(RULE_MARKDOWN) + + nested_dir = project_root / "packages" / "app" + nested_dir.mkdir(parents=True) + + original_cwd = Path.cwd() + original_project_dir = os.environ.get("CLAUDE_PROJECT_DIR") + try: + os.environ.pop("CLAUDE_PROJECT_DIR", None) + os.chdir(nested_dir) + self.assertEqual( + Path(resolve_project_claude_dir()).resolve(), + rules_dir.resolve(), + ) + self.assertEqual(len(load_rules("bash")), 1) + finally: + os.chdir(original_cwd) + if original_project_dir is None: + os.environ.pop("CLAUDE_PROJECT_DIR", None) + else: + os.environ["CLAUDE_PROJECT_DIR"] = original_project_dir + + +if __name__ == "__main__": + unittest.main()