Conversation
Reported as an install problem (Graphify-Labs#3572). Install is fine on `v8`: with `CLAUDE_CONFIG_DIR` set and a sandbox HOME, the skill tree, its references and the registration all land under the config dir and nothing touches HOME. What does not work is the other direction, in two independent ways that stack. First, `claude_uninstall`'s `md_targets` were all `project_dir`-relative, so a user-global uninstall deleted the global skill tree and then looked for the registration in whatever directory it was run from. Measured: the skill goes, `$CLAUDE_CONFIG_DIR/CLAUDE.md` keeps its three graphify lines, and the command prints "No CLAUDE.md found in current directory - nothing to do". Second, adding the global file to that list was not enough. The project block comes from `always_on/claude-md.md` and opens with `## graphify`; the global block comes from `_skill_registration` and opens with `# graphify`. The strip matched only the H2, so the global block was unremovable wherever it lived. So: one `_global_claude_md()` resolver that install and uninstall both read, so they cannot drift on `CLAUDE_CONFIG_DIR` again; the global file appended to the uninstall targets only when the global skill is being removed, so `project=True` still leaves it alone (Graphify-Labs#2215); and the strip falls through to the H1 form, ending at the next heading of any level rather than the next H2. Six cells. Both locations, the user's own notes surviving, a project uninstall leaving the global block alone, and a section the user wrote BELOW the block surviving, which is reachable because install appends to the end of the file. Five mutations, all killed; the last of those cells exists because the next-H2 boundary survived the first run of the battery.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Fixes a user-global graphify uninstall orphaning its CLAUDE.md registration: install and uninstall now resolve the same file through _global_claude_md, which honours CLAUDE_CONFIG_DIR on both sides instead of only at install time. When remove_user_skill is set, uninstall appends the global CLAUDE.md to its targets and strips the block by its H1 marker (ending at the next heading of any level) as a fallback when the H2 project marker doesn't match, and the "nothing to do" message now names every path searched. Project uninstalls are unchanged, and a section-strip preserves any user notes above or below the block.
Worth a look
- Project uninstall now strips top-level # graphify sections from local CLAUDE.md —
graphify/install.py:1991· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Global CLAUDE.md strip uses H1 boundary that stops at the block's own H1, removing nothing —
graphify/install.py:1994· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 544 functions depend on the 314 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 124 callees - new:
claude_uninstall()— 21 callers, 5 callees - new:
codebuddy_install()— 20 callers, 5 callees - new:
claude_install()— 19 callers, 4 callees - new:
_copy_skill_file()— 12 callers, 6 callees - new:
gemini_install()— 10 callers, 7 callees - new:
_project_uninstall()— 5 callers, 13 callees - new:
dispatch_install_cli()— 2 callers, 31 callees - …and 14 more — each is listed as a finding
Verification — 544 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: 463 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
31 of 275 test file(s) selected (11%) via static blast radius.
tests/test_affected_cli.py— impacttests/test_agents_platform.py— impacttests/test_atomic_version_stamp.py— impacttests/test_claude_md.py— impacttests/test_codebuddy.py— impacttests/test_devin.py— impacttests/test_explain_cli.py— impacttests/test_extract_cli.py— impacttests/test_gemini_hook.py— impacttests/test_global_add_tag_inference.py— impacttests/test_god_nodes_cli.py— impacttests/test_hollow_chunks_arm_shrink_guard.py— impacttests/test_home_sandbox.py— impacttests/test_hook_strict.py— impacttests/test_incomplete_build_guard.py— impacttests/test_install.py— impact, changed-testtests/test_install_references.py— impacttests/test_install_strings.py— impacttests/test_install_version_warning.py— impacttests/test_merge_chunks_validation.py— impacttests/test_multigraph_diagnostics.py— impacttests/test_no_dedup_flag.py— impacttests/test_path_cli.py— impacttests/test_query_cli.py— impacttests/test_query_induced_edges.py— impacttests/test_read_hook.py— impacttests/test_replace_or_append_section.py— impacttests/test_search_hook.py— impacttests/test_skill_version_warning.py— impacttests/test_uninstall_scope.py— impacttests/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
Could not verify: Could not verify claude\_uninstall.
The verifier did not have enough to check claude\_uninstall, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `project_dir` is annotated `Path | None` — outside the synthesizable primitive/collection set
Could not verify: Could not verify install.
The verifier did not have enough to check install, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
Could not verify: Could not verify \_strip\_graphify\_md\_section.
The verifier did not have enough to check \_strip\_graphify\_md\_section, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: parameter `target` is annotated `Path` — outside the synthesizable primitive/collection set
· 22 more finding(s) on lines outside this diff (see the check run).
Found via #3572, but not the defect that issue names. Install is fine on
v8, and the reverse direction is broken in two ways that stack.What I measured
Install honours
CLAUDE_CONFIG_DIRcompletely. SandboxHOME, variable set:Nothing in
HOME._platform_skill_destinationreads the variable for the skill tree (#527) and the registration reads it too (#2694). So the reporter is very likely on a version before those, and I said so on the issue.Uninstall does not.
After
graphify uninstall, Claude Code still loads a graphify block pointing at aSKILL.mdthat was just deleted. The same happens with noCLAUDE_CONFIG_DIRat all, against~/.claude/CLAUDE.md, so the variable is not what makes it fail.Two defects, stacked
One.
claude_uninstall's targets were allproject_dir-relative:A user-global uninstall deletes the global skill tree, then looks for the registration in whatever directory it was run from.
Two, and this is the half that made the first fix look wrong. Adding the global file to that list still did not remove the block. The project section comes from
always_on/claude-md.mdand opens## graphify; the global one comes from_skill_registrationand opens# graphify._strip_graphify_md_sectionmatched only the H2, so the global block was unremovable wherever it lived. The intermediate state printedgraphify section not found in CLAUDE.md, which is how I found it.The fix
_global_claude_md()resolves the file once, and install and uninstall both read it, so they cannot drift on the variable again. Install's inline branch is replaced by the call.project=Truestill leaves it alone, as claude_uninstall still ignores project_dir with project=False (root cause of #2168, unfixed by v0.9.27) #2215 documents.Tests
Six cells in
tests/test_install.py, alongside the existingtest_install_claude_md_honors_claude_config_dir. They install and then uninstall from an unrelated working directory, which is what a user does and what made the gap invisible.CLAUDE_CONFIG_DIRMutation results, 5 of 5 caught:
CLAUDE_CONFIG_DIRThe last one survived the first run of the battery, and the cell for it is why it does not now. It is reachable rather than theoretical: install appends the block to the end of the file, so anything the user writes afterwards sits below it, and a next-H2 boundary would take an
# My own rulessection with it.tests/test_install.pyandtests/test_claude_md.py: 126 passed. Ruff clean.tests/test_install_references.py::test_built_wheel_ships_the_full_skill_payloadfails on my machine. It fails identically with this branch stashed, so it is not this change.