Skip to content

Hook install/uninstall identifies graphify's hooks by the substring "graphify" and drops whole hook groups, including other commands in them #4359

Closed

Description

@ujconsulting

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_hook L2078 and _strip_graphify_hook L2102 - 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 with settings_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:

    graphify/graphify/install.py

    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:

  1. If a user adds their own command to the hooks array of one of graphify's groups, the whole group is removed on the next hook install or uninstall, including the user's command.
  2. 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 named graphify-notes) is removed as well.
  3. Uninstall does not create or update a .graphify-bak of the settings file, so recovering the removed entries depends on an earlier backup or version control. The file is also re-serialised with json.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

  1. Run graphify claude install --project so that .claude/settings.json contains graphify's PreToolUse groups.
  2. Add your own command as a second object in the hooks array of graphify's Bash|Grep group. Add a separate PreToolUse group with matcher Bash whose command path contains graphify (e.g. python /tools/graphify-notes/check.py).
  3. Run graphify claude uninstall --project in 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

  1. Identify graphify's commands inside the individual hooks[] elements instead of matching str(group), covering the variants it writes (hook-guard search, hook-guard read with 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.
  2. Optionally tag graphify's entries with a dedicated marker so they can be found unambiguously (if the host accepts the extra field).
  3. Use _write_settings_with_backup (or an equivalent single backup) on the uninstall paths too.
  4. In _strip_graphify_hook, guard h.get(...) against non-dict entries (a string entry raises AttributeError; the install path already checks isinstance(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

  1. github-actions commented on Oct 12, 2026

    @github-actions

    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.

  2. safishamsi commented on Oct 12, 2026

    @safishamsi
    Member

    Fixed in v0.9.88 (graphifyy 0.9.88 on PyPI) via #4373.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions