Files
Manfred RiemandCopilot App fa3a5c5ce7 Clarify extension catalog trust model in docs, help, and messaging (#4177)
* Clarify extension catalog trust model in docs, help, and messaging (#4176)

Extension catalog management gave no explanation of why the community
catalog is discovery-only, and the install-error text nudged users to
flip a discovery catalog to install_allowed — exactly the wrong move.

- Docs: add a "discovery-only vs. install sources" trust-model section,
  document `add --from <url>` as the lightweight vetted-install path, and
  stop implying you should make community installable.
- Help: expand the `catalog` app and `--install-allowed` help to state the
  vetting intent instead of bare mechanics.
- Messaging: rewrite the not-installable errors in `add`, `search`, and
  `info` to point at `--from` and self-curated catalogs, and to say
  explicitly not to flip a discovery-only catalog to install_allowed.
- `catalog list` now prints trust-model guidance when a discovery-only
  catalog is active.
- Tests cover the new list guidance (present/absent).

Deliberately does not add a verb to toggle install_allowed on an existing
catalog: discovery-only is a security boundary, not an inconvenience.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a86c498e-f129-4422-9983-d1a33513fd4d

* Address PR review: copy-pasteable install hint and accurate --from warning (#4176)

- The discovery-only "install directly" hint used the user-typed argument,
  which can be a display name with spaces (resolved via search) and would
  break when copied as a shell command. Emit the resolved catalog ID
  (ext_info['id']) instead. Added a regression test.
- The `--from` untrusted-source warning claimed the URL was "not listed in
  any of your configured extension catalogs", which is false for a URL
  copied from a discovery-only catalog — the exact flow this PR documents.
  Reword it to state the install is bypassing trusted (install-allowed)
  catalogs, which is accurate regardless of discovery-catalog membership.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a86c498e-f129-4422-9983-d1a33513fd4d

* Harden install hints against catalog-controlled IDs; expose archive URL (#4176)

Second review round on #4177.

Shell-safety: catalog entry IDs (especially from discovery-only catalogs)
are not validated during catalog merge, and rich.markup.escape only
neutralizes Rich markup, not shell metacharacters. A malicious ID like
`foo; rm -rf ~` was interpolated into the `specify extension add ... --from`
command we encourage the user to copy. Add `_command_safe_id`, which only
emits an ID matching the manifest rule `^[a-z0-9-]+$` (via
VALID_EXTENSION_ARTIFACT_NAME_PATTERN) and otherwise falls back to a literal
`<extension-id>` placeholder. Applied to every suggested command in `add`,
`search`, and `info`.

Discoverability: the documented `--from <archive-url>` flow gave no CLI path
to obtain the URL. `extension info` now prints the candidate `download_url`
for a discovery-only entry (clearly flagged as needing vetting), and the docs
show `extension info <name>` as the way to get the archive URL.

Tests cover the resolved-ID hint, the unsafe-ID neutralization, and pass the
full extensions + CLI suites (635).

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a86c498e-f129-4422-9983-d1a33513fd4d

* Reject leading-hyphen catalog IDs; test info archive-URL branch (#4176)

Third review round on #4177.

_command_safe_id: an ID like `--force` satisfies the manifest character rule
`^[a-z0-9-]+$` but Typer parses a leading hyphen as an option rather than the
positional extension argument, so an untrusted catalog could still yield a
non-copyable or option-altering suggested command. Reject a leading hyphen and
fall back to the `<extension-id>` placeholder.

Tests: cover the new `extension info` discovery-only branch that surfaces the
candidate `download_url` (plus the no-URL fallback), and the leading-hyphen
rejection. Full extensions suite green (528).

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a86c498e-f129-4422-9983-d1a33513fd4d

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a86c498e-f129-4422-9983-d1a33513fd4d
2026-08-17 15:48:59 -05:00

281 lines
12 KiB
Markdown

# Extensions
Extensions add new capabilities to Spec Kit — domain-specific commands, external tool integrations, quality gates, and more. They introduce new commands and templates that go beyond the built-in Spec-Driven Development workflow.
## Search Available Extensions
```bash
specify extension search [query]
```
| Option | Description |
| ------------ | ------------------------------------ |
| `--tag` | Filter by tag |
| `--author` | Filter by author |
| `--verified` | Show only verified extensions |
Searches all active catalogs for extensions matching the query. Without a query, lists all available extensions.
## Install an Extension
```bash
specify extension add <name>
```
| Option | Description |
| --------------- | -------------------------------------------------------- |
| `--dev` | Install from a local directory (for development) |
| `--from <url>` | Install from a custom URL instead of the catalog |
| `--force` | Overwrite if the extension is already installed |
| `--priority <N>`| Resolution priority (default: 10; lower = higher precedence) |
Installs an extension from the catalog, a URL, or a local directory. Extension commands are automatically registered with the currently installed AI coding agent integration.
> **Note:** All extension commands require a project already initialized with `specify init`.
## Remove an Extension
```bash
specify extension remove <name>
```
| Option | Description |
| --------------- | ---------------------------------------------- |
| `--keep-config` | Preserve configuration files during removal |
| `--force` | Skip confirmation prompt |
Removes an installed extension. Configuration files are backed up by default; use `--keep-config` to leave them in place or `--force` to skip the confirmation.
## List Installed Extensions
```bash
specify extension list
```
| Option | Description |
| ------------- | -------------------------------------------------- |
| `--available` | Show available (uninstalled) extensions |
| `--all` | Show both installed and available extensions |
Lists installed extensions with their status, version, and command counts.
## Extension Info
```bash
specify extension info <name>
```
Shows detailed information about an installed or available extension, including its description, version, commands, and configuration.
## Update Extensions
```bash
specify extension update [<name>]
```
Updates a specific extension, or all installed extensions if no name is given.
## Enable / Disable an Extension
```bash
specify extension enable <name>
specify extension disable <name>
```
Disable an extension without removing it. Disabled extensions are not loaded and their commands are not available. Re-enable with `enable`.
## Set Extension Priority
```bash
specify extension set-priority <name> <priority>
```
Changes the resolution priority of an extension. When multiple extensions provide a command with the same name, the extension with the lowest priority number takes precedence.
## Catalog Management
Extension catalogs control where `search` and `add` look for extensions. Catalogs are checked in priority order (lower number = higher precedence).
### Trust model: discovery-only vs. install sources
Catalogs come in two kinds, and the distinction is a **security boundary**, not a limitation:
- **Install sources** (`install_allowed: true`) — catalogs you trust as a place to install from. The built-in `default` (official) catalog is one, as is any catalog you author and vet yourself.
- **Discovery-only** catalogs (`install_allowed: false`) — searchable surfaces for *finding* extensions, but not installable. The built-in `community` catalog is discovery-only and is already active for `search` out of the box; you do not need to add it.
`community` is intentionally discovery-only because it is an open, unvetted list. Making everything in it one-command-installable would mean pulling arbitrary third-party code with no review.
> **Do not flip a discovery-only catalog to `install_allowed`.** That defeats the entire point of separating discovery from installation. There are two correct ways to install something you found via `community`:
>
> 1. **Install a single vetted extension directly** with `--from` (no catalog authoring needed). Get the candidate archive URL from `specify extension info <name>` — for a discovery-only entry it prints a "Candidate archive" URL. Review that release archive, then install it:
> ```bash
> specify extension info <name> # shows the candidate archive URL
> specify extension add <name> --from <archive-url>
> ```
> Treat the URL as untrusted until you have vetted it — it comes from an unvetted catalog.
> 2. **Curate your own catalog** you control and vet, and mark *that* catalog `install_allowed: true` — for when you want a governed, reusable install source (e.g. for an org).
### List Catalogs
```bash
specify extension catalog list
```
Shows all active catalogs in the stack with their priorities and install permissions.
### Add a Catalog
```bash
specify extension catalog add <url>
```
| Option | Description |
| ------------------------------------ | -------------------------------------------------- |
| `--name <name>` | Required. Unique name for the catalog |
| `--priority <N>` | Priority (default: 10; lower = higher precedence) |
| `--install-allowed / --no-install-allowed` | Mark the catalog as a trusted install source. Only enable for a catalog you own and vet; leave off (the default) for discovery-only sources. Never enable it for an unvetted public catalog. |
| `--description <text>` | Optional description |
Adds a catalog to the project's `.specify/extension-catalogs.yml`.
### Remove a Catalog
```bash
specify extension catalog remove <name>
```
Removes a catalog from the project configuration.
### Catalog Resolution Order
Catalogs are resolved in this order (first match wins):
1. **Environment variable**`SPECKIT_CATALOG_URL` overrides all catalogs
2. **Project config**`.specify/extension-catalogs.yml`
3. **User config**`~/.specify/extension-catalogs.yml`
4. **Built-in defaults** — official `default` catalog (install-allowed) + `community` catalog (discovery-only)
Example `.specify/extension-catalogs.yml` for a catalog you own and vet:
```yaml
catalogs:
- name: "my-org-catalog"
url: "https://example.com/catalog.json"
priority: 5
install_allowed: true
description: "Our approved extensions"
```
## Extension Configuration
Most extensions include configuration files in their install directory:
```text
.specify/extensions/<ext>/
├── <ext>-config.yml # Project config (version controlled)
├── <ext>-config.local.yml # Local overrides (gitignored)
└── <ext>-config.template.yml # Template reference
```
Configuration is merged in this order (highest priority last):
1. **Extension defaults** (from `extension.yml`)
2. **Project config** (`<ext>-config.yml`)
3. **Local overrides** (`<ext>-config.local.yml`)
4. **Environment variables** (`SPECKIT_<EXT>_*`)
To set up configuration for a newly installed extension, copy the template:
```bash
cp .specify/extensions/<ext>/<ext>-config.template.yml \
.specify/extensions/<ext>/<ext>-config.yml
```
## Project Extension and Hook Configuration
Spec Kit stores project-level extension registration and hook configuration in:
```text
.specify/extensions.yml
```
The file contains installed extensions, global settings, and hooks that are surfaced before or after Spec Kit commands.
```yaml
installed:
- git
- my-extension
settings:
auto_execute_hooks: true
hooks:
before_implement:
- extension: git
command: speckit.git.commit
enabled: true
optional: true
priority: 10
prompt: "Commit outstanding changes before implementation?"
description: "Auto-commit before implementation"
after_implement:
- extension: my-extension
command: speckit.my-extension.verify
enabled: true
optional: false
priority: 5
description: "Run verification after implementation"
```
### Configuration fields
The top-level `installed` list records extensions installed in the project. The `settings` mapping stores project-wide extension settings, and `hooks` groups hook registrations by event.
`auto_execute_hooks` defaults to `true`, but is currently reserved and is not consulted when hooks are surfaced or invoked.
Each hook entry supports the following fields:
| Field | Description |
| --- | --- |
| `extension` | ID of the extension that registered the hook. |
| `command` | Extension command associated with the hook. |
| `enabled` | Whether the hook is active. Hooks with `enabled: false` are skipped. |
| `optional` | Whether the hook is optional. If `true`, the hook is presented with its `prompt` and can be skipped; if `false`, the hook is emitted as an automatic hook (includes `EXECUTE_COMMAND` markers). |
| `priority` | Priority metadata for the hook. Registered hook entries use integer values >= 1; entries installed from manifests default to `10` when no priority is declared. Current command templates surface hooks in their configured YAML order and do not sort them by `priority`. |
| `prompt` | Message shown when asking whether to run an optional hook. |
| `description` | Human-readable explanation of what the hook does. |
| `condition` | Optional expression evaluated by `HookExecutor` (using `config.<path>` or `env.<VAR>` with `is set`, `==`, or `!=`). Current command templates do not evaluate conditions and skip hooks with a non-empty condition. |
Hook event names identify when a hook is invoked. They generally use `before_<command>` or `after_<command>`, such as `before_implement`, `after_implement`, `before_tasks`, and `after_tasks`.
Extension manifests reject invalid hook priorities during installation. For existing `.specify/extensions.yml` entries, `HookExecutor.get_hooks_for_event()` sorts with `normalize_priority()`: missing values, booleans, non-numeric values rejected by `int()`, and values less than `1` fall back to `10`; numeric strings and finite floats are coerced with `int()`, while non-finite floats are unsupported and may fail instead of falling back.
`HookExecutor.get_hooks_for_event()` returns hooks ordered by `priority`, with lower values first. However, current command templates read hook lists directly and surface them in their configured YAML order rather than using priority ordering.
## FAQ
### Why can't I find an extension with `search`?
Check the spelling of the extension name. The extension may not be published yet, or it may be in a catalog you haven't added. Use `specify extension catalog list` to see which catalogs are active.
### Why doesn't the extension command appear in my AI coding agent?
Verify the extension is installed and enabled with `specify extension list`. If it shows as installed, restart your AI coding agent — it may need to reload for it to take effect.
### How do I set up extension configuration?
Copy the config template that ships with the extension:
```bash
cp .specify/extensions/<ext>/<ext>-config.template.yml \
.specify/extensions/<ext>/<ext>-config.yml
```
See [Extension Configuration](#extension-configuration) for details on config layers and overrides.
### How do I resolve an incompatible version error?
Update Spec Kit to the version required by the extension.
### Who maintains extensions?
Most extensions are independently created and maintained by their respective authors. The Spec Kit maintainers do not review, audit, endorse, or support extension code. Review an extension's source code before installing and use at your own discretion. For issues with a specific extension, contact its author or file an issue on the extension's repository.