Skip to content

claude_uninstall still ignores project_dir with project=False (root cause of #2168, unfixed by v0.9.27) #2215

Description

@AirRocker

Context

#2168 reported two things: the test suite silently deleting the developer's installed skill, and the API behaviour underneath that makes it possible. It closed with fix option (a) — v0.9.27 adds an autouse _sandbox_home fixture in tests/conftest.py that sandboxes HOME/USERPROFILE/LOCALAPPDATA, clears CLAUDE_CONFIG_DIR/XDG_CONFIG_HOME, and monkeypatches Path.home. That is a solid fix and it does close the contributor-facing damage — running pytest tests/ no longer touches a real ~/.claude.

This issue tracks fix option (b), which did not ship: the root cause is still present in install.py.

Still present at v0.9.27

Verified against the v0.9.27 tag.

claude_uninstallgraphify/install.py:1801:

def claude_uninstall(project_dir: Path | None = None, *, project: bool = False) -> None:
    ...
    project_dir = project_dir or Path(".")
    _remove_skill_file("claude", project=project, project_dir=project_dir)   # line 1814

project still defaults to False. _remove_skill_file (install.py:232) forwards both arguments to _platform_skill_destination (install.py:68), where the project branch is the only one that consults project_dir:

    cfg = _PLATFORM_CONFIG[platform_name]
    if project:
        return (project_dir or Path(".")) / cfg["skill_dst"]           # line 117

    if platform_name in ("claude", "windows") and os.environ.get("CLAUDE_CONFIG_DIR"):
        return Path(os.environ["CLAUDE_CONFIG_DIR"]) / "skills" / "graphify" / "SKILL.md"
    return Path.home() / cfg["skill_dst"]                              # line 121

With project=False, the project_dir that was passed in is accepted, defaulted, and forwarded — then ignored. Resolution falls through to Path.home() / cfg["skill_dst"], i.e. ~/.claude/skills/graphify/SKILL.md (_PLATFORM_CONFIG["claude"]["skill_dst"], install.py:328). _remove_skill_file then unlinks SKILL.md (:237) and .graphify_version (:242), shutil.rmtrees references/ (:246), and rmdirs upward through the parents (:250).

For what it's worth, git diff v0.9.25 v0.9.27 -- graphify/install.py contains no claude_uninstall, project_dir, or _platform_skill_destination changes — the install.py delta in that release is the #2167 settings-clobber work.

Why it still matters

The suite is sandboxed now, so the CI/contributor path is closed. The path reachable from real usage is not:

  • graphify claude uninstall run from inside a project directory, without --project, removes the global skill rather than that project's. From the user's side it reads as uninstalling the project's copy; the global tree is what disappears, and it fails silently in the sense that nothing indicates the wrong scope was hit.
  • Any programmatic claude_uninstall(some_path) call behaves the same way. The signature invites the assumption that passing a path scopes the operation — that assumption is exactly what produced Running pytest tests/ silently deletes the user's installed Claude skill (test_claude_md.py + claude_uninstall project_dir leak) #2168.
  • A future test that legitimately sidesteps the fixture, or any caller outside the suite, re-opens the original hole. The fixture protects the suite, not the function.

Possible fixes — maintainers' call

Either direction closes it; it's a design decision:

  1. Honour project_dir when it is passed — treat a non-None project_dir as implying project scope. Matches what the signature reads like today.
  2. Keep the current semantics but make them explicit — either drop project_dir from the signature when project is the scope selector, or raise on the project_dir is not None and not project combination instead of silently ignoring it.

(2) is the smaller change and removes the ambiguity outright; (1) is closer to what callers appear to expect. Same offer as last time — happy to submit a PR for whichever you prefer, I didn't want to presume the design call.

Note

Unrelated to #2162.

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