Read OSS

Extending Spec Kit: The Plugin Architecture for Extensions and Presets

Advanced

Prerequisites

  • Article 1: Architecture and Project Navigation
  • Article 3: Integration System (for understanding CommandRegistrar)
  • Article 4: Command Templates and Workflow Engine
  • YAML syntax and JSON schema concepts

Extending Spec Kit: The Plugin Architecture for Extensions and Presets

The 9 built-in commands cover the core SDD workflow, but real projects need more. A team might want Git branch management integrated into every workflow step, or a healthcare compliance preset that adds mandatory HIPAA sections to spec templates. Spec Kit addresses this with two parallel extensibility mechanisms: extensions add new commands and lifecycle hooks, while presets override existing templates. Both systems share the same validation patterns, catalog discovery, and agent-aware output formatting.

Extension Manifest Schema and Validation

Every extension is defined by an extension.yml manifest, validated by ExtensionManifest. The schema requires four top-level fields:

schema_version: "1.0"
extension:
  id: my-ext        # ^[a-z0-9-]+$ regex validated
  name: "My Extension"
  version: "1.0.0"  # semver validated via packaging.version
  description: "..."
requires:
  speckit_version: ">=0.2.0"  # PEP 440 specifier
provides:
  commands: [...]    # At least one command OR hook required
hooks: {...}         # Optional lifecycle hooks

Command names must follow the EXTENSION_COMMAND_NAME_PATTERN regex: speckit.{extension}.{command}. This namespacing convention — e.g., speckit.git.feature or speckit.git.commit — prevents collisions between extensions and with core commands.

The validation at extensions.py#L148-L231 is strict:

  • Extension IDs must be lowercase alphanumeric with hyphens only
  • Versions must parse as valid semver via packaging.version.Version
  • The speckit_version specifier is validated against the running CLI version
  • Every command must have both name and file fields
  • Every hook must have a command field
  • The extension must provide at least one command or hook

Tip: The validation error messages are specific enough to be actionable — "Invalid command name 'git.feature': must follow pattern 'speckit.{extension}.{command}'" rather than a generic "validation failed." When developing extensions, run specify extension add . from your extension directory to get immediate validation feedback.

The Git Extension: A Canonical Case Study

The bundled git extension at extensions/git/extension.yml is the best example of what extensions can do. It provides:

5 Commands:

Command Purpose
speckit.git.feature Create feature branch with sequential/timestamp numbering
speckit.git.validate Validate branch follows naming conventions
speckit.git.remote Detect git remote URL for GitHub integration
speckit.git.initialize Initialize git repo with initial commit
speckit.git.commit Auto-commit changes after workflow steps

18 Lifecycle Hooks covering every workflow stage:

flowchart LR
    subgraph "before_ hooks"
        BC["before_constitution<br/>(git.initialize)"]
        BS["before_specify<br/>(git.feature)"]
        BCl["before_clarify<br/>(git.commit)"]
        BP["before_plan<br/>(git.commit)"]
        BT["before_tasks<br/>(git.commit)"]
        BI["before_implement<br/>(git.commit)"]
        BCh["before_checklist<br/>(git.commit)"]
        BA["before_analyze<br/>(git.commit)"]
        BTI["before_taskstoissues<br/>(git.commit)"]
    end
    subgraph "after_ hooks"
        AC["after_constitution<br/>(git.commit)"]
        AS["after_specify<br/>(git.commit)"]
        ACl["after_clarify<br/>(git.commit)"]
        AP["after_plan<br/>(git.commit)"]
        AT["after_tasks<br/>(git.commit)"]
        AI["after_implement<br/>(git.commit)"]
        ACh["after_checklist<br/>(git.commit)"]
        AA["after_analyze<br/>(git.commit)"]
        ATI["after_taskstoissues<br/>(git.commit)"]
    end

The before_constitution hook runs speckit.git.initialize (mandatory, optional: false), ensuring a git repo exists before the constitution is created. The before_specify hook runs speckit.git.feature (also mandatory) to create a feature branch. All after_* hooks run speckit.git.commit with optional: true and a user-facing prompt like "Commit specification changes?"

The extension also includes config templates and shell scripts — both bash and PowerShell variants — in extensions/git/scripts/.

Hook System Architecture

Hooks are defined in the manifest's hooks section. Each hook has:

hooks:
  before_specify:
    command: speckit.git.feature
    optional: false
    description: "Create feature branch before specification"
  after_specify:
    command: speckit.git.commit
    optional: true
    prompt: "Commit specification changes?"
    description: "Auto-commit after specification"
flowchart TD
    A["Extension installed"] --> B["Hooks merged into<br/>.specify/extensions.yml"]
    B --> C["AI reads command template"]
    C --> D["Template says: check hooks.before_specify"]
    D --> E["AI reads .specify/extensions.yml"]
    E --> F{"Hook found?"}
    F -->|No| G["Continue normally"]
    F -->|Yes| H{"optional?"}
    H -->|false| I["AI executes /speckit.git.feature"]
    H -->|true| J["AI presents prompt to user"]
    J --> K{"User approves?"}
    K -->|Yes| I
    K -->|No| G
    I --> L["Hook result"] --> G

The critical design decision: hooks are executed by the AI agent, not by the CLI. The extensions.yml file is a declarative configuration that the AI reads and interprets at runtime. The command templates contain the logic for checking hooks — the "runtime" is the LLM reading markdown instructions. This means hook execution is as reliable as the AI agent's ability to follow instructions, which is a tradeoff that keeps the system simple while leveraging the AI's natural language understanding.

ExtensionRegistry and the Catalog Stack

Installed extensions are tracked in .specify/extensions/.registry — a JSON file managed by ExtensionRegistry:

{
  "schema_version": "1.0",
  "extensions": {
    "git": {
      "id": "git",
      "version": "1.0.0",
      "enabled": true,
      "installed_at": "2026-04-12T10:30:00+00:00",
      "priority": 10
    }
  }
}

Extensions can be enabled/disabled without removing them, and each has a priority value used for template resolution when multiple extensions provide the same hook point.

For discovering extensions, Spec Kit uses a multi-catalog system. The official catalog at extensions/catalog.json lists bundled extensions:

{
  "schema_version": "1.0",
  "extensions": {
    "git": {
      "name": "Git Branching Workflow",
      "bundled": true,
      "tags": ["git", "branching", "workflow", "core"]
    }
  }
}

The bundled: true flag tells the CLI to look for the extension in core_pack/extensions/ rather than downloading it. A community catalog at extensions/catalog.community.json and org-specific catalogs can be stacked with priority-based merge resolution.

The CommandRegistrar Bridge

When an extension command needs to be written into agent-specific directories, the extension system doesn't handle format conversion itself — it delegates to CommandRegistrar.

sequenceDiagram
    participant Ext as ExtensionManager
    participant Reg as CommandRegistrar
    participant Dir as Agent Directories

    Ext->>Reg: register_commands_for_all_agents(commands, source_id, ...)
    Reg->>Reg: _ensure_configs() — load from INTEGRATION_REGISTRY
    loop For each detected agent
        Reg->>Reg: Check if agent_dir exists in project
        alt Markdown agent
            Reg->>Dir: Write speckit.git.feature.md
        else TOML agent
            Reg->>Dir: Write speckit.git.feature.toml
        else Skills agent
            Reg->>Dir: Write speckit-git-feature/SKILL.md
        end
    end

The register_commands_for_all_agents() method at agents.py#L503-L540 scans the project for existing agent directories (.claude/skills/, .github/agents/, .gemini/commands/, etc.) and writes the extension's commands in the appropriate format for each detected agent. This means installing an extension after init automatically propagates its commands to whichever agents are already configured.

The AGENT_CONFIGS dict inside CommandRegistrar is lazily populated from INTEGRATION_REGISTRY — the same single-source-of-truth pattern we saw in Part 1. The _ensure_configs() classmethod retries on import failure, handling circular import scenarios during module loading.

For Copilot specifically, register_commands() also calls write_copilot_prompt() to generate the companion .prompt.md files that Copilot requires, keeping the format-specific logic centralized.

Preset System: Template Override Layers

Presets are the second extensibility mechanism. While extensions add commands and hooks, presets replace existing templates. The PresetManifest validation mirrors ExtensionManifest closely — same schema version, same ID format rules, same semver validation.

The bundled "lean" preset at presets/lean/preset.yml replaces 5 core commands with minimal versions:

provides:
  templates:
    - type: "command"
      name: "speckit.specify"
      file: "commands/speckit.specify.md"
      description: "Lean specify - create spec.md from a feature description"
      replaces: "speckit.specify"

The replaces field indicates which core command this template overrides. The resolution priority stack is:

local project files (highest priority)
  ↓
preset templates
  ↓
extension-provided templates
  ↓
core templates (lowest priority)

Presets share the same registry pattern as extensions (.specify/presets/.registry), the same catalog discovery system, and the same CommandRegistrar bridge for agent-specific output.

Tip: Presets and extensions can coexist. You might use the git extension for branch management hooks while applying the lean preset for simpler command templates. The priority system ensures predictable resolution when multiple sources provide the same template.

What's Next

With the architecture, init pipeline, integration system, command templates, and extensibility mechanisms covered, we have one more piece: how does Spec Kit ensure all of this actually works? Part 6 examines the test suite — per-integration tests for 25+ agents, extension and preset validation tests, the CI pipeline, and a practical guide for contributing new integrations.