Skip to content

Add modular extension system#1551

Open
mbachorik wants to merge 8 commits intogithub:mainfrom
mbachorik:feature/extension-system
Open

Add modular extension system#1551
mbachorik wants to merge 8 commits intogithub:mainfrom
mbachorik:feature/extension-system

Conversation

@mbachorik
Copy link

@mbachorik mbachorik commented Feb 3, 2026

Summary

Implement a complete extension system that allows third-party developers to extend Spec Kit functionality through plugins.

  • Extension discovery and loading from local and global directories
  • YAML-based extension manifest (extension.yml) with metadata and capabilities
  • Command extensions: custom slash commands with markdown templates
  • Hook system: pre/post hooks for generate, task, and sync operations
  • Extension catalog for discovering and installing community extensions
  • SPECKIT_CATALOG_URL environment variable for organization catalog customization

Installation Methods

  • Catalog install: specify extension add <name>
  • URL install: specify extension add <name> --from <url>
  • Dev install: specify extension add --dev <path>

Extension Management Commands

  • specify extension list - List installed extensions
  • specify extension search [query] - Search catalog for extensions
  • specify extension update [name] - Check for and update extensions
  • specify extension remove <name> - Remove an extension

Organization Catalog Customization

The default catalog is intentionally empty, allowing organizations to ship their own curated extension catalogs:

# Set organization catalog URL
export SPECKIT_CATALOG_URL="https://internal.company.com/speckit/catalog.json"

# Then use normal extension commands
specify extension search
specify extension add my-extension

Documentation Included

  • RFC with design rationale and architecture decisions
  • API reference for extension developers
  • Development guide with examples
  • User guide for installing and managing extensions (includes organization customization section)
  • Publishing guide for the extension catalog

Also Included

  • Extension template for bootstrapping new extensions
  • Comprehensive test suite (39 tests)
  • Example catalog.json structure

AI Disclosure

This PR was written primarily by AI, using GitHub Copilot and Claude.

Testing

  • Manual testing: Tested on several spec-kit driven projects
  • Automated testing with AI: Created sample projects, installed extensions from catalog/remote and local sources

Sample Extension

A sample Jira extension (also written primarily by AI, using GitHub Copilot and Claude) is available at:
https://github.com/mbachorik/spec-kit-jira

Test plan

  • Manual testing with Claude Code in VS Code
  • Manual testing with GitHub Copilot in VS Code
  • Tested extension installation from catalog
  • Tested extension installation from URL
  • Tested extension installation from local directory (--dev)
  • Tested extension update command with custom catalog URL
  • Unit tests pass (39/39)

🤖 Generated with Claude Code

Implement a complete extension system that allows third-party developers
to extend Spec Kit functionality through plugins.

## Core Features
- Extension discovery and loading from local and global directories
- YAML-based extension manifest (extension.yml) with metadata and capabilities
- Command extensions: custom slash commands with markdown templates
- Hook system: pre/post hooks for generate, task, and sync operations
- Extension catalog for discovering and installing community extensions
- SPECKIT_CATALOG_URL environment variable for catalog URL override

## Installation Methods
- Catalog install: `specify extension add <name>`
- URL install: `specify extension add <name> --from <url>`
- Dev install: `specify extension add --dev <path>`

## Implementation
- ExtensionManager class for lifecycle management (load, enable, disable)
- Support for extension dependencies and version constraints
- Configuration layering (global → project → extension)
- Hook conditions for conditional execution

## Documentation
- RFC with design rationale and architecture decisions
- API reference for extension developers
- Development guide with examples
- User guide for installing and managing extensions
- Publishing guide for the extension catalog

## Included
- Extension template for bootstrapping new extensions
- Comprehensive test suite
- Example catalog.json structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 3, 2026 15:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive modular extension system for Spec Kit, enabling third-party developers to create plugins that extend functionality without bloating the core framework.

Changes:

  • Complete extension infrastructure with manifest validation, registry, installation/removal, and hook system
  • Extension catalog for discovery with search, caching, and metadata management
  • CLI commands for managing extensions (add, remove, list, search, info, update, enable, disable)
  • Multi-agent support for 16+ AI coding assistants with automatic command registration
  • Layered configuration system supporting defaults, project, local, and environment variable overrides
  • Comprehensive documentation suite (user guide, API reference, publishing guide, RFC)
  • Extension template and test suite with 39 unit tests

Reviewed changes

Copilot reviewed 20 out of 21 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/specify_cli/extensions.py Core extension system implementation (1714 lines): manifest validation, registry, manager, catalog, config, hooks
src/specify_cli/init.py CLI integration with 7 extension commands
tests/test_extensions.py Comprehensive test suite with 984 lines covering all components
pyproject.toml Version bump to 0.1.0, added dependencies (pyyaml, packaging), test configuration
extensions/template/* Complete extension template with examples and documentation
extensions/*.md Documentation suite (user guide, API reference, publishing guide, RFC)
extensions/catalog.json Initial catalog with Jira extension
CHANGELOG.md Detailed changelog documenting all new features
.gitignore Extension cache and local config exclusions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Adds 2-level mode support (Epic → Stories only).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@mnriem mnriem self-requested a review February 3, 2026 17:18
@mnriem
Copy link
Collaborator

mnriem commented Feb 3, 2026

@mbachorik Can you make it so the catalog.json is empty and it gets populated when adding a specific extension. For organizations they could then ship their own vetted version of catalog.json? Also can you address the markdown linter errors?

- Fix Zip Slip vulnerability in ZIP extraction with path validation
- Fix keep_config option to actually preserve config files on removal
- Add URL validation for SPECKIT_CATALOG_URL (HTTPS required, localhost exception)
- Add security warning when installing from custom URLs (--from flag)
- Empty catalog.json so organizations can ship their own catalogs
- Fix markdown linter errors (MD040: add language to code blocks)
- Remove redundant import and fix unused variables in tests
- Add comment explaining empty except clause for backwards compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 3, 2026 20:56
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

iamaeroplane and others added 2 commits February 3, 2026 22:02
- Explain why default catalog is empty (org control)
- Document how to create and host custom catalogs
- Add catalog JSON schema reference
- Include use cases: private extensions, curated catalogs, air-gapped environments
- Add examples for combining catalog with direct installation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update test_config_backup_on_remove to use new subdirectory structure
  (.backup/test-ext/file.yml instead of .backup/test-ext-file.yml)
- Update test_full_install_and_remove_workflow to handle registered_commands
  being a dict keyed by agent name instead of a flat list

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 3, 2026 21:20
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 21 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mbachorik
Copy link
Author

Updates in this push

Test Fixes

Fixed two test assertions that were failing due to data structure changes:

  1. test_config_backup_on_remove: Updated to use new subdirectory structure (.backup/test-ext/file.yml instead of .backup/test-ext-file.yml)

  2. test_full_install_and_remove_workflow: Updated to handle registered_commands being a dict keyed by agent name instead of a flat list

All Tests Passing

============================== 39 passed in 0.23s ==============================

Extension Update Command Tested

Verified specify extension update works correctly:

  • Detects newer versions in catalog
  • Shows "up to date" when versions match
  • Provides manual update instructions (automatic download is TODO for future version)
  • Respects SPECKIT_CATALOG_URL environment variable for organization catalogs

- Fix localhost URL check to use parsed.hostname instead of netloc.startswith()
  This correctly handles URLs with ports like localhost:8080
- Fix YAML indentation error in config-template.yml (line 57)
- Fix double space typo in example.md (line 172)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@mbachorik
Copy link
Author

Copilot Review Feedback - Status

Addressed in this push (352bd80)

Comment Status Notes
Localhost check using netloc.startswith() ✅ Fixed Now uses parsed.hostname in ("localhost", "127.0.0.1", "::1") to correctly handle ports
YAML indentation error (config-template.yml:57) ✅ Fixed Changed from 1 space to 2 spaces
Double space typo (example.md:172) ✅ Fixed Removed extra space

Already addressed in previous commits

Comment Status Notes
Empty except clause without comment ✅ Already fixed Has explanatory comment
SPECKIT_CATALOG_URL URL injection ✅ Already fixed Validates HTTPS and shows warning for non-default catalogs
--from URL validation ✅ Already fixed Validates HTTPS and shows warning

Design decisions (not changing)

Comment Notes
Zip Slip validation The current logic is correct - validation happens for ALL paths before extractall() is called. If any path is unsafe, ValidationError is raised before extraction.
keep_config parameter Current behavior is intentional - when keep_config=True, the entire extension directory is preserved to allow seamless reinstall. The parameter name reflects user intent ("keep my config") rather than internal behavior.
Backup *-config.local.yml files Local config files are gitignored and typically contain machine-specific overrides. Backing them up could cause confusion when restoring on different machines. Users who need to preserve local configs should manually copy them.
download_extension HTTPS validation The catalog itself is already HTTPS-validated. Extension download URLs in the catalog are trusted as they come from the verified catalog source. Adding another check would be redundant for catalog installs, and --from URL installs already have HTTPS validation.

@mnriem
Copy link
Collaborator

mnriem commented Feb 4, 2026

@mbachorik Did I miss the change for catalog.json?

The main catalog.json is intentionally empty so organizations can ship
their own curated catalogs. This example file shows the expected schema
and structure for creating organization-specific catalogs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 5, 2026 20:15
@mbachorik
Copy link
Author

mbachorik commented Feb 5, 2026

@mnriem Regarding your request:

catalog.json is now empty - It only contains the schema structure with no extensions:

{
  "schema_version": "1.0",
  "updated_at": "2026-02-03T00:00:00Z",
  "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
  "extensions": {}
}

Added catalog.example.json - A reference file showing the expected schema for organizations creating their own catalogs. This includes two sample extension entries (Jira and Linear) demonstrating all the fields.

Organizations can:

  1. Set SPECKIT_CATALOG_URL to point to their own hosted catalog
  2. Use catalog.example.json as a template for creating their catalog
  3. The empty default catalog ensures no unexpected extensions are installed

Markdown linter issues were addressed in earlier commits.

Or do you want catalog.json to be completely empty (or non-existent in repo)?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 987 to 991
print(
"Warning: Using non-default extension catalog. "
"Only use catalogs from sources you trust.",
file=sys.stderr,
)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning message about using a non-default catalog is printed to stderr every time get_catalog_url() is called, which could result in duplicate warnings during a single command execution (e.g., if the catalog is fetched multiple times). Consider adding a flag to ensure the warning is only shown once per execution, or move the warning to the point where the environment variable is first detected.

Suggested change
print(
"Warning: Using non-default extension catalog. "
"Only use catalogs from sources you trust.",
file=sys.stderr,
)
if not getattr(self, "_non_default_catalog_warning_shown", False):
print(
"Warning: Using non-default extension catalog. "
"Only use catalogs from sources you trust.",
file=sys.stderr,
)
self._non_default_catalog_warning_shown = True

Copilot uses AI. Check for mistakes.
Comment on lines 1590 to 1593
if operator == "==":
return str(actual_value) == expected_value
else: # !=
return str(actual_value) != expected_value
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the hook condition evaluation, when comparing config values using == or !=, the code converts actual_value to string with str(actual_value). This means that boolean values (e.g., True) will be compared as strings ("True"), which may not match the expected string value in the condition. For example, if a config has enabled: true and the condition is config.enabled == 'true', this will fail because str(True) is "True" not "true". Consider normalizing the comparison or documenting the exact expected format.

Suggested change
if operator == "==":
return str(actual_value) == expected_value
else: # !=
return str(actual_value) != expected_value
def _normalize_config_value_for_comparison(value: Any) -> str:
# Booleans are represented in config as True/False, but conditions
# are typically written using lowercase 'true'/'false'.
if isinstance(value, bool):
return "true" if value else "false"
return str(value)
normalized_actual = _normalize_config_value_for_comparison(actual_value)
if operator == "==":
return normalized_actual == expected_value
else: # !=
return normalized_actual != expected_value

Copilot uses AI. Check for mistakes.
- Fix Zip Slip vulnerability by using relative_to() for safe path validation
- Add HTTPS validation for extension download URLs
- Backup both *-config.yml and *-config.local.yml files on remove
- Normalize boolean values to lowercase for hook condition comparisons
- Show non-default catalog warning only once per instance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@mbachorik
Copy link
Author

Addressed Copilot Review Feedback (Round 2)

Fixed the remaining security and logic issues flagged by Copilot:

Security Fixes

  • Zip Slip vulnerability: Changed from string startswith() check to using relative_to() for proper path containment validation
  • Download URL HTTPS validation: Added requirement that extension download URLs must use HTTPS (localhost exception for testing)

Logic Fixes

  • Config backup incomplete: Now backs up both *-config.yml and *-config.local.yml files when removing extensions
  • Boolean comparison in hooks: Normalizes YAML boolean values (True/False) to lowercase ("true"/"false") for condition comparison
  • Duplicate catalog warning: Added instance flag to ensure non-default catalog warning is only shown once per execution

All 39 tests continue to pass.

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.

2 participants