Files
spec-kit/extensions/EXTENSION-DEVELOPMENT-GUIDE.md
Michal Bachorik f14a47ea7d Add modular extension system (#1551)
* 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>
2026-02-10 14:27:20 -06:00

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 name
  • version: Semantic version (e.g., "1.0.0")
  • description: Short description

Optional sub-fields:

  • author: Extension author
  • repository: Source code URL
  • license: SPDX license identifier
  • homepage: 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 match speckit.{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.tasks completes
  • after_implement: After /speckit.implement completes (future)

Hook object:

  • command: Command to execute (must be in provides.commands)
  • optional: If true, prompt user before executing
  • prompt: Prompt text for optional hooks
  • description: Hook description
  • condition: 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:

  1. Extension defaults (extension.ymldefaults)
  2. Project config (.specify/extensions/my-ext/my-ext-config.yml)
  3. Local overrides (.specify/extensions/my-ext/my-ext-config.local.yml - gitignored)
  4. 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

  1. Create test extension

  2. Install locally:

    specify extension add --dev /path/to/extension
    
  3. Verify installation:

    specify extension list
    
  4. Test commands with your AI agent

  5. Check command registration:

    ls .claude/commands/speckit.my-ext.*
    
  6. 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

  1. Create repository: spec-kit-my-ext

  2. Add files:

    spec-kit-my-ext/
    ├── extension.yml
    ├── commands/
    ├── scripts/
    ├── docs/
    ├── README.md
    ├── LICENSE
    └── CHANGELOG.md
    
  3. Create release: Tag with version (e.g., v1.0.0)

  4. 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:

  1. Fork spec-kit repository

  2. Add entry to extensions/catalog.json

  3. Create PR

  4. 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, not ji)
  • 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:

  1. .claude/commands/ directory exists

  2. Extension installed successfully

  3. Commands registered in registry:

    cat .specify/extensions/.registry
    

Fix: Reinstall extension to trigger registration

Config not loading

Check:

  1. Config file exists: .specify/extensions/{ext-id}/{ext-id}-config.yml
  2. YAML syntax is valid: yq eval '.' config.yml
  3. Environment variables set correctly

Getting Help

  • Issues: Report bugs at GitHub repository
  • Discussions: Ask questions in GitHub Discussions
  • Examples: See spec-kit-jira for full-featured example (Phase B)

Next Steps

  1. Create your extension following this guide
  2. Test locally with --dev flag
  3. Share with community (GitHub, catalog)
  4. Iterate based on feedback

Happy extending! 🚀