extensions/EXTENSION-API-REFERENCE.md
Technical reference for Spec Kit extension system APIs and manifest schema.
File: extension.yml
schema_version: "1.0" # Required
extension:
id: string # Required, pattern: ^[a-z0-9-]+$
name: string # Required, human-readable name
version: string # Required, semantic version (X.Y.Z)
description: string # Required, brief description (<200 chars)
author: string # Required
repository: string # Required, valid URL
license: string # Required (e.g., "MIT", "Apache-2.0")
homepage: string # Optional, valid URL
requires:
speckit_version: string # Required, version specifier (>=X.Y.Z)
tools: # Optional, array of tool requirements
- name: string # Tool name
version: string # Optional, version specifier
required: boolean # Optional, default: false
provides:
commands: # Required, at least one command
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
file: string # Required, relative path to command file
description: string # Required
aliases: [string] # Optional, same pattern as name; namespace must match extension.id and must not shadow core or installed extension commands
config: # Optional, array of config files
- name: string # Config file name
template: string # Template file path
description: string
required: boolean # Default: false
hooks: # Optional, event hooks
event_name: # e.g., "after_specify", "after_plan", "after_tasks", "after_implement"
command: string # Command to execute
optional: boolean # Default: true
prompt: string # Prompt text for optional hooks
description: string # Hook description
condition: string # Optional, condition expression
tags: # Optional, array of tags (2-10 recommended)
- string
defaults: # Optional, default configuration values
key: value # Any YAML structure
extension.id^[a-z0-9-]+$jira, linear, azure-devopsJira, my_extension, extension.idextension.version1.0.0, 0.9.5, 2.1.3v1.0, 1.0, 1.0.0-betarequires.speckit_version>=0.1.0 - Any version 0.1.0 or higher>=0.1.0,<2.0.0 - Version 0.1.x or 1.x==0.1.0 - Exactly 0.1.00.1.0, >= 0.1.0 (space), latestprovides.commands[].name^speckit\.[a-z0-9-]+\.[a-z0-9-]+$speckit.{extension-id}.{command-name}speckit.jira.specstoissues, speckit.linear.syncjira.specstoissues, speckit.command, speckit.jira.CreateIssueshooksafter_specify, after_plan, after_tasks, after_implement, before_analyze)Module: specify_cli.extensions
from specify_cli.extensions import ExtensionManifest
manifest = ExtensionManifest(Path("extension.yml"))
Properties:
manifest.id # str: Extension ID
manifest.name # str: Extension name
manifest.version # str: Version
manifest.description # str: Description
manifest.requires_speckit_version # str: Required spec-kit version
manifest.commands # List[Dict]: Command definitions
manifest.hooks # Dict: Hook definitions
Methods:
manifest.get_hash() # str: SHA256 hash of manifest file
Exceptions:
ValidationError # Invalid manifest structure
CompatibilityError # Incompatible with current spec-kit version
Module: specify_cli.extensions
from specify_cli.extensions import ExtensionRegistry
registry = ExtensionRegistry(extensions_dir)
Methods:
# Add extension to registry
registry.add(extension_id: str, metadata: dict)
# Remove extension from registry
registry.remove(extension_id: str)
# Get extension metadata
metadata = registry.get(extension_id: str) # Optional[dict]
# List all extensions
extensions = registry.list() # Dict[str, dict]
# Check if installed
is_installed = registry.is_installed(extension_id: str) # bool
Registry Format:
{
"schema_version": "1.0",
"extensions": {
"jira": {
"version": "1.0.0",
"source": "catalog",
"manifest_hash": "sha256...",
"enabled": true,
"registered_commands": ["speckit.jira.specstoissues", ...],
"installed_at": "2026-01-28T..."
}
}
}
Module: specify_cli.extensions
from specify_cli.extensions import ExtensionManager
manager = ExtensionManager(project_root)
Methods:
# Install from directory
manifest = manager.install_from_directory(
source_dir: Path,
speckit_version: str,
register_commands: bool = True
) # Returns: ExtensionManifest
# Install from ZIP
manifest = manager.install_from_zip(
zip_path: Path,
speckit_version: str
) # Returns: ExtensionManifest
# Remove extension
success = manager.remove(
extension_id: str,
keep_config: bool = False
) # Returns: bool
# List installed extensions
extensions = manager.list_installed() # List[Dict]
# Get extension manifest
manifest = manager.get_extension(extension_id: str) # Optional[ExtensionManifest]
# Check compatibility
manager.check_compatibility(
manifest: ExtensionManifest,
speckit_version: str
) # Raises: CompatibilityError if incompatible
Module: specify_cli.extensions
Represents a single catalog in the active catalog stack.
from specify_cli.extensions import CatalogEntry
entry = CatalogEntry(
url="https://example.com/catalog.json",
name="default",
priority=1,
install_allowed=True,
description="Built-in catalog of installable extensions",
)
Fields:
| Field | Type | Description |
|---|---|---|
url | str | Catalog URL (must use HTTPS, or HTTP for localhost) |
name | str | Human-readable catalog name |
priority | int | Sort order (lower = higher priority, wins on conflicts) |
install_allowed | bool | Whether extensions from this catalog can be installed |
description | str | Optional human-readable description of the catalog (default: empty) |
Module: specify_cli.extensions
from specify_cli.extensions import ExtensionCatalog
catalog = ExtensionCatalog(project_root)
Class attributes:
ExtensionCatalog.DEFAULT_CATALOG_URL # default catalog URL
ExtensionCatalog.COMMUNITY_CATALOG_URL # community catalog URL
Methods:
# Get the ordered list of active catalogs
entries = catalog.get_active_catalogs() # List[CatalogEntry]
# Fetch catalog (primary catalog, backward compat)
catalog_data = catalog.fetch_catalog(force_refresh: bool = False) # Dict
# Search extensions across all active catalogs
# Each result includes _catalog_name and _install_allowed
results = catalog.search(
query: Optional[str] = None,
tag: Optional[str] = None,
author: Optional[str] = None,
verified_only: bool = False
) # Returns: List[Dict] — each dict includes _catalog_name, _install_allowed
# Get extension info (searches all active catalogs)
# Returns None if not found; includes _catalog_name and _install_allowed
ext_info = catalog.get_extension_info(extension_id: str) # Optional[Dict]
# Check cache validity (primary catalog)
is_valid = catalog.is_cache_valid() # bool
# Clear all catalog caches
catalog.clear_cache()
Result annotation fields:
Each extension dict returned by search() and get_extension_info() includes:
| Field | Type | Description |
|---|---|---|
_catalog_name | str | Name of the source catalog |
_install_allowed | bool | Whether installation is allowed from this catalog |
Catalog config file (.specify/extension-catalogs.yml):
catalogs:
- name: "default"
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
priority: 1
install_allowed: true
description: "Built-in catalog of installable extensions"
- name: "community"
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
priority: 2
install_allowed: false
description: "Community-contributed extensions (discovery only)"
Module: specify_cli.extensions
from specify_cli.extensions import HookExecutor
hook_executor = HookExecutor(project_root)
Methods:
# Get project config
config = hook_executor.get_project_config() # Dict
# Save project config
hook_executor.save_project_config(config: Dict)
# Register hooks
hook_executor.register_hooks(manifest: ExtensionManifest)
# Unregister hooks
hook_executor.unregister_hooks(extension_id: str)
# Get hooks for event
hooks = hook_executor.get_hooks_for_event(event_name: str) # List[Dict]
# Check if hook should execute
should_run = hook_executor.should_execute_hook(hook: Dict) # bool
# Format hook message
message = hook_executor.format_hook_message(
event_name: str,
hooks: List[Dict]
) # str
Module: specify_cli.extensions
from specify_cli.extensions import CommandRegistrar
registrar = CommandRegistrar()
Methods:
# Register commands for Claude Code
registered = registrar.register_commands_for_claude(
manifest: ExtensionManifest,
extension_dir: Path,
project_root: Path
) # Returns: List[str] (command names)
# Parse frontmatter
frontmatter, body = registrar.parse_frontmatter(content: str)
# Render frontmatter
yaml_text = registrar.render_frontmatter(frontmatter: Dict) # str
File: commands/{command-name}.md
---
description: "Command description"
tools:
- 'mcp-server/tool_name'
- 'other-mcp-server/other_tool'
---
# Command Title
Command documentation in Markdown.
## Prerequisites
1. Requirement 1
2. Requirement 2
## User Input
$ARGUMENTS
## Steps
### Step 1: Description
Instruction text...
\`\`\`bash
# Shell commands
\`\`\`
### Step 2: Another Step
More instructions...
## Configuration Reference
Information about configuration options.
## Notes
Additional notes and tips.
description: string # Required, brief command description
tools: [string] # Optional, MCP tools required
$ARGUMENTS - Placeholder for user-provided arguments
Extension context automatically injected:
<!-- Extension: {extension-id} -->
<!-- Config: .specify/extensions/{extension-id}/ -->
File: .specify/extensions/{extension-id}/{extension-id}-config.yml
Extensions define their own config schema. Common patterns:
# Connection settings
connection:
url: string
api_key: string
# Project settings
project:
key: string
workspace: string
# Feature flags
features:
enabled: boolean
auto_sync: boolean
# Defaults
defaults:
labels: [string]
assignee: string
# Custom fields
field_mappings:
internal_name: "external_field_id"
extension.yml defaults section){extension-id}-config.yml){extension-id}-config.local.yml, gitignored)SPECKIT_{EXTENSION}_*)Format: SPECKIT_{EXTENSION}_{KEY}
Examples:
SPECKIT_JIRA_PROJECT_KEYSPECKIT_LINEAR_API_KEYSPECKIT_GITHUB_TOKENIn extension.yml:
hooks:
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
prompt: "Create Jira issues from tasks?"
description: "Automatically create Jira hierarchy"
condition: null
Standard events (defined by core):
before_specify - Before specification generationafter_specify - After specification generationbefore_plan - Before implementation planningafter_plan - After implementation planningbefore_tasks - Before task generationafter_tasks - After task generationbefore_implement - Before implementationafter_implement - After implementationbefore_analyze - Before cross-artifact analysisafter_analyze - After cross-artifact analysisbefore_checklist - Before checklist generationafter_checklist - After checklist generationbefore_clarify - Before spec clarificationafter_clarify - After spec clarificationbefore_constitution - Before constitution updateafter_constitution - After constitution updatebefore_taskstoissues - Before tasks-to-issues conversionafter_taskstoissues - After tasks-to-issues conversionIn .specify/extensions.yml:
hooks:
after_tasks:
- extension: jira
command: speckit.jira.specstoissues
enabled: true
optional: true
prompt: "Create Jira issues from tasks?"
description: "..."
condition: null
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
Or for mandatory hooks:
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
Usage: specify extension list [OPTIONS]
Options:
--available - Show available extensions from catalog--all - Show both installed and availableOutput: List of installed extensions with metadata
Usage: specify extension catalog list
Lists all active catalogs in the current catalog stack, showing name, description, URL, priority, and install_allowed status.
Usage: specify extension catalog add URL [OPTIONS]
Options:
--name NAME - Catalog name (required)--priority INT - Priority (lower = higher priority, default: 10)--install-allowed / --no-install-allowed - Allow installs from this catalog (default: false)--description TEXT - Optional description of the catalogArguments:
URL - Catalog URL (must use HTTPS)Adds a catalog entry to .specify/extension-catalogs.yml.
Usage: specify extension catalog remove NAME
Arguments:
NAME - Catalog name to removeRemoves a catalog entry from .specify/extension-catalogs.yml.
Usage: specify extension add EXTENSION [OPTIONS]
Options:
--from URL - Install from custom URL--dev PATH - Install from local directoryArguments:
EXTENSION - Extension name or URLNote: Extensions from catalogs with install_allowed: false cannot be installed via this command.
Usage: specify extension remove EXTENSION [OPTIONS]
Options:
--keep-config - Preserve config files--force - Skip confirmationArguments:
EXTENSION - Extension IDUsage: specify extension search [QUERY] [OPTIONS]
Searches all active catalogs simultaneously. Results include source catalog name and install_allowed status.
Options:
--tag TAG - Filter by tag--author AUTHOR - Filter by author--verified - Show only verified extensionsArguments:
QUERY - Optional search queryUsage: specify extension info EXTENSION
Shows source catalog and install_allowed status.
Arguments:
EXTENSION - Extension IDUsage: specify extension update [EXTENSION]
Arguments:
EXTENSION - Optional, extension ID (default: all)Usage: specify extension enable EXTENSION
Arguments:
EXTENSION - Extension IDUsage: specify extension disable EXTENSION
Arguments:
EXTENSION - Extension IDRaised when extension manifest validation fails.
from specify_cli.extensions import ValidationError
try:
manifest = ExtensionManifest(path)
except ValidationError as e:
print(f"Invalid manifest: {e}")
Raised when extension is incompatible with current spec-kit version.
from specify_cli.extensions import CompatibilityError
try:
manager.check_compatibility(manifest, "0.1.0")
except CompatibilityError as e:
print(f"Incompatible: {e}")
Base exception for all extension-related errors.
from specify_cli.extensions import ExtensionError
try:
manager.install_from_directory(path, "0.1.0")
except ExtensionError as e:
print(f"Extension error: {e}")
Check if a version satisfies a specifier.
from specify_cli.extensions import version_satisfies
# True if 1.2.3 satisfies >=1.0.0,<2.0.0
satisfied = version_satisfies("1.2.3", ">=1.0.0,<2.0.0") # bool
.specify/
├── extensions/
│ ├── .registry # Extension registry (JSON)
│ ├── .cache/ # Catalog cache
│ │ ├── catalog.json
│ │ └── catalog-metadata.json
│ ├── .backup/ # Config backups
│ │ └── {ext}-{config}.yml
│ ├── {extension-id}/ # Extension directory
│ │ ├── extension.yml # Manifest
│ │ ├── {ext}-config.yml # User config
│ │ ├── {ext}-config.local.yml # Local overrides (gitignored)
│ │ ├── {ext}-config.template.yml # Template
│ │ ├── commands/ # Command files
│ │ │ └── *.md
│ │ ├── scripts/ # Helper scripts
│ │ │ └── *.sh
│ │ ├── docs/ # Documentation
│ │ └── README.md
│ └── extensions.yml # Project extension config
└── scripts/ # (existing spec-kit)
.claude/
└── commands/
└── speckit.{ext}.{cmd}.md # Registered commands
Last Updated: 2026-01-28 API Version: 1.0 Spec Kit Version: 0.1.0