Skip to content

fix(uninstall): preserve independent CLI tool dotdirs (#993)#994

Merged
tw93 merged 2 commits into
tw93:mainfrom
Jiaweeee:fix/uninstall-preserve-cli-dotdirs
May 28, 2026
Merged

fix(uninstall): preserve independent CLI tool dotdirs (#993)#994
tw93 merged 2 commits into
tw93:mainfrom
Jiaweeee:fix/uninstall-preserve-cli-dotdirs

Conversation

@Jiaweeee
Copy link
Copy Markdown
Contributor

Closes #993.

Problem

find_app_files() in lib/core/app_protection.sh adds XDG-style candidate paths derived from a GUI app's display name:

"$HOME/.config/$app_name"
"$HOME/.local/share/$app_name"
"$HOME/.$app_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:

  • Uninstalling Claude.app~/.claude (Claude Code CLI: projects, skills, plugins, accumulated cross-project memory) gone.
  • Uninstalling OpenCode.app~/.local/share/opencode, ~/.config/opencode, ~/.opencode (sst/opencode CLI sessions/snapshots/logs) + the opencode binary gone.

Case-insensitive APFS amplifies the Claude case: $HOME/.Claude built from app_name="Claude" aliases to $HOME/.claude and passes the [[ -e ]] existence check. MOLE_UNINSTALL_MODE=1 then bypasses should_protect_data (intentionally), and the path is unlinked.

Fix approach

The XDG patterns themselves are intentional — tests/uninstall_naming_variants.bats already enforces them for Maestro Studio / Zed / Firefox via issue #377, so removing them would regress. What's missing is a way to distinguish:

  • GUI app that writes to ~/.config/<name> or ~/.local/share/<name> itself (Zed, Firefox, Maestro) → cleanup is correct.
  • GUI app that happens to share a name with an unrelated CLI tool (Claude Desktop / Claude Code, OpenCode.app / opencode CLI) → cleanup destroys unrelated data.

This PR adds a tight deny-list INDEPENDENT_CLI_DOTDIR_NAMES in lib/core/app_protection_data.sh and a helper _path_belongs_to_independent_cli consulted inside the existing find_app_files per-pattern loop. Match is lowercase + leading-dot-stripped (handles case-insensitive APFS) and scoped to $HOME, $HOME/.config, $HOME/.local/share, $HOME/.cache so 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:

  1. Uninstalling Claude.app preserves ~/.claude and still removes ~/Library/Application Support/Claude.
  2. Uninstalling OpenCode.app preserves all three opencode XDG roots (~/.local/share/opencode, ~/.config/opencode, ~/.opencode) and still removes the Library path.
  3. Same for Codex.app / ~/.codex.
  4. Regression: uninstalling Zed Nightly still 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 uninstall against OpenCode.app no longer lists ~/.local/share/opencode / ~/.config/opencode / ~/.opencode in the deletion preview while still listing the GUI app's ~/Library/Application Support/opencode.

Related context

Neither prior fix touched find_app_files(), which is why the uninstall path regressed independently.

Notes for review

  • Deny-list approach intentionally tight rather than heuristic. A "binary on PATH" check would be more generic but would break Zed-style GUI apps that ship a same-named launcher CLI.
  • Trash-by-default (so future false positives are recoverable) is out of scope here and probably its own PR.
  • Happy to add more entries to INDEPENDENT_CLI_DOTDIR_NAMES if you have other namesakes in mind.

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.
@Jiaweeee Jiaweeee requested a review from tw93 as a code owner May 28, 2026 02:36
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.
@tw93 tw93 merged commit 3fa3eb5 into tw93:main May 28, 2026
9 checks passed
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.

[BUG] mo uninstall destroys same-named CLI data via XDG-dotdir heuristic (Claude Code, opencode)

2 participants