Skip to content

Extension System

Spec Kit has a modular extension system that lets you add new slash commands, hooks, and configuration without modifying the core CLI.

How Extensions Work

An extension is a self-contained package that provides:

  • Commands — slash commands registered with your AI agent (Claude, Gemini, Copilot, etc.)
  • Hooks — optional actions triggered after core commands (e.g., create GitLab issues after /speckit.tasks)
  • Configuration — layered settings with defaults, project-level, local, and environment variable overrides

Extension Structure

Each extension lives in .specify/extensions/{extension-id}/ and contains:

text
.specify/extensions/
├── .registry                    # JSON registry of all installed extensions
├── .cache/                      # Cached catalog data
│   ├── catalog.json
│   └── catalog-metadata.json
├── my-extension/
│   ├── extension.yml            # Manifest (required)
│   ├── commands/
│   │   ├── command-one.md       # Slash command definitions
│   │   └── command-two.md
│   ├── my-extension-config.yml  # Project-level config
│   ├── local-config.yml         # Local config (gitignored)
│   └── scripts/
│       ├── bash/
│       └── powershell/
└── .backup/                     # Config backups after removal

The Manifest (extension.yml)

Every extension requires an extension.yml manifest:

yaml
schema_version: "1.0"

extension:
  id: "jira-sync"
  name: "Jira Sync"
  version: "1.0.0"
  description: "Sync spec tasks to Jira issues"
  author: "Your Name"
  repository: "https://gitlab.com/your-group/jira-sync-extension"
  license: "MIT"

requires:
  speckit_version: ">=0.1.0"
  tools:
    - name: "jira-api"
      version: ">=1.0.0"
      required: true

provides:
  commands:
    - name: "speckit.jira.sync"
      file: "commands/sync.md"
      description: "Sync tasks to Jira"
      aliases: ["speckit.jira-sync"]

  config:
    - name: "jira-sync-config.yml"
      template: "config-template.yml"
      description: "Jira connection settings"
      required: true

hooks:
  after_tasks:
    command: "speckit.jira.sync"
    optional: true
    prompt: "Sync new tasks to Jira?"
    description: "Creates Jira issues for generated tasks"

tags: ["jira", "project-management", "sync"]

defaults:
  connection:
    url: ""
    project_key: ""

Multi-Agent Command Registration

When you install an extension, its commands are automatically registered with every AI agent detected in your project. The CommandRegistrar adapts command format per agent:

AgentDirectoryFormatArgument Placeholder
Claude Code.claude/commands/Markdown$ARGUMENTS
Gemini.gemini/commands/TOML
GitHub Copilot.github/agents/Markdown$ARGUMENTS
Cursor.cursor/agents/Markdown$ARGUMENTS
Windsurf.windsurf/commands/Markdown$ARGUMENTS
Kilo Code.kilocode/commands/Markdown$ARGUMENTS

The registrar detects which agents are present (by checking for their directories) and only registers commands for those agents. If you later add a new agent with specify init --here --ai gemini, existing extension commands get re-registered.

Extension Catalog

A central catalog at GitLab provides discovery for published extensions:

bash
# Search the catalog
specify extension search "jira"

# Get extension details
specify extension info jira-sync

# Install from catalog
specify extension install jira-sync

The catalog is cached locally for 1 hour in .specify/extensions/.cache/.

You can override the catalog URL for private registries:

bash
export SPECKIT_CATALOG_URL=https://gitlab.mycompany.com/speckit/catalog/-/raw/main/catalog.json

Installing Extensions

From the catalog

bash
specify extension install jira-sync

From a local directory

bash
specify extension install ./my-extension/

From a ZIP file

bash
specify extension install ./jira-sync-v1.0.0.zip

What happens during install

  1. The manifest is loaded and validated
  2. Compatibility is checked against your spec-kit version
  3. Files are copied to .specify/extensions/{ext-id}/
  4. Commands are registered with all detected AI agents
  5. Hooks are registered in .specify/extensions.yml
  6. The extension is added to .specify/extensions/.registry

Layered Configuration

Extensions support 4 configuration layers (highest priority wins):

LayerLocationGit-trackedPurpose
Defaultsextension.ymldefaultsYes (in extension)Sensible defaults
Project.specify/extensions/{id}/{id}-config.ymlYesTeam-shared settings
Local.specify/extensions/{id}/local-config.ymlNo (gitignored)Machine-specific overrides
EnvironmentSPECKIT_{EXT_ID}_{KEY}NoCI/CD and secrets

Example for a Jira extension:

bash
# Environment variable pattern: SPECKIT_{EXT_ID}_{SECTION}_{KEY}
export SPECKIT_JIRA_SYNC_CONNECTION_URL=https://mycompany.atlassian.net
export SPECKIT_JIRA_SYNC_PROJECT_KEY=PROJ

Hooks

Hooks let extensions run automatically after core commands:

yaml
# .specify/extensions.yml
hooks:
  after_tasks:
    - extension: "jira-sync"
      command: "speckit.jira.sync"
      enabled: true
      optional: true          # User gets prompted before execution
      prompt: "Sync tasks to Jira?"
      condition: "config.connection.url is set"

Hook Events

EventFires After
after_spec/speckit.specify
after_plan/speckit.plan
after_tasks/speckit.tasks
after_implement/speckit.implement

Conditions

Hooks support conditional execution:

yaml
# Only run if a config value is set
condition: "config.connection.url is set"

# Only run if config matches a value
condition: "config.mode == 'auto'"

# Only run if an env var is set
condition: "env.JIRA_TOKEN is set"

Creating an Extension

  1. Create a directory with the extension structure:
text
my-extension/
├── extension.yml
├── commands/
│   └── my-command.md
└── scripts/
    └── bash/
        └── helper.sh
  1. Write your command file:
markdown
---
description: "What the command does"
handoffs:
  - label: "Next step"
    agent: "speckit.plan"
    prompt: "Continue with planning"
    send: true
scripts:
  sh: scripts/bash/helper.sh --json "{ARGS}"
---

## Description

Instructions for the AI agent.

## User Input

$ARGUMENTS
  1. Test locally:
bash
specify extension install ./my-extension/
  1. Publish to the catalog by submitting a merge request to the spec-kit extensions repository.

Removing Extensions

bash
# Remove extension (keeps config backup)
specify extension remove jira-sync

# Config is backed up to .specify/extensions/.backup/jira-sync/

Removal unregisters commands from all detected agents, removes hooks, and optionally backs up configuration files.

Released under the MIT License.