- RFC: replace FUTURE FEATURE section with full implementation docs, add catalog stack resolution order, config file examples, merge conflict resolution, and install_allowed behavior - EXTENSION-USER-GUIDE.md: add multi-catalog section with CLI examples for catalogs/catalog-add/catalog-remove, update catalog config docs - EXTENSION-API-REFERENCE.md: add CatalogEntry class docs, update ExtensionCatalog docs with new methods and result annotations, add catalog CLI commands (catalogs, catalog add, catalog remove) Also fix extension_catalogs command to correctly show "Using built-in default catalog stack" when config file exists but has empty catalogs Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
56 KiB
RFC: Spec Kit Extension System
Status: Draft Author: Stats Perform Engineering Created: 2026-01-28 Updated: 2026-01-28
Table of Contents
- Summary
- Motivation
- Design Principles
- Architecture Overview
- Extension Manifest Specification
- Extension Lifecycle
- Command Registration
- Configuration Management
- Hook System
- Extension Discovery & Catalog
- CLI Commands
- Compatibility & Versioning
- Security Considerations
- Migration Strategy
- Implementation Phases
- Open Questions
- Appendices
Summary
Introduce an extension system to Spec Kit that allows modular integration with external tools (Jira, Linear, Azure DevOps, etc.) without bloating the core framework. Extensions are self-contained packages installed into .specify/extensions/ with declarative manifests, versioned independently, and discoverable through a central catalog.
Motivation
Current Problems
-
Monolithic Growth: Adding Jira integration to core spec-kit creates:
- Large configuration files affecting all users
- Dependencies on Jira MCP server for everyone
- Merge conflicts as features accumulate
-
Limited Flexibility: Different organizations use different tools:
- GitHub Issues vs Jira vs Linear vs Azure DevOps
- Custom internal tools
- No way to support all without bloat
-
Maintenance Burden: Every integration adds:
- Documentation complexity
- Testing matrix expansion
- Breaking change surface area
-
Community Friction: External contributors can't easily add integrations without core repo PR approval and release cycles.
Goals
- Modularity: Core spec-kit remains lean, extensions are opt-in
- Extensibility: Clear API for building new integrations
- Independence: Extensions version/release separately from core
- Discoverability: Central catalog for finding extensions
- Safety: Validation, compatibility checks, sandboxing
Design Principles
1. Convention Over Configuration
- Standard directory structure (
.specify/extensions/{name}/) - Declarative manifest (
extension.yml) - Predictable command naming (
speckit.{extension}.{command})
2. Fail-Safe Defaults
- Missing extensions gracefully degrade (skip hooks)
- Invalid extensions warn but don't break core functionality
- Extension failures isolated from core operations
3. Backward Compatibility
- Core commands remain unchanged
- Extensions additive only (no core modifications)
- Old projects work without extensions
4. Developer Experience
- Simple installation:
specify extension add jira - Clear error messages for compatibility issues
- Local development mode for testing extensions
5. Security First
- Extensions run in same context as AI agent (trust boundary)
- Manifest validation prevents malicious code
- Verify signatures for official extensions (future)
Architecture Overview
Directory Structure
project/
├── .specify/
│ ├── scripts/ # Core scripts (unchanged)
│ ├── templates/ # Core templates (unchanged)
│ ├── memory/ # Session memory
│ ├── extensions/ # Extensions directory (NEW)
│ │ ├── .registry # Installed extensions metadata (NEW)
│ │ ├── jira/ # Jira extension
│ │ │ ├── extension.yml # Manifest
│ │ │ ├── jira-config.yml # Extension config
│ │ │ ├── commands/ # Command files
│ │ │ ├── scripts/ # Helper scripts
│ │ │ └── docs/ # Documentation
│ │ └── linear/ # Linear extension (example)
│ └── extensions.yml # Project extension configuration (NEW)
└── .gitignore # Ignore local extension configs
Component Diagram
┌─────────────────────────────────────────────────────────┐
│ Spec Kit Core │
│ ┌──────────────────────────────────────────────────┐ │
│ │ CLI (specify) │ │
│ │ - init, check │ │
│ │ - extension add/remove/list/update ← NEW │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Extension Manager ← NEW │ │
│ │ - Discovery, Installation, Validation │ │
│ │ - Command Registration, Hook Execution │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Core Commands │ │
│ │ - /speckit.specify │ │
│ │ - /speckit.tasks │ │
│ │ - /speckit.implement │ │
│ └─────────┬────────────────────────────────────────┘ │
└────────────┼────────────────────────────────────────────┘
│ Hook Points (after_tasks, after_implement)
↓
┌─────────────────────────────────────────────────────────┐
│ Extensions │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Jira Extension │ │
│ │ - /speckit.jira.specstoissues │ │
│ │ - /speckit.jira.discover-fields │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Linear Extension │ │
│ │ - /speckit.linear.sync │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ Calls external tools
↓
┌─────────────────────────────────────────────────────────┐
│ External Tools │
│ - Jira MCP Server │
│ - Linear API │
│ - GitHub API │
└─────────────────────────────────────────────────────────┘
Extension Manifest Specification
Schema: extension.yml
# Extension Manifest Schema v1.0
# All extensions MUST include this file at root
# Schema version for compatibility
schema_version: "1.0"
# Extension metadata (REQUIRED)
extension:
id: "jira" # Unique identifier (lowercase, alphanumeric, hyphens)
name: "Jira Integration" # Human-readable name
version: "1.0.0" # Semantic version
description: "Create Jira Epics, Stories, and Issues from spec-kit artifacts"
author: "Stats Perform" # Author/organization
repository: "https://github.com/statsperform/spec-kit-jira"
license: "MIT" # SPDX license identifier
homepage: "https://github.com/statsperform/spec-kit-jira/blob/main/README.md"
# Compatibility requirements (REQUIRED)
requires:
# Spec-kit version (semantic version range)
speckit_version: ">=0.1.0,<2.0.0"
# External tools required by extension
tools:
- name: "jira-mcp-server"
required: true
version: ">=1.0.0" # Optional: version constraint
description: "Jira MCP server for API access"
install_url: "https://github.com/your-org/jira-mcp-server"
check_command: "jira --version" # Optional: CLI command to verify
# Core spec-kit commands this extension depends on
commands:
- "speckit.tasks" # Extension needs tasks command
# Core scripts required
scripts:
- "check-prerequisites.sh"
# What this extension provides (REQUIRED)
provides:
# Commands added to AI agent
commands:
- name: "speckit.jira.specstoissues"
file: "commands/specstoissues.md"
description: "Create Jira hierarchy from spec and tasks"
aliases: ["speckit.specstoissues"] # Alternate names
- name: "speckit.jira.discover-fields"
file: "commands/discover-fields.md"
description: "Discover Jira custom fields for configuration"
- name: "speckit.jira.sync-status"
file: "commands/sync-status.md"
description: "Sync task completion status to Jira"
# Configuration files
config:
- name: "jira-config.yml"
template: "jira-config.template.yml"
description: "Jira integration configuration"
required: true # User must configure before use
# Helper scripts
scripts:
- name: "parse-jira-config.sh"
file: "scripts/parse-jira-config.sh"
description: "Parse jira-config.yml to JSON"
executable: true # Make executable on install
# Extension configuration defaults (OPTIONAL)
defaults:
project:
key: null # No default, user must configure
hierarchy:
issue_type: "subtask"
update_behavior:
mode: "update"
sync_completion: true
# Configuration schema for validation (OPTIONAL)
config_schema:
type: "object"
required: ["project"]
properties:
project:
type: "object"
required: ["key"]
properties:
key:
type: "string"
pattern: "^[A-Z]{2,10}$"
description: "Jira project key (e.g., MSATS)"
# Integration hooks (OPTIONAL)
hooks:
# Hook fired after /speckit.tasks completes
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
prompt: "Create Jira issues from tasks?"
description: "Automatically create Jira hierarchy after task generation"
# Hook fired after /speckit.implement completes
after_implement:
command: "speckit.jira.sync-status"
optional: true
prompt: "Sync completion status to Jira?"
# Tags for discovery (OPTIONAL)
tags:
- "issue-tracking"
- "jira"
- "atlassian"
- "project-management"
# Changelog URL (OPTIONAL)
changelog: "https://github.com/statsperform/spec-kit-jira/blob/main/CHANGELOG.md"
# Support information (OPTIONAL)
support:
documentation: "https://github.com/statsperform/spec-kit-jira/blob/main/docs/"
issues: "https://github.com/statsperform/spec-kit-jira/issues"
discussions: "https://github.com/statsperform/spec-kit-jira/discussions"
email: "support@statsperform.com"
Validation Rules
- MUST have
schema_version,extension,requires,provides - MUST follow semantic versioning for
version - MUST have unique
id(no conflicts with other extensions) - MUST declare all external tool dependencies
- SHOULD include
config_schemaif extension uses config - SHOULD include
supportinformation - Command
filepaths MUST be relative to extension root - Hook
commandnames MUST match a command inprovides.commands
Extension Lifecycle
1. Discovery
specify extension search jira
# Searches catalog for extensions matching "jira"
Process:
- Fetch extension catalog from GitHub
- Filter by search term (name, tags, description)
- Display results with metadata
2. Installation
specify extension add jira
Process:
- Resolve: Look up extension in catalog
- Download: Fetch extension package (ZIP from GitHub release)
- Validate: Check manifest schema, compatibility
- Extract: Unpack to
.specify/extensions/jira/ - Configure: Copy config templates
- Register: Add commands to AI agent config
- Record: Update
.specify/extensions/.registry
Registry Format (.specify/extensions/.registry):
{
"schema_version": "1.0",
"extensions": {
"jira": {
"version": "1.0.0",
"installed_at": "2026-01-28T14:30:00Z",
"source": "catalog",
"manifest_hash": "sha256:abc123...",
"enabled": true
}
}
}
3. Configuration
# User edits extension config
vim .specify/extensions/jira/jira-config.yml
Config discovery order:
- Extension defaults (
extension.yml→defaults) - Project config (
jira-config.yml) - Local overrides (
jira-config.local.yml- gitignored) - Environment variables (
SPECKIT_JIRA_*)
4. Usage
claude
> /speckit.jira.specstoissues
Command resolution:
- AI agent finds command in
.claude/commands/speckit.jira.specstoissues.md - Command file references extension scripts/config
- Extension executes with full context
5. Update
specify extension update jira
Process:
- Check catalog for newer version
- Download new version
- Validate compatibility
- Back up current config
- Extract new version (preserve config)
- Re-register commands
- Update registry
6. Removal
specify extension remove jira
Process:
- Confirm with user (show what will be removed)
- Unregister commands from AI agent
- Remove from
.specify/extensions/jira/ - Update registry
- Optionally preserve config for reinstall
Command Registration
Per-Agent Registration
Extensions provide universal command format (Markdown-based), and CLI converts to agent-specific format during registration.
Universal Command Format
Location: Extension's commands/specstoissues.md
---
# Universal metadata (parsed by all agents)
description: "Create Jira hierarchy from spec and tasks"
tools:
- 'jira-mcp-server/epic_create'
- 'jira-mcp-server/story_create'
scripts:
sh: ../../scripts/bash/check-prerequisites.sh --json
ps: ../../scripts/powershell/check-prerequisites.ps1 -Json
---
# Command implementation
## User Input
$ARGUMENTS
## Steps
1. Load jira-config.yml
2. Parse spec.md and tasks.md
3. Create Jira items
Claude Code Registration
Output: .claude/commands/speckit.jira.specstoissues.md
---
description: "Create Jira hierarchy from spec and tasks"
tools:
- 'jira-mcp-server/epic_create'
- 'jira-mcp-server/story_create'
scripts:
sh: .specify/scripts/bash/check-prerequisites.sh --json
ps: .specify/scripts/powershell/check-prerequisites.ps1 -Json
---
# Command implementation (copied from extension)
## User Input
$ARGUMENTS
## Steps
1. Load jira-config.yml from .specify/extensions/jira/
2. Parse spec.md and tasks.md
3. Create Jira items
Transformation:
- Copy frontmatter with adjustments
- Rewrite script paths (relative to repo root)
- Add extension context (config location)
Gemini CLI Registration
Output: .gemini/commands/speckit.jira.specstoissues.toml
[command]
name = "speckit.jira.specstoissues"
description = "Create Jira hierarchy from spec and tasks"
[command.tools]
tools = [
"jira-mcp-server/epic_create",
"jira-mcp-server/story_create"
]
[command.script]
sh = ".specify/scripts/bash/check-prerequisites.sh --json"
ps = ".specify/scripts/powershell/check-prerequisites.ps1 -Json"
[command.template]
content = """
# Command implementation
## User Input
{{args}}
## Steps
1. Load jira-config.yml from .specify/extensions/jira/
2. Parse spec.md and tasks.md
3. Create Jira items
"""
Transformation:
- Convert Markdown frontmatter to TOML
- Convert
$ARGUMENTSto{{args}} - Rewrite script paths
Registration Code
Location: src/specify_cli/extensions.py
def register_extension_commands(
project_path: Path,
ai_assistant: str,
manifest: dict
) -> None:
"""Register extension commands with AI agent."""
agent_config = AGENT_CONFIG.get(ai_assistant)
if not agent_config:
console.print(f"[yellow]Unknown agent: {ai_assistant}[/yellow]")
return
ext_id = manifest['extension']['id']
ext_dir = project_path / ".specify" / "extensions" / ext_id
agent_commands_dir = project_path / agent_config['folder'].rstrip('/') / "commands"
agent_commands_dir.mkdir(parents=True, exist_ok=True)
for cmd_info in manifest['provides']['commands']:
cmd_name = cmd_info['name']
source_file = ext_dir / cmd_info['file']
if not source_file.exists():
console.print(f"[red]Command file not found:[/red] {cmd_info['file']}")
continue
# Convert to agent-specific format
if ai_assistant == "claude":
dest_file = agent_commands_dir / f"{cmd_name}.md"
convert_to_claude(source_file, dest_file, ext_dir)
elif ai_assistant == "gemini":
dest_file = agent_commands_dir / f"{cmd_name}.toml"
convert_to_gemini(source_file, dest_file, ext_dir)
elif ai_assistant == "copilot":
dest_file = agent_commands_dir / f"{cmd_name}.md"
convert_to_copilot(source_file, dest_file, ext_dir)
# ... other agents
console.print(f" ✓ Registered: {cmd_name}")
def convert_to_claude(
source: Path,
dest: Path,
ext_dir: Path
) -> None:
"""Convert universal command to Claude format."""
# Parse universal command
content = source.read_text()
frontmatter, body = parse_frontmatter(content)
# Adjust script paths (relative to repo root)
if 'scripts' in frontmatter:
for key in frontmatter['scripts']:
frontmatter['scripts'][key] = adjust_path_for_repo_root(
frontmatter['scripts'][key]
)
# Inject extension context
body = inject_extension_context(body, ext_dir)
# Write Claude command
dest.write_text(render_frontmatter(frontmatter) + "\n" + body)
Configuration Management
Configuration File Hierarchy
# .specify/extensions/jira/jira-config.yml (Project config)
project:
key: "MSATS"
hierarchy:
issue_type: "subtask"
defaults:
epic:
labels: ["spec-driven", "typescript"]
# .specify/extensions/jira/jira-config.local.yml (Local overrides - gitignored)
project:
key: "MYTEST" # Override for local testing
# Environment variables (highest precedence)
export SPECKIT_JIRA_PROJECT_KEY="DEVTEST"
Config Loading Function
Location: Extension command (e.g., commands/specstoissues.md)
## Load Configuration
1. Run helper script to load and merge config:
```bash
config_json=$(bash .specify/extensions/jira/scripts/parse-jira-config.sh)
echo "$config_json"
```
1. Parse JSON and use in subsequent steps
Script: .specify/extensions/jira/scripts/parse-jira-config.sh
#!/usr/bin/env bash
set -euo pipefail
EXT_DIR=".specify/extensions/jira"
CONFIG_FILE="$EXT_DIR/jira-config.yml"
LOCAL_CONFIG="$EXT_DIR/jira-config.local.yml"
# Start with defaults from extension.yml
defaults=$(yq eval '.defaults' "$EXT_DIR/extension.yml" -o=json)
# Merge project config
if [ -f "$CONFIG_FILE" ]; then
project_config=$(yq eval '.' "$CONFIG_FILE" -o=json)
defaults=$(echo "$defaults $project_config" | jq -s '.[0] * .[1]')
fi
# Merge local config
if [ -f "$LOCAL_CONFIG" ]; then
local_config=$(yq eval '.' "$LOCAL_CONFIG" -o=json)
defaults=$(echo "$defaults $local_config" | jq -s '.[0] * .[1]')
fi
# Apply environment variable overrides
if [ -n "${SPECKIT_JIRA_PROJECT_KEY:-}" ]; then
defaults=$(echo "$defaults" | jq ".project.key = \"$SPECKIT_JIRA_PROJECT_KEY\"")
fi
# Output merged config as JSON
echo "$defaults"
Config Validation
In command file:
## Validate Configuration
1. Load config (from previous step)
2. Validate against schema from extension.yml:
```python
import jsonschema
schema = load_yaml(".specify/extensions/jira/extension.yml")['config_schema']
config = json.loads(config_json)
try:
jsonschema.validate(config, schema)
except jsonschema.ValidationError as e:
print(f"❌ Invalid jira-config.yml: {e.message}")
print(f" Path: {'.'.join(str(p) for p in e.path)}")
exit(1)
```
1. Proceed with validated config
Hook System
Hook Definition
In extension.yml:
hooks:
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
prompt: "Create Jira issues from tasks?"
description: "Automatically create Jira hierarchy"
condition: "config.project.key is set"
Hook Registration
During extension installation, record hooks in project config:
File: .specify/extensions.yml (project-level extension config)
# Extensions installed in this project
installed:
- jira
- linear
# Global extension settings
settings:
auto_execute_hooks: true # Prompt for optional hooks after commands
# Hook configuration
hooks:
after_tasks:
- extension: jira
command: speckit.jira.specstoissues
enabled: true
optional: true
prompt: "Create Jira issues from tasks?"
after_implement:
- extension: jira
command: speckit.jira.sync-status
enabled: true
optional: true
prompt: "Sync completion status to Jira?"
Hook Execution
In core command (e.g., templates/commands/tasks.md):
Add at end of command:
## Extension Hooks
After task generation completes, check for registered hooks:
```bash
# Check if extensions.yml exists and has after_tasks hooks
if [ -f ".specify/extensions.yml" ]; then
# Parse hooks for after_tasks
hooks=$(yq eval '.hooks.after_tasks[] | select(.enabled == true)' .specify/extensions.yml -o=json)
if [ -n "$hooks" ]; then
echo ""
echo "📦 Extension hooks available:"
# Iterate hooks
echo "$hooks" | jq -c '.' | while read -r hook; do
extension=$(echo "$hook" | jq -r '.extension')
command=$(echo "$hook" | jq -r '.command')
optional=$(echo "$hook" | jq -r '.optional')
prompt_text=$(echo "$hook" | jq -r '.prompt')
if [ "$optional" = "true" ]; then
# Prompt user
echo ""
read -p "$prompt_text (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "▶ Executing: $command"
# Let AI agent execute the command
# (AI agent will see this and execute)
echo "EXECUTE_COMMAND: $command"
fi
else
# Auto-execute mandatory hooks
echo "▶ Executing: $command (required)"
echo "EXECUTE_COMMAND: $command"
fi
done
fi
fi
```
AI Agent Handling:
The AI agent sees EXECUTE_COMMAND: speckit.jira.specstoissues in output and automatically invokes that command.
Alternative: Direct call in agent context (if agent supports it):
# In AI agent's command execution engine
def execute_command_with_hooks(command_name: str, args: str):
# Execute main command
result = execute_command(command_name, args)
# Check for hooks
hooks = load_hooks_for_phase(f"after_{command_name}")
for hook in hooks:
if hook.optional:
if confirm(hook.prompt):
execute_command(hook.command, args)
else:
execute_command(hook.command, args)
return result
Hook Conditions
Extensions can specify conditions for hooks:
hooks:
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
condition: "config.project.key is set and config.enabled == true"
Condition evaluation (in hook executor):
def should_execute_hook(hook: dict, config: dict) -> bool:
"""Evaluate hook condition."""
condition = hook.get('condition')
if not condition:
return True # No condition = always eligible
# Simple expression evaluator
# "config.project.key is set" → check if config['project']['key'] exists
# "config.enabled == true" → check if config['enabled'] is True
return eval_condition(condition, config)
Extension Discovery & Catalog
Dual Catalog System
Spec Kit uses two catalog files with different purposes:
User Catalog (catalog.json)
URL: https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json
- Purpose: Organization's curated catalog of approved extensions
- Default State: Empty by design - users populate with extensions they trust
- Usage: Primary catalog (priority 1,
install_allowed: true) in the default stack - Control: Organizations maintain their own fork/version for their teams
Community Reference Catalog (catalog.community.json)
URL: https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json
- Purpose: Reference catalog of available community-contributed extensions
- Verification: Community extensions may have
verified: falseinitially - Status: Active - open for community contributions
- Submission: Via Pull Request following the Extension Publishing Guide
- Usage: Secondary catalog (priority 2,
install_allowed: false) in the default stack — discovery only
How It Works (default stack):
- Discover:
specify extension searchsearches both catalogs — community extensions appear automatically - Review: Evaluate community extensions for security, quality, and organizational fit
- Curate: Copy approved entries from community catalog to your
catalog.json, or add to.specify/extension-catalogs.ymlwithinstall_allowed: true - Install: Use
specify extension add <name>— only allowed frominstall_allowed: truecatalogs
This approach gives organizations full control over which extensions can be installed while still providing community discoverability out of the box.
Catalog Format
Format (same for both catalogs):
{
"schema_version": "1.0",
"updated_at": "2026-01-28T14:30:00Z",
"extensions": {
"jira": {
"name": "Jira Integration",
"id": "jira",
"description": "Create Jira Epics, Stories, and Issues from spec-kit artifacts",
"author": "Stats Perform",
"version": "1.0.0",
"download_url": "https://github.com/statsperform/spec-kit-jira/releases/download/v1.0.0/spec-kit-jira-1.0.0.zip",
"repository": "https://github.com/statsperform/spec-kit-jira",
"homepage": "https://github.com/statsperform/spec-kit-jira/blob/main/README.md",
"documentation": "https://github.com/statsperform/spec-kit-jira/blob/main/docs/",
"changelog": "https://github.com/statsperform/spec-kit-jira/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0,<2.0.0",
"tools": [
{
"name": "jira-mcp-server",
"version": ">=1.0.0"
}
]
},
"tags": ["issue-tracking", "jira", "atlassian", "project-management"],
"verified": true,
"downloads": 1250,
"stars": 45
},
"linear": {
"name": "Linear Integration",
"id": "linear",
"description": "Sync spec-kit tasks with Linear issues",
"author": "Community",
"version": "0.9.0",
"download_url": "https://github.com/example/spec-kit-linear/releases/download/v0.9.0/spec-kit-linear-0.9.0.zip",
"repository": "https://github.com/example/spec-kit-linear",
"requires": {
"speckit_version": ">=0.1.0"
},
"tags": ["issue-tracking", "linear"],
"verified": false
}
}
}
Catalog Discovery Commands
# List all available extensions
specify extension search
# Search by keyword
specify extension search jira
# Search by tag
specify extension search --tag issue-tracking
# Show extension details
specify extension info jira
Custom Catalogs
Spec Kit supports a catalog stack — an ordered list of catalogs that the CLI merges and searches across. This allows organizations to benefit from org-approved extensions, an internal catalog, and community discovery all at once.
Catalog Stack Resolution
The active catalog stack is resolved in this order (first match wins):
SPECKIT_CATALOG_URLenvironment variable — single catalog replacing all defaults (backward compat)- Project-level
.specify/extension-catalogs.yml— full control for the project - User-level
~/.specify/extension-catalogs.yml— personal defaults - Built-in default stack —
catalog.json(install_allowed: true) +catalog.community.json(install_allowed: false)
Default Built-in Stack
When no config file exists, the CLI uses:
| Priority | Catalog | install_allowed | Purpose |
|---|---|---|---|
| 1 | catalog.json (org-approved) |
true |
Extensions your org approves for installation |
| 2 | catalog.community.json (community) |
false |
Discovery only — browse but not install |
This means specify extension search surfaces community extensions out of the box, while specify extension add is still restricted to org-approved entries.
.specify/extension-catalogs.yml Config File
catalogs:
- name: "org-approved"
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
priority: 1 # Highest — only approved entries can be installed
install_allowed: true
- name: "internal"
url: "https://internal.company.com/spec-kit/catalog.json"
priority: 2
install_allowed: true
- name: "community"
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
priority: 3 # Lowest — discovery only, not installable
install_allowed: false
A user-level equivalent lives at ~/.specify/extension-catalogs.yml. When a project-level config is present, it takes full control and the built-in defaults are not applied.
Catalog CLI Commands
# List active catalogs with name, URL, priority, and install_allowed
specify extension catalogs
# Add a catalog (project-scoped)
specify extension catalog add --name "internal" --install-allowed \
https://internal.company.com/spec-kit/catalog.json
# Add a discovery-only catalog
specify extension catalog add --name "community" \
https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json
# Remove a catalog
specify extension catalog remove internal
# Show which catalog an extension came from
specify extension info jira
# → Source catalog: org-approved
Merge Conflict Resolution
When the same extension id appears in multiple catalogs, the higher-priority (lower priority number) catalog wins. Extensions from lower-priority catalogs with the same id are ignored.
install_allowed: false Behavior
Extensions from discovery-only catalogs are shown in specify extension search results but cannot be installed directly:
⚠ 'linear' is available in the 'community' catalog but installation is not allowed from that catalog.
To enable installation, add 'linear' to an approved catalog (install_allowed: true) in .specify/extension-catalogs.yml.
SPECKIT_CATALOG_URL (Backward Compatibility)
The SPECKIT_CATALOG_URL environment variable still works — it is treated as a single install_allowed: true catalog, replacing both defaults for full backward compatibility:
# Point to your organization's catalog
export SPECKIT_CATALOG_URL="https://internal.company.com/spec-kit/catalog.json"
# All extension commands now use your custom catalog
specify extension search # Uses custom catalog
specify extension add jira # Installs from custom catalog
Requirements:
- URL must use HTTPS (HTTP only allowed for localhost testing)
- Catalog must follow the standard catalog.json schema
- Must be publicly accessible or accessible within your network
Example for testing:
# Test with localhost during development
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
specify extension search
CLI Commands
specify extension Subcommands
specify extension list
List installed extensions in current project.
$ specify extension list
Installed Extensions:
✓ jira (v1.0.0) - Jira Integration
Commands: 3 | Hooks: 2 | Status: Enabled
✓ linear (v0.9.0) - Linear Integration
Commands: 1 | Hooks: 1 | Status: Enabled
Options:
--available: Show available (not installed) extensions from catalog--all: Show both installed and available
specify extension search [QUERY]
Search extension catalog.
$ specify extension search jira
Found 1 extension:
┌─────────────────────────────────────────────────────────┐
│ jira (v1.0.0) ✓ Verified │
│ Jira Integration │
│ │
│ Create Jira Epics, Stories, and Issues from spec-kit │
│ artifacts │
│ │
│ Author: Stats Perform │
│ Tags: issue-tracking, jira, atlassian │
│ Downloads: 1,250 │
│ │
│ Repository: github.com/statsperform/spec-kit-jira │
│ Documentation: github.com/.../docs │
└─────────────────────────────────────────────────────────┘
Install: specify extension add jira
Options:
--tag TAG: Filter by tag--author AUTHOR: Filter by author--verified: Show only verified extensions
specify extension info NAME
Show detailed information about an extension.
$ specify extension info jira
Jira Integration (jira) v1.0.0
Description:
Create Jira Epics, Stories, and Issues from spec-kit artifacts
Author: Stats Perform
License: MIT
Repository: https://github.com/statsperform/spec-kit-jira
Documentation: https://github.com/statsperform/spec-kit-jira/blob/main/docs/
Requirements:
• Spec Kit: >=0.1.0,<2.0.0
• Tools: jira-mcp-server (>=1.0.0)
Provides:
Commands:
• speckit.jira.specstoissues - Create Jira hierarchy from spec and tasks
• speckit.jira.discover-fields - Discover Jira custom fields
• speckit.jira.sync-status - Sync task completion status
Hooks:
• after_tasks - Prompt to create Jira issues
• after_implement - Prompt to sync status
Tags: issue-tracking, jira, atlassian, project-management
Downloads: 1,250 | Stars: 45 | Verified: ✓
Install: specify extension add jira
specify extension add NAME
Install an extension.
$ specify extension add jira
Installing extension: Jira Integration
✓ Downloaded spec-kit-jira-1.0.0.zip (245 KB)
✓ Validated manifest
✓ Checked compatibility (spec-kit 0.1.0 ≥ 0.1.0)
✓ Extracted to .specify/extensions/jira/
✓ Registered 3 commands with claude
✓ Installed config template (jira-config.yml)
⚠ Configuration required:
Edit .specify/extensions/jira/jira-config.yml to set your Jira project key
Extension installed successfully!
Next steps:
1. Configure: vim .specify/extensions/jira/jira-config.yml
2. Discover fields: /speckit.jira.discover-fields
3. Use commands: /speckit.jira.specstoissues
Options:
--from URL: Install from custom URL or Git repo--version VERSION: Install specific version--dev PATH: Install from local path (development mode)--no-register: Skip command registration (manual setup)
specify extension remove NAME
Uninstall an extension.
$ specify extension remove jira
⚠ This will remove:
• 3 commands from AI agent
• Extension directory: .specify/extensions/jira/
• Config file: jira-config.yml (will be backed up)
Continue? (yes/no): yes
✓ Unregistered commands
✓ Backed up config to .specify/extensions/.backup/jira-config.yml
✓ Removed extension directory
✓ Updated registry
Extension removed successfully.
To reinstall: specify extension add jira
Options:
--keep-config: Don't remove config file--force: Skip confirmation
specify extension update [NAME]
Update extension(s) to latest version.
$ specify extension update jira
Checking for updates...
jira: 1.0.0 → 1.1.0 available
Changes in v1.1.0:
• Added support for custom workflows
• Fixed issue with parallel tasks
• Improved error messages
Update? (yes/no): yes
✓ Downloaded spec-kit-jira-1.1.0.zip
✓ Validated manifest
✓ Backed up current version
✓ Extracted new version
✓ Preserved config file
✓ Re-registered commands
Extension updated successfully!
Changelog: https://github.com/statsperform/spec-kit-jira/blob/main/CHANGELOG.md#v110
Options:
--all: Update all extensions--check: Check for updates without installing--force: Force update even if already latest
specify extension enable/disable NAME
Enable or disable an extension without removing it.
$ specify extension disable jira
✓ Disabled extension: jira
• Commands unregistered (but files preserved)
• Hooks will not execute
To re-enable: specify extension enable jira
Compatibility & Versioning
Semantic Versioning
Extensions follow SemVer 2.0.0:
- MAJOR: Breaking changes (command API changes, config schema changes)
- MINOR: New features (new commands, new config options)
- PATCH: Bug fixes (no API changes)
Compatibility Checks
At installation:
def check_compatibility(extension_manifest: dict) -> bool:
"""Check if extension is compatible with current environment."""
requires = extension_manifest['requires']
# 1. Check spec-kit version
current_speckit = get_speckit_version() # e.g., "0.1.5"
required_speckit = requires['speckit_version'] # e.g., ">=0.1.0,<2.0.0"
if not version_satisfies(current_speckit, required_speckit):
raise IncompatibleVersionError(
f"Extension requires spec-kit {required_speckit}, "
f"but {current_speckit} is installed. "
f"Upgrade spec-kit with: uv tool install specify-cli --force"
)
# 2. Check required tools
for tool in requires.get('tools', []):
tool_name = tool['name']
tool_version = tool.get('version')
if tool.get('required', True):
if not check_tool(tool_name):
raise MissingToolError(
f"Extension requires tool: {tool_name}\n"
f"Install from: {tool.get('install_url', 'N/A')}"
)
if tool_version:
installed = get_tool_version(tool_name, tool.get('check_command'))
if not version_satisfies(installed, tool_version):
raise IncompatibleToolVersionError(
f"Extension requires {tool_name} {tool_version}, "
f"but {installed} is installed"
)
# 3. Check required commands
for cmd in requires.get('commands', []):
if not command_exists(cmd):
raise MissingCommandError(
f"Extension requires core command: {cmd}\n"
f"Update spec-kit to latest version"
)
return True
Deprecation Policy
Extension manifest can mark features as deprecated:
provides:
commands:
- name: "speckit.jira.old-command"
file: "commands/old-command.md"
deprecated: true
deprecated_message: "Use speckit.jira.new-command instead"
removal_version: "2.0.0"
At runtime, show warning:
⚠️ Warning: /speckit.jira.old-command is deprecated
Use /speckit.jira.new-command instead
This command will be removed in v2.0.0
Security Considerations
Trust Model
Extensions run with same privileges as AI agent:
- Can execute shell commands
- Can read/write files in project
- Can make network requests
Trust boundary: User must trust extension author.
Verification
Verified Extensions (in catalog):
- Published by known organizations (GitHub, Stats Perform, etc.)
- Code reviewed by spec-kit maintainers
- Marked with ✓ badge in catalog
Community Extensions:
-
Not verified, use at own risk
-
Show warning during installation:
⚠️ This extension is not verified. Review code before installing: https://github.com/... Continue? (yes/no):
Sandboxing (Future)
Phase 2 (not in initial release):
- Extensions declare required permissions in manifest
- CLI enforces permission boundaries
- Example permissions:
filesystem:read,network:external,env:read
# Future extension.yml
permissions:
- "filesystem:read:.specify/extensions/jira/" # Can only read own config
- "filesystem:write:.specify/memory/" # Can write to memory
- "network:external:*.atlassian.net" # Can call Jira API
- "env:read:SPECKIT_JIRA_*" # Can read own env vars
Package Integrity
Future: Sign extension packages with GPG/Sigstore
# catalog.json
"jira": {
"download_url": "...",
"checksum": "sha256:abc123...",
"signature": "https://github.com/.../spec-kit-jira-1.0.0.sig",
"signing_key": "https://github.com/statsperform.gpg"
}
CLI verifies signature before extraction.
Migration Strategy
Backward Compatibility
Goal: Existing spec-kit projects work without changes.
Strategy:
-
Core commands unchanged:
/speckit.tasks,/speckit.implement, etc. remain in core -
Optional extensions: Users opt-in to extensions
-
Gradual migration: Existing
taskstoissuesstays in core, Jira extension is alternative -
Deprecation timeline:
- v0.2.0: Introduce extension system, keep core
taskstoissues - v0.3.0: Mark core
taskstoissuesas "legacy" (still works) - v1.0.0: Consider removing core
taskstoissuesin favor of extension
- v0.2.0: Introduce extension system, keep core
Migration Path for Users
Scenario 1: User has no taskstoissues usage
- No migration needed, extensions are opt-in
Scenario 2: User uses core taskstoissues (GitHub Issues)
- Works as before
- Optional: Migrate to
github-projectsextension for more features
Scenario 3: User wants Jira (new requirement)
specify extension add jira- Configure and use
Scenario 4: User has custom scripts calling taskstoissues
- Scripts still work (core command preserved)
- Migration guide shows how to call extension commands instead
Extension Migration Guide
For extension authors (if core command becomes extension):
# Old (core command)
/speckit.taskstoissues
# New (extension command)
specify extension add github-projects
/speckit.github.taskstoissues
Compatibility shim (if needed):
# extension.yml
provides:
commands:
- name: "speckit.github.taskstoissues"
file: "commands/taskstoissues.md"
aliases: ["speckit.taskstoissues"] # Backward compatibility
AI agent registers both names, so old scripts work.
Implementation Phases
Phase 1: Core Extension System (Week 1-2)
Goal: Basic extension infrastructure
Deliverables:
- Extension manifest schema (
extension.yml) - Extension directory structure
- CLI commands:
specify extension listspecify extension add(from URL)specify extension remove
- Extension registry (
.specify/extensions/.registry) - Command registration (Claude only initially)
- Basic validation (manifest schema, compatibility)
- Documentation (extension development guide)
Testing:
- Unit tests for manifest parsing
- Integration test: Install dummy extension
- Integration test: Register commands with Claude
Phase 2: Jira Extension (Week 3)
Goal: First production extension
Deliverables:
- Create
spec-kit-jirarepository - Port Jira functionality to extension
- Create
jira-config.ymltemplate - Commands:
specstoissues.mddiscover-fields.mdsync-status.md
- Helper scripts
- Documentation (README, configuration guide, examples)
- Release v1.0.0
Testing:
- Test on
eng-msa-tsproject - Verify spec→Epic, phase→Story, task→Issue mapping
- Test configuration loading and validation
- Test custom field application
Phase 3: Extension Catalog (Week 4)
Goal: Discovery and distribution
Deliverables:
- Central catalog (
extensions/catalog.jsonin spec-kit repo) - Catalog fetch and parsing
- CLI commands:
specify extension searchspecify extension info
- Catalog publishing process (GitHub Action)
- Documentation (how to publish extensions)
Testing:
- Test catalog fetch
- Test extension search/filtering
- Test catalog caching
Phase 4: Advanced Features (Week 5-6)
Goal: Hooks, updates, multi-agent support
Deliverables:
- Hook system (
hooksin extension.yml) - Hook registration and execution
- Project extensions config (
.specify/extensions.yml) - CLI commands:
specify extension updatespecify extension enable/disable
- Command registration for multiple agents (Gemini, Copilot)
- Extension update notifications
- Configuration layer resolution (project, local, env)
Testing:
- Test hooks in core commands
- Test extension updates (preserve config)
- Test multi-agent registration
Phase 5: Polish & Documentation (Week 7)
Goal: Production ready
Deliverables:
- Comprehensive documentation:
- User guide (installing/using extensions)
- Extension development guide
- Extension API reference
- Migration guide (core → extension)
- Error messages and validation improvements
- CLI help text updates
- Example extension template (cookiecutter)
- Blog post / announcement
- Video tutorial
Testing:
- End-to-end testing on multiple projects
- Community beta testing
- Performance testing (large projects)
Open Questions
1. Extension Namespace
Question: Should extension commands use namespace prefix?
Options:
- A) Prefixed:
/speckit.jira.specstoissues(explicit, avoids conflicts) - B) Short alias:
/jira.specstoissues(shorter, less verbose) - C) Both: Register both names, prefer prefixed in docs
Recommendation: C (both), prefixed is canonical
2. Config File Location
Question: Where should extension configs live?
Options:
- A) Extension directory:
.specify/extensions/jira/jira-config.yml(encapsulated) - B) Root level:
.specify/jira-config.yml(more visible) - C) Unified:
.specify/extensions.yml(all extension configs in one file)
Recommendation: A (extension directory), cleaner separation
3. Command File Format
Question: Should extensions use universal format or agent-specific?
Options:
- A) Universal Markdown: Extensions write once, CLI converts per-agent
- B) Agent-specific: Extensions provide separate files for each agent
- C) Hybrid: Universal default, agent-specific overrides
Recommendation: A (universal), reduces duplication
4. Hook Execution Model
Question: How should hooks execute?
Options:
- A) AI agent interprets: Core commands output
EXECUTE_COMMAND: name - B) CLI executes: Core commands call
specify extension hook after_tasks - C) Agent built-in: Extension system built into AI agent (Claude SDK)
Recommendation: A initially (simpler), move to C long-term
5. Extension Distribution
Question: How should extensions be packaged?
Options:
- A) ZIP archives: Downloaded from GitHub releases
- B) Git repos: Cloned directly (
git clone) - C) Python packages: Installable via
uv tool install
Recommendation: A (ZIP), simpler for non-Python extensions in future
6. Multi-Version Support
Question: Can multiple versions of same extension coexist?
Options:
- A) Single version: Only one version installed at a time
- B) Multi-version: Side-by-side versions (
.specify/extensions/jira@1.0/,.specify/extensions/jira@2.0/) - C) Per-branch: Different branches use different versions
Recommendation: A initially (simpler), consider B in future if needed
Appendices
Appendix A: Example Extension Structure
Complete structure of spec-kit-jira extension:
spec-kit-jira/
├── README.md # Overview, features, installation
├── LICENSE # MIT license
├── CHANGELOG.md # Version history
├── .gitignore # Ignore local configs
│
├── extension.yml # Extension manifest (required)
├── jira-config.template.yml # Config template
│
├── commands/ # Command files
│ ├── specstoissues.md # Main command
│ ├── discover-fields.md # Helper: Discover custom fields
│ └── sync-status.md # Helper: Sync completion status
│
├── scripts/ # Helper scripts
│ ├── parse-jira-config.sh # Config loader (bash)
│ ├── parse-jira-config.ps1 # Config loader (PowerShell)
│ └── validate-jira-connection.sh # Connection test
│
├── docs/ # Documentation
│ ├── installation.md # Installation guide
│ ├── configuration.md # Configuration reference
│ ├── usage.md # Usage examples
│ ├── troubleshooting.md # Common issues
│ └── examples/
│ ├── eng-msa-ts-config.yml # Real-world config example
│ └── simple-project.yml # Minimal config example
│
├── tests/ # Tests (optional)
│ ├── test-extension.sh # Extension validation
│ └── test-commands.sh # Command execution tests
│
└── .github/ # GitHub integration
└── workflows/
└── release.yml # Automated releases
Appendix B: Extension Development Guide (Outline)
Documentation for creating new extensions:
-
Getting Started
- Prerequisites (tools needed)
- Extension template (cookiecutter)
- Directory structure
-
Extension Manifest
- Schema reference
- Required vs optional fields
- Versioning guidelines
-
Command Development
- Universal command format
- Frontmatter specification
- Template variables
- Script references
-
Configuration
- Config file structure
- Schema validation
- Layered config resolution
- Environment variable overrides
-
Hooks
- Available hook points
- Hook registration
- Conditional execution
- Best practices
-
Testing
- Local development setup
- Testing with
--devflag - Validation checklist
- Integration testing
-
Publishing
- Packaging (ZIP format)
- GitHub releases
- Catalog submission
- Versioning strategy
-
Examples
- Minimal extension
- Extension with hooks
- Extension with configuration
- Extension with multiple commands
Appendix C: Compatibility Matrix
Planned support matrix:
| Extension Feature | Spec Kit Version | AI Agent Support |
|---|---|---|
| Basic commands | 0.2.0+ | Claude, Gemini, Copilot |
| Hooks (after_tasks) | 0.3.0+ | Claude, Gemini |
| Config validation | 0.2.0+ | All |
| Multiple catalogs | 0.4.0+ | All |
| Permissions (sandboxing) | 1.0.0+ | TBD |
Appendix D: Extension Catalog Schema
Full schema for catalog.json:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["schema_version", "updated_at", "extensions"],
"properties": {
"schema_version": {
"type": "string",
"pattern": "^\\d+\\.\\d+$"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"extensions": {
"type": "object",
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "object",
"required": ["name", "id", "version", "download_url", "repository"],
"properties": {
"name": { "type": "string" },
"id": { "type": "string", "pattern": "^[a-z0-9-]+$" },
"description": { "type": "string" },
"author": { "type": "string" },
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
"download_url": { "type": "string", "format": "uri" },
"repository": { "type": "string", "format": "uri" },
"homepage": { "type": "string", "format": "uri" },
"documentation": { "type": "string", "format": "uri" },
"changelog": { "type": "string", "format": "uri" },
"license": { "type": "string" },
"requires": {
"type": "object",
"properties": {
"speckit_version": { "type": "string" },
"tools": {
"type": "array",
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"version": { "type": "string" }
}
}
}
}
},
"tags": {
"type": "array",
"items": { "type": "string" }
},
"verified": { "type": "boolean" },
"downloads": { "type": "integer" },
"stars": { "type": "integer" },
"checksum": { "type": "string" }
}
}
}
}
}
}
Summary & Next Steps
This RFC proposes a comprehensive extension system for Spec Kit that:
- Keeps core lean while enabling unlimited integrations
- Supports multiple agents (Claude, Gemini, Copilot, etc.)
- Provides clear extension API for community contributions
- Enables independent versioning of extensions and core
- Includes safety mechanisms (validation, compatibility checks)
Immediate Next Steps
- Review this RFC with stakeholders
- Gather feedback on open questions
- Refine design based on feedback
- Proceed to Phase A: Implement core extension system
- Then Phase B: Build Jira extension as proof-of-concept
Questions for Discussion
- Does the extension architecture meet your needs for Jira integration?
- Are there additional hook points we should consider?
- Should we support extension dependencies (extension A requires extension B)?
- How should we handle extension deprecation/removal from catalog?
- What level of sandboxing/permissions do we need in v1.0?