Skip to content

[Bug]: Removing supplemental extension skills deletes unrelated user files (rmtree) #5381

Open

Description

@markuswondrak

Bug Description

ExtensionManager._unregister_extension_skills removes non-generic supplemental extension skill directories with shutil.rmtree(skill_subdir) (src/specify_cli/extensions/__init__.py, ~l.2230 on main). Any file a user added to such a skill directory (notes, extra references) is deleted on extension remove (confirmed). extension update runs the same removal before reinstalling (code inspection; not reproduced end-to-end). Ownership is only checked through the SKILL.md provenance marker, not per file.

The generic branch already uses hash-checked SKILL.md removal plus rmdir, which keeps unrelated files.

Steps to Reproduce

Bob in its default skills mode (supplemental skills route), on main @ 7ec720e:

tmp=$(mktemp -d)
mkdir -p "$tmp/ext/commands"

cat > "$tmp/ext/extension.yml" <<'EOF'
schema_version: '1.0'
extension:
  id: sample
  name: Sample
  version: 1.0.0
  description: Test extension
  author: Test
requires:
  speckit_version: '>=0.1.0'
provides:
  commands:
    - name: speckit.sample.review
      file: commands/review.md
EOF

cat > "$tmp/ext/commands/review.md" <<'EOF'
---
description: Review
---
Review the project.
EOF

cd "$tmp"
specify init project --integration bob --script sh \
  --non-interactive --ignore-agent-tools
cd project
specify extension add --dev "$tmp/ext"

skill=.bob/skills/speckit-sample-review
echo 'personal notes' > "$skill/my-notes.md"
ls -la "$skill"

specify extension remove sample --force
if test -d "$skill"; then ls -la "$skill"; else echo "directory deleted"; fi

Before removal the skill dir contains SKILL.md and my-notes.md; after extension remove the whole directory is gone.

Minimal pytest reproduction (passes while the bug is present):

import yaml
from specify_cli import save_init_options
from specify_cli.extensions import ExtensionManager


def test_supplemental_skill_removal_deletes_user_file(tmp_path):
    project = tmp_path / "project"
    (project / ".specify").mkdir(parents=True)
    save_init_options(project, {"ai": "bob", "ai_skills": True, "script": "sh"})

    extension = tmp_path / "extension"
    (extension / "commands").mkdir(parents=True)
    (extension / "commands/review.md").write_text(
        "---\ndescription: Review\n---\nReview the project.\n"
    )
    (extension / "extension.yml").write_text(yaml.safe_dump({
        "schema_version": "1.0",
        "extension": {
            "id": "sample", "name": "Sample", "version": "1.0.0",
            "description": "Test extension", "author": "Test",
        },
        "requires": {"speckit_version": ">=0.1.0"},
        "provides": {"commands": [{
            "name": "speckit.sample.review",
            "file": "commands/review.md",
        }]},
    }))

    manager = ExtensionManager(project)
    manager.install_from_directory(extension, "1.0.0")
    skill = project / ".bob/skills/speckit-sample-review"
    assert (skill / "SKILL.md").is_file()
    assert manager.registry.get("sample")["registered_skills"] == [
        "speckit-sample-review"
    ]
    notes = skill / "my-notes.md"
    notes.write_text("personal notes\n")
    assert notes.is_file()
    assert manager.remove("sample")
    assert not skill.exists()  # Bug: the user-authored file was deleted.

Expected Behavior

Only files the extension created (SKILL.md and recorded resources) are removed; the directory is removed only if empty. User files survive.

Actual Behavior

The whole skill directory, including my-notes.md, is deleted.

Specify CLI Version

main @ 7ec720e (1.3.4.dev0)

AI Agent

Bob and Copilot in their default skills mode (supplemental route); native agents such as Claude/Kimi only when command registration is disabled

Operating System

Linux 6.17

Python Version

3.14.4

Error Logs

n/a

Additional Context

Found while planning #5366 (extension skill resources). Reproduced end-to-end with Bob's default skills mode on clean main at 7ec720e8; pytest and ownership-marker controls also passed (a mismatched metadata.source marker preserves the directory; native Claude/Kimi command registration and the generic integration preserve the extra file). #5366 keeps this behaviour unchanged and documents it as an exception.

AI Disclosure

Drafted with the pi coding agent (orchestrator model: Claude Opus 5.5 via GitHub Copilot; implementation/review subagents: GPT-6.1-terra, GPT-6-astra), human-supervised by the filer. Reproductions were run locally by the agent and reviewed by the filer before filing.

Activity

  1. added
    triage-must-haveVerdict: high-value, important work for Spec Kit — do first
    on Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    triage-must-haveVerdict: high-value, important work for Spec Kit — do first

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions