Repository navigation
Hook install/uninstall identifies graphify's hooks by the substring "graphify" and drops whole hook groups, including other commands in them #4359
Description
Summary
When graphify installs or removes its own PreToolUse hooks, it identifies them by checking whether the case-sensitive substring graphify occurs anywhere in the stringified hook group. For Claude Code and CodeBuddy the group must additionally use one of the matchers recognised by the removal filter (Glob|Grep, Bash, Bash|Grep, Read|Glob; the current install writes Bash|Grep and Read|Glob); for Codex there is no matcher restriction. Every group that passes this filter is removed as a whole, including other commands in it and unrelated groups whose command path happens to contain graphify. The uninstall path rewrites the settings file without creating a backup.
Where (v0.9.84, graphify/install.py)
- Claude:
_install_claude_hookL2078 and_strip_graphify_hookL2102 -h.get("matcher") in ("Glob|Grep", "Bash", "Bash|Grep", "Read|Glob") and "graphify" in str(h). Install writes through_write_settings_with_backup(L2080); uninstall writes withsettings_path.write_text(json.dumps(settings, indent=2), ...)at L2106. - Codex: L1770 and L1790 -
"graphify" not in str(h), no matcher restriction. - Gemini (
BeforeTool): L1012 and L1026; CodeBuddy: L2261 and L2275 (same matcher restriction as for Claude Code). - Permalink:
Lines 2078 to 2106 in 2cb81c6
hooks["PreToolUse"] = [h for h in pre_tool if not (isinstance(h, dict) and h.get("matcher") in ("Glob|Grep", "Bash", "Bash|Grep", "Read|Glob") and "graphify" in str(h))] hooks["PreToolUse"].extend(_claude_pretooluse_hooks(strict=strict, project=project)) _write_settings_with_backup(settings_path, settings) _mode = " (strict)" if strict else "" print(f" .claude/settings.json -> PreToolUse hooks registered (Bash|Grep search + Read/Glob){_mode}") def _uninstall_claude_hook(project_dir: Path) -> None: """Remove the graphify PreToolUse hook from .claude/settings.json and its local-only sibling .claude/settings.local.json. A user may relocate the hook into settings.local.json so it is not committed to a shared repo, so uninstall has to clean whichever file holds it (#1731). """ claude_dir = project_dir / ".claude" for name in ("settings.json", "settings.local.json"): _strip_graphify_hook(claude_dir / name) def _strip_graphify_hook(settings_path: Path) -> None: """Drop graphify PreToolUse hooks from a single Claude settings file, if present.""" if not settings_path.exists(): return try: settings = json.loads(settings_path.read_text(encoding="utf-8")) except json.JSONDecodeError: return pre_tool = settings.get("hooks", {}).get("PreToolUse", []) filtered = [h for h in pre_tool if not (h.get("matcher") in ("Glob|Grep", "Bash", "Bash|Grep", "Read|Glob") and "graphify" in str(h))] if len(filtered) == len(pre_tool): return settings["hooks"]["PreToolUse"] = filtered settings_path.write_text(json.dumps(settings, indent=2), encoding="utf-8")
For Claude Code, the hooks are installed by graphify claude install [--project] and removed by graphify claude uninstall [--project] and by graphify uninstall.
Why it matters
str(h) covers the whole group ({"matcher": ..., "hooks": [...]}), not graphify's individual command, so:
- If a user adds their own command to the
hooksarray of one of graphify's groups, the whole group is removed on the next hook install or uninstall, including the user's command. - A separate group of another tool that uses one of the four matchers above (any matcher for Codex) and whose command path contains
graphify(for example a script under a folder namedgraphify-notes) is removed as well. - Uninstall does not create or update a
.graphify-bakof the settings file, so recovering the removed entries depends on an earlier backup or version control. The file is also re-serialised withjson.dumps(settings, indent=2), which escapes non-ASCII characters and changes the indentation to two spaces.
Guard hooks (security, formatting, audit) are typical entries in these files, and a guard that disappears without a specific message is easy to miss.
Minimal reproduction
- Run
graphify claude install --projectso that.claude/settings.jsoncontains graphify'sPreToolUsegroups. - Add your own command as a second object in the
hooksarray of graphify'sBash|Grepgroup. Add a separatePreToolUsegroup with matcherBashwhose command path containsgraphify(e.g.python /tools/graphify-notes/check.py). - Run
graphify claude uninstall --projectin the same project directory.
Expected: only graphify's own commands are removed; your command and the unrelated group stay.
Actual: both groups are removed completely, and no backup of the previous settings file is written.
Suggested fix
- Identify graphify's commands inside the individual
hooks[]elements instead of matchingstr(group), covering the variants it writes (hook-guard search,hook-guard readwith optional--strict,hook-guard gemini,hook-check, with the emitted executable path). Remove only those commands and drop a group only when it becomes empty. - Optionally tag graphify's entries with a dedicated marker so they can be found unambiguously (if the host accepts the extra field).
- Use
_write_settings_with_backup(or an equivalent single backup) on the uninstall paths too. - In
_strip_graphify_hook, guardh.get(...)against non-dict entries (a string entry raisesAttributeError; the install path already checksisinstance(h, dict)).
Environment
graphify 0.9.84 (code re-checked on the release commit above). Windows 11, Claude Code. Found during an internal security review.
Activity
github-actions commentedon Oct 12, 2026
Thanks for opening this issue, @ujconsulting. A maintainer will take a look soon.
If you would like to discuss it in real time, come say hi on our Discord server. For longer-form questions and ideas there is also GitHub Discussions.
To help us triage, please make sure the report includes what you expected, what actually happened, and the steps (and a small sample) to reproduce it.
fix(install): remove only graphify's own hook entries, keep others (#…
safishamsi commentedon Oct 12, 2026
Fixed in v0.9.88 (
graphifyy 0.9.88on PyPI) via #4373.