Repository navigation
Description
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"; fiBefore 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