Fix PreToolUse hook for Claude Code v2.1.117+ (Glob|Grep → Bash matcher)

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 <noreply@anthropic.com>
This commit is contained in:
Safi
2026-04-28 00:56:54 +01:00
parent a566bfb566
commit ee1df22b25
4 changed files with 23 additions and 11 deletions
+4
View File
@@ -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.
+14 -6
View File
@@ -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
+1 -1
View File
@@ -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" }
+4 -4
View File
@@ -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)