Skip to content

[Feature]: Support GITHUB_TOKEN authentication for private catalog and extension download URLs #2037

@userhas404d

Description

@userhas404d

Description

When using specify extension commands with a catalog hosted in a private GitHub repository, all network operations fail with HTTP 404 because the CLI uses unauthenticated urllib.request.urlopen for both catalog fetches and extension ZIP downloads.

This affects:

  • specify extension search — fetching catalog JSON from raw.githubusercontent.com
  • specify extension add — downloading extension ZIP from GitHub release assets
  • SPECKIT_CATALOG_URL — same issue when pointing to a private repo

Current Behavior

# Catalog hosted in a private repo
$ specify extension catalog add \
    "https://raw.githubusercontent.com/my-org/my-repo/main/speckit-extensions/catalog.json" \
    --name internal --install-allowed

$ specify extension search jira
# Warning: Could not fetch catalog 'internal': Failed to fetch catalog from
# https://raw.githubusercontent.com/my-org/my-repo/main/speckit-extensions/catalog.json:
# HTTP Error 404: Not Found

# Same with SPECKIT_CATALOG_URL
$ SPECKIT_CATALOG_URL="https://raw.githubusercontent.com/my-org/my-repo/main/speckit-extensions/catalog.json" \
    specify extension search jira
# HTTP Error 404: Not Found

# Setting GITHUB_TOKEN or GH_TOKEN has no effect
$ GITHUB_TOKEN=$(gh auth token) specify extension search jira
# HTTP Error 404: Not Found

Expected Behavior

When GITHUB_TOKEN or GH_TOKEN is set in the environment, speckit should include an Authorization: token <value> header on requests to GitHub-hosted URLs (raw.githubusercontent.com, github.com, api.github.com). This would enable organizations to host private extension catalogs without workarounds.

# Should work with token in environment
$ GITHUB_TOKEN=$(gh auth token) specify extension search jira
# Found 1 extension(s): ...

$ GITHUB_TOKEN=$(gh auth token) specify extension add jira-sync
# ✓ Extension installed successfully!

Suggested Implementation

The change is small — in the _fetch_single_catalog and download_extension methods of extensions.py, add auth headers when a recognized env var is present and the URL is a GitHub domain:

import urllib.request

headers = {}
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
if token and any(
    host in url
    for host in ("raw.githubusercontent.com", "github.com", "api.github.com")
):
    headers["Authorization"] = f"token {token}"

req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=10) as response:
    ...

Workarounds

Currently the only options for private repos are:

  1. --dev from local clonespecify extension add /path/to/local/extension --dev (requires repo cloned locally)
  2. Localhost proxy — download catalog/zips via gh CLI, rewrite URLs to localhost, serve with python3 -m http.server, then point SPECKIT_CATALOG_URL at localhost
  3. --from with local servegh release download + localhost HTTP server + specify extension add ext --from http://localhost:PORT/ext.zip

None of these support the native specify extension searchspecify extension add workflow that public catalogs enjoy.

Use Case

Organizations hosting internal extension catalogs in private GitHub repositories. The extension-catalogs.yml multi-catalog feature (added in #1707) already supports adding custom catalog URLs — this feature request completes the story by making those catalogs work when the source repo is private.

Environment

  • speckit CLI: specify-cli (installed via uv tool)
  • OS: macOS
  • Auth available: gh auth token returns valid PAT with repo scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions