Sitelet https://github.com/Graphify-Labs/graphify/pull/3953
Skip to content

fix(install): keep a symlinked instructions file on uninstall - #3953

Closed
bercedev wants to merge 1 commit into
Graphify-Labs:v8from
bercedev:fix/uninstall-symlink-target
Closed

bercedev wants to merge 1 commit into
Graphify-Labs:v8from
bercedev:fix/uninstall-symlink-target

Conversation

@bercedev

Copy link
Copy Markdown
Contributor

What does this PR do?

Follow-up to the review on #3950 (comment). Refs #3805.

When removing graphify's section leaves an instructions file empty, uninstall deletes the file. If that file is a symlink, only the link gets deleted. The graphify section stays in the file the link points to, and the user loses their link.

Repro with a project whose .claude/CLAUDE.md is a symlink to an empty dotfiles/rules.md:

$ graphify install --project      # writes "# graphify" into dotfiles/rules.md through the link
$ graphify uninstall --project

# v8
  CLAUDE.md        ->  deleted .claude/CLAUDE.md
.claude/CLAUDE.md exists: no     dotfiles/rules.md still has "# graphify": yes

# this branch
  CLAUDE.md        ->  graphify skill registration removed from .claude/CLAUDE.md
.claude/CLAUDE.md exists: yes    dotfiles/rules.md still has "# graphify": no

The fix: when the path is a symlink, uninstall writes the cleaned content through it instead of unlinking. Regular files that end up empty are still deleted as before. The same if cleaned: write / else: unlink pattern is in six places, and all six get the same one-line change:

  • .claude/CLAUDE.md skill registration (_remove_claude_skill_registration)
  • CLAUDE.md, CLAUDE.local.md, .claude/CLAUDE.local.md (_strip_graphify_md_section)
  • GEMINI.md, AGENTS.md, CODEBUDDY.md, .github/copilot-instructions.md

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

Invariant: uninstall never removes a user's symlink, and it removes graphify's section from the file that install actually wrote to.

  • Read the CONTRIBUTING.md guide.
  • Reproduced the issue and identified the invariant.
  • Made the smallest fix necessary.
  • Added a regression test (if bug fix) or isolated boundary test.
  • Kept the PR description synchronized with the final implementation.
  • Documented any limitations / unsupported cases explicitly.

Limitations:

  • If the link target contained only the graphify section, it is left as an empty file (one newline) instead of being deleted. The file belongs to the user, so I didn't want uninstall to delete it.
  • A dangling symlink is still treated as "no file" and left alone, same as before.

How was this tested?

# new tests against the unchanged v8 install.py
.venv/bin/python -m pytest -q tests/test_uninstall_symlink.py
6 failed, 6 passed      # every symlink case: "the user's symlink was deleted"

# with the fix
.venv/bin/python -m pytest -q tests/test_uninstall_symlink.py
12 passed

.venv/bin/python -m pytest -q
6173 passed, 14 skipped

tests/test_uninstall_symlink.py runs each of the six uninstall paths twice. The symlink case checks that the link is kept and the section is gone from the target. The regular-file case checks that a file left empty is still deleted. Those pass both before and after the fix. The symlink tests use the existing requires_symlinks fixture, so they skip on Windows shells that can't create symlinks.

I also ran the repro above with the real CLI and a throwaway HOME, on v8 and on this branch. The output is shown above.

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments.
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.

Note: I used Claude Code while working on this fix. I reviewed and ran all of it myself.

When removing the graphify section leaves an instructions file empty,
uninstall deletes it. If the file is a symlink, only the link was deleted
and the graphify section stayed in the file it points to. Now uninstall
writes through the symlink instead, so the target is cleaned and the
link stays.

Applies to .claude/CLAUDE.md, CLAUDE.md, GEMINI.md, AGENTS.md,
CODEBUDDY.md and .github/copilot-instructions.md.
@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @bercedev. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. 6 change(s) tested, no difference found (not proven).


Graphify review — findings

Fixes uninstall for symlinked instruction files (CLAUDE.md, .claude/CLAUDE.md, GEMINI.md, AGENTS.md, CODEBUDDY.md, .github/copilot-instructions.md). Previously, when stripping graphify's section left one empty, uninstall deleted the path, which removed only the link and left the section in the target. Now uninstall writes the emptied content through the link, keeping the user's symlink and cleaning the file it points to; empty regular files are still deleted.

No blocking issues surfaced. 6 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 493 functions depend on the 155 functions this change touches.

Health — this change adds coupling hotspots:

  • new: main() — 98 callers, 3 callees
  • new: dispatch_command() — 2 callers, 126 callees
  • new: install() — 13 callers, 11 callees
  • new: _copy_skill_file() — 15 callers, 7 callees
  • new: codebuddy_install() — 20 callers, 5 callees
  • new: _refresh_stale_skills() — 25 callers, 4 callees
  • new: gemini_install() — 11 callers, 7 callees
  • new: claude_install() — 19 callers, 4 callees
  • …and 20 more — each is listed as a finding

Verification — 493 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 339 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

33 of 309 test file(s) selected (11%) via static blast radius.

  • tests/test_affected_cli.py — impact
  • tests/test_agents_platform.py — impact
  • tests/test_atomic_version_stamp.py — impact
  • tests/test_claude_md.py — impact
  • tests/test_codebuddy.py — impact
  • tests/test_devin.py — impact
  • tests/test_explain_cli.py — impact
  • tests/test_extract_cli.py — impact
  • tests/test_gemini_hook.py — impact
  • tests/test_global_add_tag_inference.py — impact
  • tests/test_god_nodes_cli.py — impact
  • tests/test_hollow_chunks_arm_shrink_guard.py — impact
  • tests/test_home_sandbox.py — impact
  • tests/test_hook_strict.py — impact
  • tests/test_incomplete_build_guard.py — impact
  • tests/test_install.py — impact
  • tests/test_install_references.py — impact
  • tests/test_install_strings.py — impact
  • tests/test_install_version_warning.py — impact
  • tests/test_merge_chunks_validation.py — impact
  • tests/test_multigraph_diagnostics.py — impact
  • tests/test_no_dedup_flag.py — impact
  • tests/test_path_cli.py — impact
  • tests/test_query_cli.py — impact
  • tests/test_query_induced_edges.py — impact
  • tests/test_read_hook.py — impact
  • tests/test_replace_or_append_section.py — impact
  • tests/test_search_hook.py — impact
  • tests/test_skill_auto_refresh.py — impact
  • tests/test_skill_version_warning.py — impact
  • tests/test_uninstall_scope.py — impact
  • tests/test_uninstall_symlink.py — impact, changed-test
  • tests/test_unverified_semantic_shrink.py — impact

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

No difference found (not proven): No behavior difference found in \_agents\_uninstall (not a proof).

The verifier ran both versions of \_agents\_uninstall on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_remove\_claude\_skill\_registration (not a proof).

The verifier ran both versions of \_remove\_claude\_skill\_registration on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_strip\_graphify\_md\_section (not a proof).

The verifier ran both versions of \_strip\_graphify\_md\_section on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in codebuddy\_uninstall (not a proof).

The verifier ran both versions of codebuddy\_uninstall on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in gemini\_uninstall (not a proof).

The verifier ran both versions of gemini\_uninstall on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in vscode\_uninstall (not a proof).

The verifier ran both versions of vscode\_uninstall on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

· 28 more finding(s) on lines outside this diff (see the check run).

safishamsi added a commit that referenced this pull request Sep 30, 2026
Solidity free functions (#3906), VB.NET qualified calls (#3909), Astro
frontmatter-only AST pass (#3902), surprise bonus/reason alignment (#3934),
exclude-hubs stranded-neighbour (#3933), graph-DB push index (#3957),
label retry keep-named (#3956), stale-hook status (#3951), virtual-workspace
Cargo.toml skip (#3930), and symlinked-instructions install/uninstall
handling (#3950/#3953).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Member

Shipped in v0.9.73 (now on PyPI) via an authorship-preserving cherry-pick, so your commit keeps contributor-graph credit. Thanks @bercedev!

Release: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.73

@safishamsi safishamsi closed this Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants