fix: prevent extension self-install from deleting source dir (#2990)#2991
Open
jinaparkdev wants to merge 1 commit into
Open
fix: prevent extension self-install from deleting source dir (#2990)#2991jinaparkdev wants to merge 1 commit into
jinaparkdev wants to merge 1 commit into
Conversation
…2990) `specify extension add <path> --dev --force` permanently deleted the extension directory without registering it when the source path resolved to the extension's own install location (`.specify/extensions/<id>`). With `--force`, `install_from_directory()` removed the existing installation (the source) and then `shutil.copytree()` tried to copy from the now-deleted directory, destroying it and crashing. Add a guard that fails fast with a clear ValidationError when the resolved source path equals the install destination, before any destructive operation runs. Includes a regression test asserting the directory and its contents survive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a data-loss edge case in the extension installation flow where specify extension add <path> --dev --force could delete the extension directory if the provided source path resolved to the extension’s own install destination (.specify/extensions/<id>). It adds an early guard to reject self-installs before any destructive operations, and includes a regression test to ensure the directory contents remain intact.
Changes:
- Add a fast-fail guard in
ExtensionManager.install_from_directory()to reject installing from the computed install destination path. - Reuse the computed
dest_dirlater in the install flow to avoid recomputation and keep the guard aligned with the actual install target. - Add a regression test asserting self-install attempts with
--forceraise aValidationErrorwithout deleting the extension directory or its files.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/extensions.py |
Adds a self-install safety guard before --force removal/copy to prevent deleting the source directory. |
tests/test_extensions.py |
Adds a regression test verifying self-install is rejected and the installed extension directory remains intact. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 2/2 changed files
- Comments generated: 1
Comment on lines
+1339
to
+1342
| try: | ||
| same_location = source_dir.resolve() == dest_dir.resolve() | ||
| except OSError: | ||
| same_location = False |
Collaborator
|
Please address Copilot feedback |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
specify extension add <path> --dev --forcepermanently deleted the extension directory without registering it when the source path resolved to the extension's own install location(
.specify/extensions/<id>).With
--force,install_from_directory()removed the existing installation (the source) and thenshutil.copytree()tried to copy from the now-deleted directory, destroying itand crashing.
Add a guard that fails fast with a clear ValidationError when the resolved source path equals the install destination, before any destructive operation runs. Includes a regression
test asserting the directory and its contents survive.
Description
When the source path passed to
extension add --dev --forceresolves to the extension's own install destination, the old flow removed the source and then copied from a directorythat no longer existed — destroying the extension with no registration. This PR adds an early
source == destinationcheck inExtensionManager.install_from_directory()(
src/specify_cli/extensions.py) that raises a clearValidationErrorbefore any removal/copy happens, so the directory is never touched on the error path.Testing
uv run specify --helpuv sync && uv run pytestManual end-to-end on macOS/zsh:
extension add .specify/extensions/<id> --dev --force(self-install) — with fixextension.yml+ commands left intactFileNotFoundError, directory destroyedextension add /other/path --dev --force(different location)Added regression test
test_install_from_install_dir_is_rejected_without_data_loss; full suitetests/test_extensions.py→ 283 passed.AI Disclosure
Developed with Claude Code (AI). The AI located the root cause, wrote the guard and the regression test. I reviewed every change, reproduced the original data-loss bug locally, and
verified the fix end-to-end myself before submitting.