* Add modular extension system for Spec Kit 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> * Update Jira extension to v2.1.0 in catalog Adds 2-level mode support (Epic → Stories only). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Address PR review feedback - 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> * Add comprehensive organization catalog customization docs - 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> * Fix test assertions for extension system data structures - 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> * Address Copilot review feedback - 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> * Add catalog.example.json as reference for organizations 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> * Address remaining Copilot security and logic review feedback - 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> * Ignoring linter for extensions directory --------- Co-authored-by: iamaeroplane <michal.bachorik@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Manfred Riem <manfred.riem@microsoft.com>
13 KiB
Extension Development Guide
A guide for creating Spec Kit extensions.
Quick Start
1. Create Extension Directory
mkdir my-extension
cd my-extension
2. Create extension.yml Manifest
schema_version: "1.0"
extension:
id: "my-ext" # Lowercase, alphanumeric + hyphens only
name: "My Extension"
version: "1.0.0" # Semantic versioning
description: "My custom extension"
author: "Your Name"
repository: "https://github.com/you/spec-kit-my-ext"
license: "MIT"
requires:
speckit_version: ">=0.1.0" # Minimum spec-kit version
tools: # Optional: External tools required
- name: "my-tool"
required: true
version: ">=1.0.0"
commands: # Optional: Core commands needed
- "speckit.tasks"
provides:
commands:
- name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd}
file: "commands/hello.md"
description: "Say hello"
aliases: ["speckit.hello"] # Optional aliases
config: # Optional: Config files
- name: "my-ext-config.yml"
template: "my-ext-config.template.yml"
description: "Extension configuration"
required: false
hooks: # Optional: Integration hooks
after_tasks:
command: "speckit.my-ext.hello"
optional: true
prompt: "Run hello command?"
tags: # Optional: For catalog search
- "example"
- "utility"
3. Create Commands Directory
mkdir commands
4. Create Command File
File: commands/hello.md
---
description: "Say hello command"
tools: # Optional: AI tools this command uses
- 'some-tool/function'
scripts: # Optional: Helper scripts
sh: ../../scripts/bash/helper.sh
ps: ../../scripts/powershell/helper.ps1
---
# Hello Command
This command says hello!
## User Input
$ARGUMENTS
## Steps
1. Greet the user
2. Show extension is working
```bash
echo "Hello from my extension!"
echo "Arguments: $ARGUMENTS"
Extension Configuration
Load extension config from .specify/extensions/my-ext/my-ext-config.yml.
5. Test Locally
cd /path/to/spec-kit-project
specify extension add --dev /path/to/my-extension
6. Verify Installation
specify extension list
# Should show:
# ✓ My Extension (v1.0.0)
# My custom extension
# Commands: 1 | Hooks: 1 | Status: Enabled
7. Test Command
If using Claude:
claude
> /speckit.my-ext.hello world
The command will be available in .claude/commands/speckit.my-ext.hello.md.
Manifest Schema Reference
Required Fields
schema_version
Extension manifest schema version. Currently: "1.0"
extension
Extension metadata block.
Required sub-fields:
id: Extension identifier (lowercase, alphanumeric, hyphens)name: Human-readable nameversion: Semantic version (e.g., "1.0.0")description: Short description
Optional sub-fields:
author: Extension authorrepository: Source code URLlicense: SPDX license identifierhomepage: Extension homepage URL
requires
Compatibility requirements.
Required sub-fields:
speckit_version: Semantic version specifier (e.g., ">=0.1.0,<2.0.0")
Optional sub-fields:
tools: External tools required (array of tool objects)commands: Core spec-kit commands needed (array of command names)scripts: Core scripts required (array of script names)
provides
What the extension provides.
Required sub-fields:
commands: Array of command objects (must have at least one)
Command object:
name: Command name (must matchspeckit.{ext-id}.{command})file: Path to command file (relative to extension root)description: Command description (optional)aliases: Alternative command names (optional, array)
Optional Fields
hooks
Integration hooks for automatic execution.
Available hook points:
after_tasks: After/speckit.taskscompletesafter_implement: After/speckit.implementcompletes (future)
Hook object:
command: Command to execute (must be inprovides.commands)optional: If true, prompt user before executingprompt: Prompt text for optional hooksdescription: Hook descriptioncondition: Execution condition (future)
tags
Array of tags for catalog discovery.
defaults
Default extension configuration values.
config_schema
JSON Schema for validating extension configuration.
Command File Format
Frontmatter (YAML)
---
description: "Command description" # Required
tools: # Optional
- 'tool-name/function'
scripts: # Optional
sh: ../../scripts/bash/helper.sh
ps: ../../scripts/powershell/helper.ps1
---
Body (Markdown)
Use standard Markdown with special placeholders:
$ARGUMENTS: User-provided arguments{SCRIPT}: Replaced with script path during registration
Example:
## Steps
1. Parse arguments
2. Execute logic
```bash
args="$ARGUMENTS"
echo "Running with args: $args"
```
Script Path Rewriting
Extension commands use relative paths that get rewritten during registration:
In extension:
scripts:
sh: ../../scripts/bash/helper.sh
After registration:
scripts:
sh: .specify/scripts/bash/helper.sh
This allows scripts to reference core spec-kit scripts.
Configuration Files
Config Template
File: my-ext-config.template.yml
# My Extension Configuration
# Copy this to my-ext-config.yml and customize
# Example configuration
api:
endpoint: "https://api.example.com"
timeout: 30
features:
feature_a: true
feature_b: false
credentials:
# DO NOT commit credentials!
# Use environment variables instead
api_key: "${MY_EXT_API_KEY}"
Config Loading
In your command, load config with layered precedence:
- Extension defaults (
extension.yml→defaults) - Project config (
.specify/extensions/my-ext/my-ext-config.yml) - Local overrides (
.specify/extensions/my-ext/my-ext-config.local.yml- gitignored) - Environment variables (
SPECKIT_MY_EXT_*)
Example loading script:
#!/usr/bin/env bash
EXT_DIR=".specify/extensions/my-ext"
# Load and merge config
config=$(yq eval '.' "$EXT_DIR/my-ext-config.yml" -o=json)
# Apply env overrides
if [ -n "${SPECKIT_MY_EXT_API_KEY:-}" ]; then
config=$(echo "$config" | jq ".api.api_key = \"$SPECKIT_MY_EXT_API_KEY\"")
fi
echo "$config"
Validation Rules
Extension ID
- Pattern:
^[a-z0-9-]+$ - Valid:
my-ext,tool-123,awesome-plugin - Invalid:
MyExt(uppercase),my_ext(underscore),my ext(space)
Extension Version
- Format: Semantic versioning (MAJOR.MINOR.PATCH)
- Valid:
1.0.0,0.1.0,2.5.3 - Invalid:
1.0,v1.0.0,1.0.0-beta
Command Name
- Pattern:
^speckit\.[a-z0-9-]+\.[a-z0-9-]+$ - Valid:
speckit.my-ext.hello,speckit.tool.cmd - Invalid:
my-ext.hello(missing prefix),speckit.hello(no extension namespace)
Command File Path
- Must be relative to extension root
- Valid:
commands/hello.md,commands/subdir/cmd.md - Invalid:
/absolute/path.md,../outside.md
Testing Extensions
Manual Testing
-
Create test extension
-
Install locally:
specify extension add --dev /path/to/extension -
Verify installation:
specify extension list -
Test commands with your AI agent
-
Check command registration:
ls .claude/commands/speckit.my-ext.* -
Remove extension:
specify extension remove my-ext
Automated Testing
Create tests for your extension:
# tests/test_my_extension.py
import pytest
from pathlib import Path
from specify_cli.extensions import ExtensionManifest
def test_manifest_valid():
"""Test extension manifest is valid."""
manifest = ExtensionManifest(Path("extension.yml"))
assert manifest.id == "my-ext"
assert len(manifest.commands) >= 1
def test_command_files_exist():
"""Test all command files exist."""
manifest = ExtensionManifest(Path("extension.yml"))
for cmd in manifest.commands:
cmd_file = Path(cmd["file"])
assert cmd_file.exists(), f"Command file not found: {cmd_file}"
Distribution
Option 1: GitHub Repository
-
Create repository:
spec-kit-my-ext -
Add files:
spec-kit-my-ext/ ├── extension.yml ├── commands/ ├── scripts/ ├── docs/ ├── README.md ├── LICENSE └── CHANGELOG.md -
Create release: Tag with version (e.g.,
v1.0.0) -
Install from repo:
git clone https://github.com/you/spec-kit-my-ext specify extension add --dev spec-kit-my-ext/
Option 2: ZIP Archive (Future)
Create ZIP archive and host on GitHub Releases:
zip -r spec-kit-my-ext-1.0.0.zip extension.yml commands/ scripts/ docs/
Users install with:
specify extension add --from https://github.com/.../spec-kit-my-ext-1.0.0.zip
Option 3: Extension Catalog (Future)
Submit to official catalog:
-
Fork spec-kit repository
-
Add entry to
extensions/catalog.json -
Create PR
-
After merge, users can install with:
specify extension add my-ext # No URL needed!
Best Practices
Naming Conventions
- Extension ID: Use descriptive, hyphenated names (
jira-integration, notji) - Commands: Use verb-noun pattern (
create-issue,sync-status) - Config files: Match extension ID (
jira-config.yml)
Documentation
- README.md: Overview, installation, usage
- CHANGELOG.md: Version history
- docs/: Detailed guides
- Command descriptions: Clear, concise
Versioning
- Follow SemVer:
MAJOR.MINOR.PATCH - MAJOR: Breaking changes
- MINOR: New features
- PATCH: Bug fixes
Security
- Never commit secrets: Use environment variables
- Validate input: Sanitize user arguments
- Document permissions: What files/APIs are accessed
Compatibility
- Specify version range: Don't require exact version
- Test with multiple versions: Ensure compatibility
- Graceful degradation: Handle missing features
Example Extensions
Minimal Extension
Smallest possible extension:
# extension.yml
schema_version: "1.0"
extension:
id: "minimal"
name: "Minimal Extension"
version: "1.0.0"
description: "Minimal example"
requires:
speckit_version: ">=0.1.0"
provides:
commands:
- name: "speckit.minimal.hello"
file: "commands/hello.md"
<!-- commands/hello.md -->
---
description: "Hello command"
---
# Hello World
```bash
echo "Hello, $ARGUMENTS!"
```
Extension with Config
Extension using configuration:
# extension.yml
# ... metadata ...
provides:
config:
- name: "tool-config.yml"
template: "tool-config.template.yml"
required: true
# tool-config.template.yml
api_endpoint: "https://api.example.com"
timeout: 30
<!-- commands/use-config.md -->
# Use Config
Load config:
```bash
config_file=".specify/extensions/tool/tool-config.yml"
endpoint=$(yq eval '.api_endpoint' "$config_file")
echo "Using endpoint: $endpoint"
```
Extension with Hooks
Extension that runs automatically:
# extension.yml
hooks:
after_tasks:
command: "speckit.auto.analyze"
optional: false # Always run
description: "Analyze tasks after generation"
Troubleshooting
Extension won't install
Error: Invalid extension ID
- Fix: Use lowercase, alphanumeric + hyphens only
Error: Extension requires spec-kit >=0.2.0
- Fix: Update spec-kit with
uv tool install specify-cli --force
Error: Command file not found
- Fix: Ensure command files exist at paths specified in manifest
Commands not registered
Symptom: Commands don't appear in AI agent
Check:
-
.claude/commands/directory exists -
Extension installed successfully
-
Commands registered in registry:
cat .specify/extensions/.registry
Fix: Reinstall extension to trigger registration
Config not loading
Check:
- Config file exists:
.specify/extensions/{ext-id}/{ext-id}-config.yml - YAML syntax is valid:
yq eval '.' config.yml - Environment variables set correctly
Getting Help
- Issues: Report bugs at GitHub repository
- Discussions: Ask questions in GitHub Discussions
- Examples: See
spec-kit-jirafor full-featured example (Phase B)
Next Steps
- Create your extension following this guide
- Test locally with
--devflag - Share with community (GitHub, catalog)
- Iterate based on feedback
Happy extending! 🚀