2026-02-10 21:27:20 +01:00
# Extension API Reference
Technical reference for Spec Kit extension system APIs and manifest schema.
## Table of Contents
1. [Extension Manifest ](#extension-manifest )
2. [Python API ](#python-api )
3. [Command File Format ](#command-file-format )
4. [Configuration Schema ](#configuration-schema )
5. [Hook System ](#hook-system )
6. [CLI Commands ](#cli-commands )
---
## Extension Manifest
### Schema Version 1.0
File: `extension.yml`
```yaml
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 :
2026-08-08 01:55:55 +08:00
commands : # At least one of commands/templates/scripts/hooks/events required
2026-02-10 21:27:20 +01:00
- name : string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
file : string # Required, relative path to command file
description : string # Required
2026-03-27 17:55:26 +02:00
aliases : [ string] # Optional, same pattern as name; namespace must match extension.id and must not shadow core or installed extension commands
2026-02-10 21:27:20 +01:00
2026-08-08 01:55:55 +08:00
templates : # Optional, array of declared templates. Always resolve
# as "replace" -- 'strategy' is not an authorable field here.
- name : string # Required, pattern: ^[a-z0-9-]+$
file : string # Required, relative path to template file
description : string # Optional
scripts : # Optional, array of declared scripts. Always resolve
# as "replace" -- 'strategy' is not an authorable field here.
- name : string # Required, pattern: ^[a-z0-9-]+$
file : string # Required, relative path to script file
description : string # Optional
runtimes : [ string] # Optional, subset of: bash, powershell, python
2026-02-10 21:27:20 +01:00
config : # Optional, array of config files
- name : string # Config file name
template : string # Template file path
description : string
required : boolean # Default: false
2026-06-08 22:03:46 +09:00
hooks : # Optional, event hooks. Each event accepts either form below.
2026-03-19 04:37:03 -07:00
event_name : # e.g., "after_specify", "after_plan", "after_tasks", "after_implement"
2026-02-10 21:27:20 +01:00
command : string # Command to execute
2026-06-08 22:03:46 +09:00
priority : integer # Optional, >= 1, default 10 (lower runs first)
2026-02-10 21:27:20 +01:00
optional : boolean # Default: true
prompt : string # Prompt text for optional hooks
description : string # Hook description
condition : string # Optional, condition expression
2026-06-08 22:03:46 +09:00
another_event : # Any event may instead use a list of mappings (multiple commands)
- command : string # Same fields as the single mapping, per entry
priority : integer
- command : string
priority : integer
2026-02-10 21:27:20 +01:00
tags : # Optional, array of tags (2-10 recommended)
- string
defaults : # Optional, default configuration values
key : value # Any YAML structure
```
### Field Specifications
#### `extension.id`
- **Type**: string
- **Pattern**: `^[a-z0-9-]+$`
- **Description**: Unique extension identifier
- **Examples**: `jira` , `linear` , `azure-devops`
- **Invalid**: `Jira` , `my_extension` , `extension.id`
#### `extension.version`
- **Type**: string
- **Format**: Semantic versioning (X.Y.Z)
- **Description**: Extension version
- **Examples**: `1.0.0` , `0.9.5` , `2.1.3`
- **Invalid**: `v1.0` , `1.0` , `1.0.0-beta`
#### `requires.speckit_version`
- **Type**: string
- **Format**: Version specifier
- **Description**: Required spec-kit version range
- **Examples**:
- `>=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.0
- **Invalid**: `0.1.0` , `>= 0.1.0` (space), `latest`
#### `provides.commands[].name`
- **Type**: string
- **Pattern**: `^speckit\.[a-z0-9-]+\.[a-z0-9-]+$`
- **Description**: Namespaced command name
- **Format**: `speckit.{extension-id}.{command-name}`
- **Examples**: `speckit.jira.specstoissues` , `speckit.linear.sync`
- **Invalid**: `jira.specstoissues` , `speckit.command` , `speckit.jira.CreateIssues`
2026-08-08 01:55:55 +08:00
#### `provides.templates[].name` / `provides.scripts[].name`
- **Type**: string
- **Pattern**: `^[a-z0-9-]+$`
- **Description**: Unlike commands, templates and scripts are not invoked by
name, so they use the same plain slug pattern as `extension.id` rather than
the namespaced command pattern.
- **Examples**: `myext-template` , `myext-collect`
#### `provides.templates[].strategy` / `provides.scripts[].strategy`
- Not an authorable field. Extension-contributed templates and scripts are
always resolved as `replace` ; a manifest that includes a `strategy` key on
one of these entries is rejected with a `ValidationError` . Composable
strategies (`wrap` /`prepend` /`append` ) are preset-only.
#### `provides.scripts[].runtimes`
- **Type**: array of strings
- **Values**: `bash` , `powershell` , `python`
- **Description**: Declares which runtimes the script supports. Purely
informational metadata — it is not used to select or invoke the script.
2026-02-10 21:27:20 +01:00
#### `hooks`
- **Type**: object
2026-04-07 08:39:35 -05:00
- **Keys**: Event names (e.g., `after_specify` , `after_plan` , `after_tasks` , `after_implement` , `before_analyze` )
2026-06-08 22:03:46 +09:00
- **Value**: A single hook mapping, or a list of hook mappings to register multiple commands on one event
2026-02-10 21:27:20 +01:00
- **Description**: Hooks that execute at lifecycle events
- **Events**: Defined by core spec-kit commands
2026-06-08 22:03:46 +09:00
- **Ordering**: Within an event, hooks run by ascending `priority` (integer ≥ 1, default 10; lower runs first; equal priorities keep authoring order via a stable sort)
2026-02-10 21:27:20 +01:00
---
## Python API
### ExtensionManifest
**Module** : `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionManifest
manifest = ExtensionManifest ( Path ( "extension.yml" ))
```
**Properties** :
```python
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
2026-08-08 01:55:55 +08:00
manifest . templates # List[Dict]: Declared template definitions
manifest . scripts # List[Dict]: Declared script definitions
2026-02-10 21:27:20 +01:00
manifest . hooks # Dict: Hook definitions
```
**Methods** :
```python
manifest . get_hash () # str: SHA256 hash of manifest file
```
**Exceptions** :
```python
ValidationError # Invalid manifest structure
CompatibilityError # Incompatible with current spec-kit version
```
### ExtensionRegistry
**Module** : `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionRegistry
registry = ExtensionRegistry ( extensions_dir )
```
**Methods** :
```python
# 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** :
```json
{
"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..."
}
}
}
```
### ExtensionManager
**Module** : `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionManager
manager = ExtensionManager ( project_root )
```
**Methods** :
```python
# 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
```
2026-03-09 14:30:27 -05:00
### CatalogEntry
**Module** : `specify_cli.extensions`
Represents a single catalog in the active catalog stack.
```python
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) |
2026-02-10 21:27:20 +01:00
### ExtensionCatalog
**Module** : `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionCatalog
catalog = ExtensionCatalog ( project_root )
```
2026-03-09 14:30:27 -05:00
**Class attributes** :
```python
ExtensionCatalog . DEFAULT_CATALOG_URL # default catalog URL
ExtensionCatalog . COMMUNITY_CATALOG_URL # community catalog URL
```
2026-02-10 21:27:20 +01:00
**Methods** :
```python
2026-03-09 14:30:27 -05:00
# Get the ordered list of active catalogs
entries = catalog . get_active_catalogs () # List[CatalogEntry]
# Fetch catalog (primary catalog, backward compat)
2026-02-10 21:27:20 +01:00
catalog_data = catalog . fetch_catalog ( force_refresh : bool = False ) # Dict
2026-03-09 14:30:27 -05:00
# Search extensions across all active catalogs
# Each result includes _catalog_name and _install_allowed
2026-02-10 21:27:20 +01:00
results = catalog . search (
query : Optional [ str ] = None ,
tag : Optional [ str ] = None ,
author : Optional [ str ] = None ,
verified_only : bool = False
2026-03-09 14:30:27 -05:00
) # Returns: List[Dict] — each dict includes _catalog_name, _install_allowed
2026-02-10 21:27:20 +01:00
2026-03-09 14:30:27 -05:00
# Get extension info (searches all active catalogs)
# Returns None if not found; includes _catalog_name and _install_allowed
2026-02-10 21:27:20 +01:00
ext_info = catalog . get_extension_info ( extension_id : str ) # Optional[Dict]
2026-03-09 14:30:27 -05:00
# Check cache validity (primary catalog)
2026-02-10 21:27:20 +01:00
is_valid = catalog . is_cache_valid () # bool
2026-03-09 14:30:27 -05:00
# Clear all catalog caches
2026-02-10 21:27:20 +01:00
catalog . clear_cache ()
```
2026-03-09 14:30:27 -05:00
**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` ):
```yaml
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)"
```
2026-02-10 21:27:20 +01:00
### HookExecutor
**Module** : `specify_cli.extensions`
```python
from specify_cli.extensions import HookExecutor
hook_executor = HookExecutor ( project_root )
```
**Methods** :
```python
# 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
```
### CommandRegistrar
**Module** : `specify_cli.extensions`
```python
from specify_cli.extensions import CommandRegistrar
registrar = CommandRegistrar ()
```
**Methods** :
```python
# 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
```
---
## Command File Format
### Universal Command Format
**File** : `commands/{command-name}.md`
```markdown
---
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.
```
### Frontmatter Fields
```yaml
description : string # Required, brief command description
tools : [ string] # Optional, MCP tools required
```
### Special Variables
- `$ARGUMENTS` - Placeholder for user-provided arguments
- Extension context automatically injected:
```markdown
<!-- Extension: {extension-id} -->
<!-- Config: .specify/extensions/{extension-id}/ -->
` ``
---
## Configuration Schema
### Extension Config File
**File**: ` .specify/extensions/{extension-id}/{extension-id}-config.yml`
Extensions define their own config schema. Common patterns:
` ``yaml
# 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"
` ``
### Config Layers
1. **Extension Defaults** (from ` extension.yml` ` defaults` section)
2. **Project Config** (` {extension-id}-config.yml`)
3. **Local Override** (` {extension-id}-config.local.yml`, gitignored)
4. **Environment Variables** (` SPECKIT_{EXTENSION}_*`)
### Environment Variable Pattern
Format: ` SPECKIT_{EXTENSION}_{KEY}`
Examples:
- ` SPECKIT_JIRA_PROJECT_KEY`
- ` SPECKIT_LINEAR_API_KEY`
- ` SPECKIT_GITHUB_TOKEN`
---
## Hook System
### Hook Definition
2026-06-08 22:03:46 +09:00
Each event accepts either a single hook mapping or a list of mappings. A list registers multiple commands on the same event.
**Single mapping (in extension.yml)**:
2026-02-10 21:27:20 +01:00
` ``yaml
hooks:
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
prompt: "Create Jira issues from tasks?"
description: "Automatically create Jira hierarchy"
condition: null
` ``
2026-06-08 22:03:46 +09:00
**List of mappings with priority**:
` ``yaml
hooks:
after_plan:
- command: "speckit.my-ext.verify"
priority: 5
optional: false
description: "Verify the plan"
- command: "speckit.my-ext.report"
priority: 10
optional: true
prompt: "Generate the report?"
description: "Generate a report from the plan"
` ``
Within a single manifest list, a repeated ` command` is deduped as "last wins" and moved to the end, so it also breaks equal-priority ties in authoring order.
2026-02-10 21:27:20 +01:00
### Hook Events
Standard events (defined by core):
2026-03-19 04:37:03 -07:00
- ` before_specify` - Before specification generation
- ` after_specify` - After specification generation
- ` before_plan` - Before implementation planning
- ` after_plan` - After implementation planning
- ` before_tasks` - Before task generation
2026-02-10 21:27:20 +01:00
- ` after_tasks` - After task generation
2026-03-19 04:37:03 -07:00
- ` before_implement` - Before implementation
2026-02-10 21:27:20 +01:00
- ` after_implement` - After implementation
2026-04-07 08:39:35 -05:00
- ` before_analyze` - Before cross-artifact analysis
- ` after_analyze` - After cross-artifact analysis
- ` before_checklist` - Before checklist generation
- ` after_checklist` - After checklist generation
- ` before_clarify` - Before spec clarification
- ` after_clarify` - After spec clarification
- ` before_constitution` - Before constitution update
- ` after_constitution` - After constitution update
- ` before_taskstoissues` - Before tasks-to-issues conversion
- ` after_taskstoissues` - After tasks-to-issues conversion
2026-02-10 21:27:20 +01:00
### Hook Configuration
**In ` .specify/extensions.yml`**:
` ``yaml
hooks:
after_tasks:
- extension: jira
command: speckit.jira.specstoissues
enabled: true
optional: true
prompt: "Create Jira issues from tasks?"
description: "..."
condition: null
` ``
### Hook Message Format
` ``markdown
## Extension Hooks
**Optional Hook**: {extension}
Command: ` /{command}`
Description: {description}
Prompt: {prompt}
To execute: ` /{command}`
` ``
Or for mandatory hooks:
` ``markdown
**Automatic Hook**: {extension}
Executing: ` /{command}`
EXECUTE_COMMAND: {command}
` ``
---
## CLI Commands
### extension list
**Usage**: ` specify extension list [OPTIONS]`
**Options**:
- ` --available` - Show available extensions from catalog
- ` --all` - Show both installed and available
**Output**: List of installed extensions with metadata
2026-03-09 14:30:27 -05:00
### extension catalog list
**Usage**: ` specify extension catalog list`
Lists all active catalogs in the current catalog stack, showing name, description, URL, priority, and ` install_allowed` status.
### extension catalog add
**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 catalog
**Arguments**:
- ` URL` - Catalog URL (must use HTTPS)
Adds a catalog entry to ` .specify/extension-catalogs.yml`.
### extension catalog remove
**Usage**: ` specify extension catalog remove NAME`
**Arguments**:
- ` NAME` - Catalog name to remove
Removes a catalog entry from ` .specify/extension-catalogs.yml`.
2026-02-10 21:27:20 +01:00
### extension add
**Usage**: ` specify extension add EXTENSION [OPTIONS]`
**Options**:
- ` --from URL` - Install from custom URL
- ` --dev PATH` - Install from local directory
**Arguments**:
- ` EXTENSION` - Extension name or URL
2026-03-09 14:30:27 -05:00
**Note**: Extensions from catalogs with ` install_allowed: false` cannot be installed via this command.
2026-02-10 21:27:20 +01:00
### extension remove
**Usage**: ` specify extension remove EXTENSION [OPTIONS]`
**Options**:
- ` --keep-config` - Preserve config files
- ` --force` - Skip confirmation
**Arguments**:
- ` EXTENSION` - Extension ID
### extension search
**Usage**: ` specify extension search [QUERY] [OPTIONS]`
2026-03-09 14:30:27 -05:00
Searches all active catalogs simultaneously. Results include source catalog name and install_allowed status.
2026-02-10 21:27:20 +01:00
**Options**:
- ` --tag TAG` - Filter by tag
- ` --author AUTHOR` - Filter by author
- ` --verified` - Show only verified extensions
**Arguments**:
- ` QUERY` - Optional search query
### extension info
**Usage**: ` specify extension info EXTENSION`
2026-03-09 14:30:27 -05:00
Shows source catalog and install_allowed status.
2026-02-10 21:27:20 +01:00
**Arguments**:
- ` EXTENSION` - Extension ID
### extension update
**Usage**: ` specify extension update [EXTENSION]`
**Arguments**:
- ` EXTENSION` - Optional, extension ID (default: all)
### extension enable
**Usage**: ` specify extension enable EXTENSION`
**Arguments**:
- ` EXTENSION` - Extension ID
### extension disable
**Usage**: ` specify extension disable EXTENSION`
**Arguments**:
- ` EXTENSION` - Extension ID
---
## Exceptions
### ValidationError
Raised when extension manifest validation fails.
` ``python
from specify_cli.extensions import ValidationError
try:
manifest = ExtensionManifest(path)
except ValidationError as e:
print(f"Invalid manifest: {e}")
` ``
### CompatibilityError
Raised when extension is incompatible with current spec-kit version.
` ``python
from specify_cli.extensions import CompatibilityError
try:
manager.check_compatibility(manifest, "0.1.0")
except CompatibilityError as e:
print(f"Incompatible: {e}")
` ``
### ExtensionError
Base exception for all extension-related errors.
` ``python
from specify_cli.extensions import ExtensionError
try:
manager.install_from_directory(path, "0.1.0")
except ExtensionError as e:
print(f"Extension error: {e}")
` ``
---
## Version Functions
### version_satisfies
Check if a version satisfies a specifier.
` ``python
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
` ``
---
## File System Layout
` ``text
.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*