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:
.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 removalThe Manifest (extension.yml)
Every extension requires an extension.yml manifest:
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:
| Agent | Directory | Format | Argument 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:
# Search the catalog
specify extension search "jira"
# Get extension details
specify extension info jira-sync
# Install from catalog
specify extension install jira-syncThe catalog is cached locally for 1 hour in .specify/extensions/.cache/.
You can override the catalog URL for private registries:
export SPECKIT_CATALOG_URL=https://gitlab.mycompany.com/speckit/catalog/-/raw/main/catalog.jsonInstalling Extensions
From the catalog
specify extension install jira-syncFrom a local directory
specify extension install ./my-extension/From a ZIP file
specify extension install ./jira-sync-v1.0.0.zipWhat happens during install
- The manifest is loaded and validated
- Compatibility is checked against your spec-kit version
- Files are copied to
.specify/extensions/{ext-id}/ - Commands are registered with all detected AI agents
- Hooks are registered in
.specify/extensions.yml - The extension is added to
.specify/extensions/.registry
Layered Configuration
Extensions support 4 configuration layers (highest priority wins):
| Layer | Location | Git-tracked | Purpose |
|---|---|---|---|
| Defaults | extension.yml → defaults | Yes (in extension) | Sensible defaults |
| Project | .specify/extensions/{id}/{id}-config.yml | Yes | Team-shared settings |
| Local | .specify/extensions/{id}/local-config.yml | No (gitignored) | Machine-specific overrides |
| Environment | SPECKIT_{EXT_ID}_{KEY} | No | CI/CD and secrets |
Example for a Jira extension:
# Environment variable pattern: SPECKIT_{EXT_ID}_{SECTION}_{KEY}
export SPECKIT_JIRA_SYNC_CONNECTION_URL=https://mycompany.atlassian.net
export SPECKIT_JIRA_SYNC_PROJECT_KEY=PROJHooks
Hooks let extensions run automatically after core commands:
# .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
| Event | Fires After |
|---|---|
after_spec | /speckit.specify |
after_plan | /speckit.plan |
after_tasks | /speckit.tasks |
after_implement | /speckit.implement |
Conditions
Hooks support conditional execution:
# 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
- Create a directory with the extension structure:
my-extension/
├── extension.yml
├── commands/
│ └── my-command.md
└── scripts/
└── bash/
└── helper.sh- Write your command file:
---
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- Test locally:
specify extension install ./my-extension/- Publish to the catalog by submitting a merge request to the spec-kit extensions repository.
Removing Extensions
# 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.