Skip to content

feat: extension lifecycle hook for post-install setup #3359

Closed

Description

@rhuss

This builds on the discussion in #3303. The conclusion there was that agent-specific command specialization belongs in userland (presets, extensions) rather than in a core capability matrix, and I agree with the materialization invariant argument. What I'd like to propose here is the minimal plumbing that would make that userland approach practical.

The problem

Extensions that target multiple agent harnesses need to adapt their commands at install time. Today, the consuming project's init script has to centralize all that logic: detect the harness, then run per-extension, per-harness setup (select presets, adjust configuration, copy adapter scripts). This doesn't scale. Every new extension means more branches in the project's init script, maintained by someone who didn't write the extension.

The extension author is the right person to own that adaptation, but specify extension add currently gives them no hook to run it.

Proposal

Add an optional on_install lifecycle hook to extension.yml:

extension:
  id: spex-teams
  # ...existing fields...

hooks:
  on_install:
    script: scripts/on-install.sh
    description: "Select agent-appropriate preset for subagent dispatch"

When specify extension add processes an extension that declares on_install, it invokes the script after the extension is installed into .specify/extensions/<id>/. The script runs with the extension's directory as context and can do whatever it needs: select a preset, skip unsupported commands, copy adapter files, adjust configuration.

Harness context

The script needs to know which harness it's adapting for. Two options, not mutually exclusive:

  1. Extension detects it. The script runs its own detection logic (environment variables, directory presence, init-options). This works today, no core changes needed beyond the hook itself.

  2. specify passes it. If the active integration is already known at install time (from specify init --integration <name> or the stored .specify/init-options.json), specify could pass it as an environment variable (SPECKIT_INTEGRATION=claude) or a script argument. This would be a convenience, not a requirement. The extension could still override or extend the detection.

Option 2 is cleaner since specify already knows the integration. But the hook is useful even with just option 1.

What the hook would NOT do

This deliberately avoids the capability matrix from #3303. The hook doesn't introduce conditional template blocks, capability keys, or any shared vocabulary between core and extensions. The result is still fully materialized, auditable, self-contained project files. The hook is pure plumbing: "run this script after install."

Use case: cc-spex

cc-spex has 7 extensions with 30 commands. 24 are portable as-is. 6 need agent-specific adaptation (subagent dispatch patterns, interactive prompt tools). Today, spex-init.sh centralizes all of this in a 400-line script. With on_install, each extension would own its own adaptation:

  • spex-teams installs its Claude Code subagent preset or falls back to sequential execution
  • spex-deep-review selects the right dispatch section for the detected harness
  • spex core adjusts ship pipeline for available parallelism

The init script shrinks to just specify extension add calls. Each extension is self-contained.

Minimal implementation

  1. Parse on_install from extension.yml during specify extension add
  2. After installation, invoke the script with:
    • Working directory: the extension's installed path
    • Environment: SPECKIT_INTEGRATION (if known), SPECKIT_PROJECT_ROOT
  3. Non-zero exit from the script fails the installation (with the script's stderr as the error message)

An on_uninstall counterpart would be natural but isn't required for the initial implementation.

Activity

  1. mnriem commented on Jul 6, 2026

    @mnriem
    Collaborator

    Thanks for the detailed write-up, and for separating this cleanly from the capability-matrix discussion in #3303.

    I want to push back on the need for a new on_install hook, because I think everything here is already achievable with existing primitives — and the workflow engine is a natural fit for this.

    This can already be accomplished with a workflow. The pain you describe — a 400-line spex-init.sh that centralizes per-extension, per-harness setup — is something a workflow can express directly. The engine already ships shell, command, switch/if, and init step types, resumable state, gates, and an expression engine with integration: "auto" resolution. Adaptation logic that today lives in an imperative init script becomes declarative, reviewable steps:

    inputs:
      integration: { type: string, default: "auto" }
    
    steps:
      - { id: add-teams,  type: shell, run: "specify extension add spex-teams" }
      - { id: add-review, type: shell, run: "specify extension add spex-deep-review" }
    
      - id: adapt-teams
        type: switch
        expression: "{{ inputs.integration }}"
        cases:
          claude:
            - { type: shell, run: ".specify/extensions/spex-teams/adapt/claude.sh" }
        default:
          - { type: shell, run: ".specify/extensions/spex-teams/adapt/sequential.sh" }

    This hits your core goal directly: the author owns the adaptation, and the consuming project's init script shrinks to nothing.

    It ships with the extensions. cc-spex's real distribution unit is a bundle, and bundles already declare provides.workflows alongside provides.extensions (see examples/bundles/developer/bundle.yml). So the setup workflow travels with the extensions — no separate maintenance, no central branching.

    And the one-command install you're reaching for already exists. specify workflow run natively accepts an HTTPS URL — it downloads the YAML, enforces HTTPS-only (HTTP only for loopback), resolves GitHub release assets, and re-validates the scheme across redirects. So the whole thing is:

    specify workflow run https://raw.githubusercontent.com/rhuss/cc-spex/main/spex-setup.yml

    That single command installs every extension, branches on the detected integration, runs the per-harness adapters, and is resumable — with zero new extension.yml schema.

    Mapping it against the proposal:

    Need from the issue Existing solution
    Author owns per-harness adaptation workflow shell + switch
    Ships with the extensions bundle provides.workflows
    One-command install specify workflow run <https-url>
    specify passes the integration inputs.integration / auto

    The only thing on_install adds on top of this is implicit execution during specify extension add — and that implicitness is exactly the cost. Today the CLI executes zero extension scripts; add is a materialization step, not a code-execution step. An on_install hook would have specify extension add silently subprocess arbitrary author shell on every install, which is a meaningfully larger trust surface than the current model (where author code is either surfaced to the agent the user is watching, or run explicitly via a workflow the user invoked). It also drags in an on_uninstall counterpart, cross-platform bash+powershell pairs, and containment rules for what the script may touch.

    With the workflow approach, the same shell invocations live in fetched-and-validated, reviewable YAML that the user runs knowingly, rather than a hidden side effect of add.

    So I'd lean toward not adding the hook, and instead documenting specify workflow run <url> (bundled workflow) as the supported pattern for post-install, agent-specific setup. Happy to help sketch a reference spex-setup.yml if that'd be useful.

  2. rhuss commented on Jul 11, 2026

    @rhuss
    ContributorAuthor

    Thanks for the greate feedback (and also the pushback). After some meandering, I think spec-kit workflows are the right solution to get cc-spex agent-agnostic. Let's close this issue.

    Really appreciate the insights and also the pushback to prevent spec-kit to become bloated with unneeded features. It's too easy to throw over something without having the full context of the project. I'm happy that there is still a human in the loop :-) 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions