fix(uninstall): preserve independent CLI tool dotdirs (#993)#994
Merged
Conversation
find_app_files() built XDG-style deletion candidates (~/.config/<name>, ~/.local/share/<name>, ~/.<name>) from a GUI app's display name. When the display name collides with an independently-shipped CLI tool, uninstalling the GUI app silently deleted the CLI's entire state directory. Observed: - Uninstalling Claude.app wiped ~/.claude (Claude Code CLI: projects, skills, plugins, accumulated cross-project memory). - Uninstalling OpenCode.app wiped ~/.local/share/opencode, ~/.config/opencode and the opencode binary. Case-insensitive APFS amplified the Claude case: $HOME/.Claude built from app_name="Claude" aliases to $HOME/.claude. Fix: introduce INDEPENDENT_CLI_DOTDIR_NAMES (claude, opencode, codex, gemini) and a _path_belongs_to_independent_cli helper that find_app_files consults after the existing empty-name safety check. Match is lowercase + leading-dot-stripped and scoped to $HOME, $HOME/.config, $HOME/.local/share, $HOME/.cache so case-insensitive collisions are caught without affecting unrelated paths. The deny-list is intentionally tight; Maestro Studio / Zed / Firefox style XDG cleanup (tw93#377) is unaffected and is covered by a regression test. Tests: tests/uninstall_naming_variants.bats — 4 new cases (Claude preservation, opencode preservation across all three XDG roots, codex preservation, Zed XDG cleanup still works). bats reports 16/16 passing. Closes tw93#993.
The helper iterated INDEPENDENT_CLI_DOTDIR_NAMES, which is a readonly array declared in app_protection_data.sh. Bats 1.x carries readonly scalar reentry guards from setup() into the @test body but not readonly arrays, so once find_app_files reached the helper the array was undeclared and set -u tripped "unbound variable" on every protected case. The deny-list staying live in fresh bats subshells is also more robust than relying on the data file being re-sourced. Inline the four names in the helper and keep the array in the data file as a discoverability anchor (with a sync note in the helper). Restores maestro-studio / Firefox Developer Edition / Claude / OpenCode / Codex / Zed Nightly tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #993.
Problem
find_app_files()inlib/core/app_protection.shadds XDG-style candidate paths derived from a GUI app's display name:When the display name collides with an independently-shipped CLI tool,
mo uninstall <GUI app>silently wipes the CLI's entire state directory. Reproduced on the same machine within minutes:Claude.app→~/.claude(Claude Code CLI: projects, skills, plugins, accumulated cross-project memory) gone.OpenCode.app→~/.local/share/opencode,~/.config/opencode,~/.opencode(sst/opencode CLI sessions/snapshots/logs) + theopencodebinary gone.Case-insensitive APFS amplifies the Claude case:
$HOME/.Claudebuilt fromapp_name="Claude"aliases to$HOME/.claudeand passes the[[ -e ]]existence check.MOLE_UNINSTALL_MODE=1then bypassesshould_protect_data(intentionally), and the path is unlinked.Fix approach
The XDG patterns themselves are intentional —
tests/uninstall_naming_variants.batsalready enforces them for Maestro Studio / Zed / Firefox via issue #377, so removing them would regress. What's missing is a way to distinguish:~/.config/<name>or~/.local/share/<name>itself (Zed, Firefox, Maestro) → cleanup is correct.This PR adds a tight deny-list
INDEPENDENT_CLI_DOTDIR_NAMESinlib/core/app_protection_data.shand a helper_path_belongs_to_independent_cliconsulted inside the existingfind_app_filesper-pattern loop. Match is lowercase + leading-dot-stripped (handles case-insensitive APFS) and scoped to$HOME,$HOME/.config,$HOME/.local/share,$HOME/.cacheso unrelated paths never get skipped.Initial list:
claude,opencode,codex,gemini. Easy to extend as more GUI/CLI namesakes show up.Tests
tests/uninstall_naming_variants.bats— 4 new cases:Claude.apppreserves~/.claudeand still removes~/Library/Application Support/Claude.OpenCode.apppreserves all three opencode XDG roots (~/.local/share/opencode,~/.config/opencode,~/.opencode) and still removes the Library path.Codex.app/~/.codex.Zed Nightlystill picks up~/.config/zed/~/.local/share/zed(deny-list must not over-protect issue [BUG] maestro studio delete #377 behavior).Full local verification:
bats tests/uninstall_naming_variants.bats→ 16/16 pass (12 existing + 4 new).bats tests/uninstall_safety.bats tests/uninstall_remove_file_list.bats tests/uninstall.bats tests/uninstall_scan_bash32.bats→ 79/79 pass.bash scripts/check.sh→ shellcheck + bash syntax check pass; shfmt applied no further changes.I also patched a local install of Mole 1.39.1 with the equivalent change and confirmed
mo uninstallagainstOpenCode.appno longer lists~/.local/share/opencode/~/.config/opencode/~/.opencodein the deletion preview while still listing the GUI app's~/Library/Application Support/opencode.Related context
~/.claude/projects/*/memory/— months of irreplaceable context lost #823 (closed by32bfb5b9) fixedmo cleanwiping~/.claude/projects/*/memory/by removing automatic cleanup of~/.claude/*fromlib/clean/dev.sh. Different code path — doesn't cover uninstall.mo clean误删 Claude Code CLI 的 active 版本,导致claude命令失效 #801 (closed by6f13e08) added symlink-aware preservation for~/.local/share/claude/versions/. Different mechanism.Neither prior fix touched
find_app_files(), which is why the uninstall path regressed independently.Notes for review
INDEPENDENT_CLI_DOTDIR_NAMESif you have other namesakes in mind.