Skip to content

fix(install): stop legacy commands cleanup from deleting itself on case-insensitive filesystems#219

Merged
edheltzel merged 1 commit into
mainfrom
worktree-fix-install-symlink-verify
Jul 7, 2026
Merged

fix(install): stop legacy commands cleanup from deleting itself on case-insensitive filesystems#219
edheltzel merged 1 commit into
mainfrom
worktree-fix-install-symlink-verify

Conversation

@edheltzel

Copy link
Copy Markdown
Owner

Problem

Every fresh ./install.sh run left every Recall:* slash command missing, and re-running the installer reproduced the exact same failure forever — only recall doctor --fix actually worked, until the next install.sh run undid it again.

Screenshot reproduced:

✗ Install verification failed — these symlinks are not in their expected state:
    MISSING:   /Users/ed/.claude/commands/Recall/add.md
    MISSING:   /Users/ed/.claude/commands/Recall/doctor.md
    ... (all 9 commands)
✗ Self-check failed — the install did not fully verify; left marked incomplete.

Root cause

install.sh's Commands step creates the commands/Recall/*.md symlinks, then a "legacy lowercase directory" cleanup runs right after to remove any leftover commands/recall/ from an older naming scheme:

local commands_legacy="$CLAUDE_DIR/commands/recall"
...
if [[ -d "$commands_legacy" && "$commands_legacy" != "$commands_dest" ]]; then
  rm -rf "$commands_legacy"
fi

On the default case-insensitive-but-case-preserving macOS filesystem, commands/recall and commands/Recall are the same directory on disk. The plain string != sees them as different paths, so the guard never trips — rm -rf deletes the directory the loop directly above had just populated with symlinks.

The identical block was duplicated in lib/install-lib.sh's recall_copy_runtime_files (used by update.sh), so update.sh had the same bug.

recall doctor --fix "worked" only because its repair path has no such cleanup step — but the next install.sh/update.sh run re-triggered the delete.

Fix

Replace the string comparison with bash's -ef operator (compares by device+inode, so it correctly identifies "same directory on disk" regardless of case-folding) in both duplicated locations.

Testing

  • Added a regression test in tests/install/update.test.ts exercising recall_copy_runtime_files against a case-aliased legacy path.
  • Reproduced the original bug against pre-fix code (symlinks vanished after the Commands step) and confirmed the fix prevents it, via direct function-level testing against lib/install-lib.sh.
  • Full real end-to-end install.sh --yes dry run (isolated sandbox $HOME): self-check passes, all 9 command symlinks survive two consecutive runs (matching the original bug's exact repro).
  • Ran the real, fixed install.sh against my own live $HOME: cleared a stale .install-incomplete marker left over from hitting this exact bug earlier, recall doctor health score went from 26/30 to 27/30 with "Install completed (no interrupted run)" now passing.
  • Full test suite: 1198 pass; 10 pre-existing failures unrelated to this change (OpenCode/Pi MCP JSON-config hardening tests).

🤖 Generated with Claude Code

…se-insensitive filesystems

install.sh's Commands step created the commands/Recall/*.md symlinks, then
immediately deleted them: the legacy-directory cleanup compared
"$CLAUDE_DIR/commands/recall" against "$CLAUDE_DIR/commands/Recall" with a
plain string `!=`. On the default case-insensitive-but-case-preserving macOS
filesystem those two paths resolve to the SAME directory, so the guard never
tripped and `rm -rf` wiped out every symlink the loop above had just created.

Every fresh install left every Recall:* slash command missing; re-running
install.sh reproduced the identical create-then-delete cycle forever, and
only `recall doctor --fix` (which has no such cleanup step) actually fixed
it — until the next install.sh run undid it again.

Fix: compare by device+inode (`-ef`) instead of by string, in both places
this logic is duplicated (install.sh's inline Commands step and
lib/install-lib.sh's recall_copy_runtime_files, used by update.sh).

Added a regression test exercising recall_copy_runtime_files against a
case-aliased legacy path.
@edheltzel edheltzel merged commit 3ed6700 into main Jul 7, 2026
2 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.

1 participant