Skip to content

[Bug]: extension update removes extension skills of non-default integrations #5002

Open

Description

@PascalThuet

Bug Description

In a project with two integrations installed, specify extension update deletes the extension's skills from the integration that is not the default one. The registry then lists commands for the default integration only, while integration.json still lists both integrations as installed.

The update re-registers commands through _active_command_registration_scope(), which restricts registration to the active agent (only_agent=active_agent in src/specify_cli/extensions/__init__.py). The files previously registered for the other integration are removed and not re-created.

Steps to Reproduce

  1. Create a project with Spec Kit 0.12.4, Claude as default and Codex as a second integration, then add the git extension:
    git init -q p1
    cd p1
    uvx --from git+https://github.com/github/spec-kit.git@v0.12.4 specify init --here --force --integration claude --script sh --ignore-agent-tools
    uvx --from git+https://github.com/github/spec-kit.git@v0.12.4 specify integration install codex --script sh
    uvx --from git+https://github.com/github/spec-kit.git@v0.12.4 specify extension add git
    Both .claude/skills/speckit-git-feature/ and .agents/skills/speckit-git-feature/ exist.
  2. Upgrade both integrations with 1.2.1:
    uvx --from git+https://github.com/github/spec-kit.git@v1.2.1 specify integration upgrade claude
    uvx --from git+https://github.com/github/spec-kit.git@v1.2.1 specify integration upgrade codex
    Both skill directories still exist.
  3. Update the git extension (1.0.0 to 1.0.1) and answer y:
    uvx --from git+https://github.com/github/spec-kit.git@v1.2.1 specify extension update git
  4. List .agents/skills/speckit-git-*.

Expected Behavior

The extension stays registered for every installed integration, as it was before the update: .agents/skills/speckit-git-* are refreshed, not removed.

Actual Behavior

.claude/skills/speckit-git-* are refreshed and .agents/skills/speckit-git-* are deleted. The registry entry for git keeps registered_commands for claude only. In our own repository, updating agent-context (1.0.0 to 1.0.2) removed .agents/skills/speckit-agent-context-update/ the same way.

Running specify integration use codex and then specify integration use claude brings the Codex skills back, which is the workaround we use.

Specify CLI Version

1.2.1

AI Agent

Claude Code

Operating System

macOS 26 (Darwin 25.6.0, arm64)

Python Version

Python 3.12.7

Error Logs

--- 1.2.1: after integration upgrade claude, then codex
p1/.claude/skills/speckit-git-feature
p1/.agents/skills/speckit-git-feature
  • git: 1.0.0 → 1.0.1
   ✓ Updated to v1.0.1
✓ Successfully updated 1 extension(s)
--- 1.2.1: after extension update git
p1/.claude/skills/speckit-git-feature
(none: p1/.agents/skills/speckit-git-feature)
registry git 1.0.1 registered_commands for ['claude']
installed_integrations ['claude', 'codex']

Additional Context

Related but different: #2948 and #3599 are about which format extension skills get for a non-active agent. Here they are removed altogether.

Found while upgrading a repository that commits both .claude/skills and .agents/skills, so the deletion showed up as a diff.

AI Disclosure

Investigated and drafted with Claude Code (Claude Opus 5.5, human-supervised). The reproduction was run as written and the log above is its unedited output. I reviewed the report before filing.

Activity

  1. github-actions commented on Oct 17, 2026

    @github-actions
    Contributor

    Bug assessment — multi-integration-update: Valid · severity medium


    Bug Assessment: extension update removes non-default integration skills

    Report (summarized)

    The reporter describes a project with Claude as the default integration and Codex installed as a second integration. After specify extension update git on Specify CLI 1.2.1, the extension skill files remain under .claude/skills/, but the corresponding .agents/skills/ files are deleted. The registry then records only the default integration's commands even though both integrations remain installed. Switching to Codex and back to Claude recreates the missing files. The report includes a successful reproduction and identifies _active_command_registration_scope() as the suspected restriction point.

    Symptom

    Updating an installed extension removes its previously generated skill mirrors for non-active integrations instead of refreshing them. Expected behavior is that all extension artifacts belonging to installed integrations remain available after an update; actual behavior leaves only the active/default integration's skills and can create a repository diff that deletes the other integration's files.

    Reproduction

    1. Initialize a project with Claude as the default integration and install Codex as a second integration.
    2. Add an extension such as git, confirming its skills exist under both .claude/skills/ and .agents/skills/.
    3. Upgrade both integrations, then run specify extension update git and confirm the update.
    4. Inspect both skill directories and the extension registry.
    5. Observe that the active/default integration is refreshed, while the non-default integration's skill directory is gone and its registry registration is not retained.

    The supplied report states this was reproduced with Specify 1.2.1, Python 3.12.7, and macOS arm64. The same behavior was observed while updating agent-context in the reporter's repository.

    Suspected Code Paths

    • src/specify_cli/extensions/_command_update_transaction.py:273-319 — update backup enumerates command artifacts from registered_commands; this registry structure is per-agent, so only agents recorded there are backed up.
    • src/specify_cli/extensions/_command_update_transaction.py:358-364 — the update reads the flat registered_skills list and backs up matching skill directories, but those backups are used for rollback, not to preserve mirrors across a successful replacement.
    • src/specify_cli/extensions/_command_update_transaction.py:551-554 — the update removes the old extension before installing the new archive, invoking normal extension artifact cleanup.
    • src/specify_cli/extensions/__init__.py:2226-2260 — _unregister_extension_skills() treats an omitted skills_dir as an unscoped cleanup and scans all configured agent skill directories.
    • src/specify_cli/extensions/__init__.py:1440-1480 — _active_command_registration_scope() resolves one active agent from init-options.json; it does not include other installed integrations in the registration scope.
    • src/specify_cli/extensions/__init__.py:1516-1577 — _register_commands_for_active_agent() passes only_agent=active_agent, so a fresh extension install during the update only registers command-backed artifacts for the active integration.
    • src/specify_cli/extensions/__init__.py:4054-4061 — the extension skill phase is explicitly guarded by if agent_name == active_agent, so the replacement install does not regenerate skills for inactive integrations.
    • tests/specify_cli/extensions/test_command_update_transaction.py:206-264 — existing coverage verifies rollback backup behavior for skill-shaped command files, but not successful updates with multiple installed integrations.
    • tests/specify_cli/extensions/test_command_update.py:652-735 — existing skill-update coverage focuses on restoring the active Claude skills after a failed update, not preserving an inactive integration after a successful update.

    Root Cause Hypothesis

    Confidence: high. A successful update is implemented as remove-then-install. The remove path invokes unscoped extension-skill cleanup, which can remove marker-owned skill mirrors in every configured agent directory. The subsequent install path intentionally registers only the active agent (only_agent=active_agent) and runs skill generation only when agent_name == active_agent. Therefore, any non-active mirror removed during step 7 is not recreated. The flat registered_skills metadata and per-agent registered_commands metadata do not provide a successful-update path that preserves or rehydrates inactive integration artifacts.

    Proposed Remediation

    Preferred: Make extension update preserve the complete set of artifacts that existed before replacement, including marker-owned skill mirrors for every installed integration. Before removal, capture each owned skill directory together with its owning/configured agent root (and all registered command artifacts). After installing the new extension, re-render or restore the replacement for every previously active integration, while keeping the normal active-integration behavior for ordinary extension add. Update registry metadata so all surviving per-agent command registrations and flat skill ownership remain accurate. The cleanup should remain ownership-verified and path-safe, and should not overwrite user-customized skills.

    A less invasive implementation could make the update path invoke the existing integration activation/rescaffold operation for each previously registered integration after the active install, but it must avoid changing the project's selected active integration and must handle integrations whose output directories are global or skills-mode specific.

    Files likely to change:

    • src/specify_cli/extensions/_command_update_transaction.py
    • src/specify_cli/extensions/__init__.py
    • tests/specify_cli/extensions/test_command_update.py
    • tests/specify_cli/extensions/test_command_update_transaction.py
    • Potentially integration-state/installer code if the fix reuses a multi-integration rescaffold helper.

    Tests to add or update:

    • Successful extension update with Claude active and Codex installed, asserting both skill trees exist afterward and the registry retains both integrations' registrations.
    • The same scenario with command-backed integrations and with skills-mode integrations, including a global-output integration such as Hermes where applicable.
    • A non-active integration with a user-customized or unrelated skill of the same name, asserting it is not overwritten or deleted.
    • Failure/rollback coverage asserting all pre-update integration artifacts and metadata are restored.
    • A single-active-integration update regression confirming existing behavior and registry cleanup remain unchanged.

    Risks & Considerations

    • Re-registering inactive integrations may alter generated file contents when the new extension changes commands; that is intended, but user-owned/customized files must continue to be protected by ownership markers.
    • Integration output roots can be project-local or home-scoped, so the fix must retain existing containment and symlink safety checks.
    • Registry metadata is currently asymmetric: commands are keyed by agent while registered_skills is flat. A fix should not silently orphan files or drop metadata needed by later removal.
    • Running additional integration registration during update may increase update time and can expose failures from an integration not selected as active; failures should be explicit and transactional rather than silently producing a partial update.

    Open Questions

    • [NEEDS CLARIFICATION: Should an update refresh every integration that has a currently existing, marker-owned mirror, or every integration listed in integration.json even if its artifacts were previously removed manually?]
    • [NEEDS CLARIFICATION: For integrations installed in command mode rather than skills mode, should update preserve only existing artifacts or also recreate missing artifacts for all installed integrations?]

    Generated by 🐛 Assess Bug from Labeled Issue for #5002 · copilot · gpt52codex · 3.03 AIC · ⌖ 0.604 AIC · ⊞ 22.3K · ◷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions