From ee1df22b25c1c2e79817783cbcf00f1b3f86f60f Mon Sep 17 00:00:00 2001 From: Safi Date: Tue, 28 Apr 2026 00:56:54 +0100 Subject: [PATCH] =?UTF-8?q?Fix=20PreToolUse=20hook=20for=20Claude=20Code?= =?UTF-8?q?=20v2.1.117+=20(Glob|Grep=20=E2=86=92=20Bash=20matcher)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grep and Glob tools removed in CC v2.1.117; searches now go through Bash. Hook now reads stdin tool_input and pattern-matches on search commands. Uninstall/reinstall handles both old and new matcher for clean upgrades. Closes #578 Co-Authored-By: Claude Sonnet 4.6 --- README.md | 4 ++++ graphify/__main__.py | 20 ++++++++++++++------ pyproject.toml | 2 +- tests/test_claude_md.py | 8 ++++---- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bd692dff..6d211814 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,10 @@ dist/ Same syntax as `.gitignore`. You can keep a single `.graphifyignore` at your repo root — patterns work correctly even when graphify is run on a subfolder. +## What's new in v0.5.2 + +- **Hook fix for Claude Code v2.1.117+** — the PreToolUse hook now matches on `Bash` instead of `Glob|Grep`. Claude Code v2.1.117 removed dedicated Grep/Glob tools; searches now go through Bash. The hook inspects the command string and only fires on search-like calls (grep, rg, find, fd etc.), so it does not trigger on every shell command. + ## What's new in v0.5.1 - **Node ID collision fix** — files sharing the same name in different directories (e.g. two `utils.py` files) now get unique IDs by prefixing the parent directory name. diff --git a/graphify/__main__.py b/graphify/__main__.py index be14274f..34364f01 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -37,14 +37,22 @@ def _refresh_all_version_stamps() -> None: vf.write_text(__version__, encoding="utf-8") _SETTINGS_HOOK = { - "matcher": "Glob|Grep", + # Claude Code v2.1.117+ removed dedicated Grep/Glob tools; searches now go through Bash. + # We match on Bash and inspect the command string to avoid firing on every shell call. + "matcher": "Bash", "hooks": [ { "type": "command", "command": ( - "[ -f graphify-out/graph.json ] && " - r"""echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files."}}' """ - "|| true" + "CMD=$(python3 -c \"" + "import json,sys; d=json.load(sys.stdin); " + "print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); " + "case \"$CMD\" in " + "*grep*|*rg\ *|*ripgrep*|*find\ *|*fd\ *|*ack\ *|*ag\ *) " + " [ -f graphify-out/graph.json ] && " + r""" echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files."}}' """ + " || true ;; " + "esac" ), } ], @@ -857,7 +865,7 @@ def _install_claude_hook(project_dir: Path) -> None: hooks = settings.setdefault("hooks", {}) pre_tool = hooks.setdefault("PreToolUse", []) - hooks["PreToolUse"] = [h for h in pre_tool if not (h.get("matcher") == "Glob|Grep" and "graphify" in str(h))] + hooks["PreToolUse"] = [h for h in pre_tool if not (h.get("matcher") in ("Glob|Grep", "Bash") and "graphify" in str(h))] hooks["PreToolUse"].append(_SETTINGS_HOOK) settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8") print(f" .claude/settings.json -> PreToolUse hook registered") @@ -873,7 +881,7 @@ def _uninstall_claude_hook(project_dir: Path) -> None: except json.JSONDecodeError: return pre_tool = settings.get("hooks", {}).get("PreToolUse", []) - filtered = [h for h in pre_tool if not (h.get("matcher") == "Glob|Grep" and "graphify" in str(h))] + filtered = [h for h in pre_tool if not (h.get("matcher") in ("Glob|Grep", "Bash") and "graphify" in str(h))] if len(filtered) == len(pre_tool): return settings["hooks"]["PreToolUse"] = filtered diff --git a/pyproject.toml b/pyproject.toml index fcbc304c..2c39472d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.5.1" +version = "0.5.2" description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = { file = "LICENSE" } diff --git a/tests/test_claude_md.py b/tests/test_claude_md.py index 4a5a519f..f81f10dd 100644 --- a/tests/test_claude_md.py +++ b/tests/test_claude_md.py @@ -109,7 +109,7 @@ def test_install_creates_settings_json(tmp_path): assert settings_path.exists() settings = json.loads(settings_path.read_text()) hooks = settings.get("hooks", {}).get("PreToolUse", []) - assert any("Glob|Grep" in h.get("matcher", "") for h in hooks) + assert any(h.get("matcher") == "Bash" for h in hooks) def test_install_settings_json_idempotent(tmp_path): @@ -120,8 +120,8 @@ def test_install_settings_json_idempotent(tmp_path): settings_path = tmp_path / ".claude" / "settings.json" settings = json.loads(settings_path.read_text()) hooks = settings.get("hooks", {}).get("PreToolUse", []) - glob_grep_hooks = [h for h in hooks if "Glob|Grep" in h.get("matcher", "")] - assert len(glob_grep_hooks) == 1 + bash_hooks = [h for h in hooks if h.get("matcher") == "Bash" and "graphify" in str(h)] + assert len(bash_hooks) == 1 def test_uninstall_removes_settings_hook(tmp_path): @@ -133,4 +133,4 @@ def test_uninstall_removes_settings_hook(tmp_path): if settings_path.exists(): settings = json.loads(settings_path.read_text()) hooks = settings.get("hooks", {}).get("PreToolUse", []) - assert not any("Glob|Grep" in h.get("matcher", "") for h in hooks) + assert not any(h.get("matcher") == "Bash" and "graphify" in str(h) for h in hooks)