Skip to content

gstack-uninstall leaves orphaned ~/.claude/skills/gstack-* directories (prefix-mode install) #896

@xiaodream551-a11y

Description

@xiaodream551-a11y

Summary

gstack-uninstall removes ~/.claude/skills/gstack/ but leaves all the per-skill ~/.claude/skills/gstack-* directories behind as empty shells containing dangling SKILL.md symlinks. This happens when gstack is installed in "prefix" mode (where each skill lives in its own directory rather than as a top-level symlink).

Environment

  • macOS 15.4 (Darwin 25.4.0), Apple Silicon
  • gstack installed via prefix-mode setup at ~/.claude/skills/gstack
  • 36 per-skill directories: gstack-browse, gstack-ship, gstack-plan-ceo-review, etc.

Repro

  1. Install gstack in prefix mode. The installer creates:
    ~/.claude/skills/gstack/                 (real directory, the toolkit)
    ~/.claude/skills/gstack-browse/          (real directory, NOT a symlink)
    ~/.claude/skills/gstack-browse/SKILL.md  (symlink → ../gstack/browse/SKILL.md)
    ~/.claude/skills/gstack-ship/            (real directory, NOT a symlink)
    ~/.claude/skills/gstack-ship/SKILL.md    (symlink → ../gstack/ship/SKILL.md)
    ... 36 total
    
  2. Run ~/.claude/skills/gstack/bin/gstack-uninstall --force
  3. Output:
    Removed: ~/.claude/skills/gstack /Users/<me>/.gstack
    gstack uninstalled.
    
  4. Inspect:
    $ ls ~/.claude/skills/ | grep gstack
    gstack-autoplan
    gstack-benchmark
    gstack-browse
    ... (35 leftover directories)
    
    $ ls -la ~/.claude/skills/gstack-browse
    SKILL.md -> /Users/<me>/.claude/skills/gstack/browse/SKILL.md   (target gone)
    

Expected

All ~/.claude/skills/gstack-* directories created by the installer should be removed, matching the script header docs:

~/.claude/skills/{skill}      — per-skill symlinks created by setup

Actual

35 orphaned directories remain. Each contains a single dangling SKILL.md symlink. They show up in Claude Code's skill list at session start until manually rm -rf'd.

Root cause

bin/gstack-uninstall lines 135-143:

for _LINK in "$CLAUDE_SKILLS"/*; do
  [ -L "$_LINK" ] || continue            # ← bails out for prefix-mode dirs
  _NAME="$(basename "$_LINK")"
  [ "$_NAME" = "gstack" ] && continue
  _TARGET="$(readlink "$_LINK" 2>/dev/null || true)"
  case "$_TARGET" in
    gstack/*|*/gstack/*) rm -f "$_LINK"; REMOVED+=("claude/$_NAME") ;;
  esac
done

The loop only handles entries where ~/.claude/skills/<entry> is itself a symlink. In prefix-mode, each gstack-<name> is a regular directory whose inner SKILL.md is the symlink, so [ -L "$_LINK" ] || continue skips them on the outer pass and the inner symlink is never inspected.

Suggested fix

After the existing loop, add a second pass that handles prefix-mode directories — match gstack-* directories whose SKILL.md is a symlink pointing into gstack/:

for _DIR in "$CLAUDE_SKILLS"/gstack-*; do
  [ -d "$_DIR" ] || continue
  [ -L "$_DIR/SKILL.md" ] || continue
  _TARGET="$(readlink "$_DIR/SKILL.md" 2>/dev/null || true)"
  case "$_TARGET" in
    *gstack/*) rm -rf "$_DIR"; REMOVED+=("claude/$(basename "$_DIR")") ;;
  esac
done

Workaround

rm -rf ~/.claude/skills/gstack-*

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions