Skip to content

fix: prevent extension self-install from deleting source dir (#2990)#2991

Open
jinaparkdev wants to merge 1 commit into
github:mainfrom
jinaparkdev:fix/2990-extension-self-install-guard
Open

fix: prevent extension self-install from deleting source dir (#2990)#2991
jinaparkdev wants to merge 1 commit into
github:mainfrom
jinaparkdev:fix/2990-extension-self-install-guard

Conversation

@jinaparkdev

@jinaparkdev jinaparkdev commented Jun 16, 2026

Copy link
Copy Markdown

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.

Description

When the source path passed to extension add --dev --force resolves to the extension's own install destination, the old flow removed the source and then copied from a directory
that no longer existed — destroying the extension with no registration. This PR adds an early source == destination check in ExtensionManager.install_from_directory()
(src/specify_cli/extensions.py) that raises a clear ValidationError before any removal/copy happens, so the directory is never touched on the error path.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
  • Tested with a sample project (if applicable)

Manual end-to-end on macOS/zsh:

Scenario Result
extension add .specify/extensions/<id> --dev --force (self-install) — with fix ✅ Clear error, directory + extension.yml + commands left intact
Same command without fix (reverted) Reproduced original bug: FileNotFoundError, directory destroyed
extension add /other/path --dev --force (different location) ✅ Still installs — no regression

Added regression test test_install_from_install_dir_is_rejected_without_data_loss; full suite tests/test_extensions.py → 283 passed.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

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.

…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>
@jinaparkdev jinaparkdev requested a review from mnriem as a code owner June 16, 2026 07:42
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