* feat(auth): add github_provider_hosts() to enumerate GHES hosts from auth.json Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous) * fix(extensions): resolve GHES release assets via /api/v3 Generalizes resolve_github_release_asset_api_url to GitHub Enterprise Server hosts (gated by auth.json github hosts), fixing private GHES extension/preset downloads. github/spec-kit#3147 Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous) * fix(extensions,presets): pass auth.json github hosts into release resolver Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous) * docs(auth): document GHES private catalog + release-asset auth Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous) * fix(presets,workflows): pass auth.json github hosts into remaining release resolvers Wires preset add --from and workflow add through github_provider_hosts() so private GHES release assets resolve via /api/v3 there too. github/spec-kit#3147 Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous) * test(presets): use module-level io.BytesIO in GHES preset test Addresses Copilot review on PR #3157: drop unnecessary __import__("io") in test_preset_add_from_ghes_release_url_resolves_via_api_v3 since io is already imported at module level. * fix(github-http): pass through GHES asset API URLs by path shape Addresses Copilot review on PR #3157. A direct GHES /api/v3 release asset URL was only returned as already-resolved when its host was in the allowlist; otherwise the resolver returned None and the caller downloaded the same URL without 'Accept: application/octet-stream', fetching JSON metadata instead of the binary. Gate the passthrough on path shape alone, mirroring the github.com case. This is safe: passthrough returns the input URL unchanged and the caller fetches it either way, so no new request to an arbitrary host is induced; the token stays independently gated by auth.json in open_url. The allowlist remains the anti-SSRF gate on the tag-lookup resolving path. Add test_passthrough_for_unlisted_ghes_api_asset_url.
5.7 KiB
Authentication
Specify CLI uses opt-in authentication for HTTP requests to catalog sources, extension downloads, and release checks. No credentials are sent unless you explicitly configure them.
Configuration
Create ~/.specify/auth.json to enable authentication:
{
"providers": [
{
"hosts": ["github.com", "api.github.com", "raw.githubusercontent.com", "codeload.github.com"],
"provider": "github",
"auth": "bearer",
"token_env": "GH_TOKEN"
}
]
}
Security: Restrict the file to owner-only access:
chmod 600 ~/.specify/auth.json
Without this file, all HTTP requests are unauthenticated.
Fields
Each entry in the providers array has the following fields:
| Field | Required | Description |
|---|---|---|
hosts |
Yes | Array of hostnames this entry applies to. Supports exact hostnames, or a leading *. wildcard for subdomains only (for example, *.visualstudio.com). *.visualstudio.com matches foo.visualstudio.com, but not visualstudio.com. Other glob patterns such as *github.com or gith?b.com are not supported. |
provider |
Yes | Built-in provider key: github or azure-devops. |
auth |
Yes | Auth scheme (see below). |
token |
No | Token value (inline). Use token_env instead when possible. |
token_env |
No | Environment variable name to read the token from. |
For azure-ad auth, additional fields are required:
| Field | Required | Description |
|---|---|---|
tenant_id |
Yes | Azure AD tenant ID. |
client_id |
Yes | Service principal client ID. |
client_secret_env |
Yes | Environment variable containing the client secret. |
Either token or token_env must be set for bearer and basic-pat schemes.
Providers and auth schemes
GitHub (github)
| Scheme | Header | Use for |
|---|---|---|
bearer |
Authorization: Bearer <token> |
PATs, fine-grained PATs, OAuth tokens, GitHub App tokens |
Example — PAT via environment variable:
{
"hosts": ["github.com", "api.github.com", "raw.githubusercontent.com", "codeload.github.com"],
"provider": "github",
"auth": "bearer",
"token_env": "GH_TOKEN"
}
GitHub Enterprise Server (GHES)
To use a private catalog or extension hosted on a GitHub Enterprise Server
instance, add a github entry listing your GHES host(s). The same entry
authenticates both catalog JSON fetches and private release-asset
downloads — Specify recognizes the listed hosts as GitHub Enterprise and
resolves release downloads through the GHES REST API (/api/v3).
{
"providers": [
{
"hosts": ["ghes.example.com", "raw.ghes.example.com", "codeload.ghes.example.com"],
"provider": "github",
"auth": "bearer",
"token_env": "GH_ENTERPRISE_TOKEN"
}
]
}
List the bare web host (e.g. ghes.example.com) — release-download URLs
live there. If your instance uses subdomain isolation, also list the raw.
and codeload. subdomains your catalog/extension URLs use. A
*.ghes.example.com wildcard matches subdomains but not the bare host,
so always include the bare host explicitly.
Azure DevOps (azure-devops)
| Scheme | Header | Use for |
|---|---|---|
basic-pat |
Authorization: Basic base64(:<PAT>) |
Personal Access Tokens |
bearer |
Authorization: Bearer <token> |
Pre-acquired OAuth / Azure AD tokens |
azure-cli |
Authorization: Bearer <token> |
Token acquired via az account get-access-token |
azure-ad |
Authorization: Bearer <token> |
Token acquired via OAuth2 client credentials flow |
Example — PAT via environment variable:
{
"hosts": ["dev.azure.com"],
"provider": "azure-devops",
"auth": "basic-pat",
"token_env": "AZURE_DEVOPS_PAT"
}
Example — Azure CLI (interactive login):
{
"hosts": ["dev.azure.com"],
"provider": "azure-devops",
"auth": "azure-cli"
}
Requires az login to have been run beforehand.
Example — Azure AD service principal (CI/automation):
{
"hosts": ["dev.azure.com"],
"provider": "azure-devops",
"auth": "azure-ad",
"tenant_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"client_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"client_secret_env": "AZURE_CLIENT_SECRET"
}
Multiple entries
You can configure multiple entries for different hosts or organizations:
{
"providers": [
{
"hosts": ["github.com", "api.github.com", "raw.githubusercontent.com", "codeload.github.com"],
"provider": "github",
"auth": "bearer",
"token_env": "GH_TOKEN"
},
{
"hosts": ["dev.azure.com"],
"provider": "azure-devops",
"auth": "basic-pat",
"token_env": "AZURE_DEVOPS_PAT"
}
]
}
How it works
- For each outbound HTTP request, the URL hostname is matched against
the
hostspatterns inauth.json. - If a match is found, the corresponding provider resolves the token
and attaches the appropriate
Authorizationheader. - If the request receives a 401 or 403, the next matching entry is tried.
- After all matching entries are exhausted, an unauthenticated request is attempted as a final fallback.
- On redirects, the
Authorizationheader is stripped if the redirect target leaves the entry's declared hosts — preventing credential leakage to CDNs or third-party services.
Template
A reference auth.json with GitHub pre-configured:
{
"providers": [
{
"hosts": [
"github.com",
"api.github.com",
"raw.githubusercontent.com",
"codeload.github.com"
],
"provider": "github",
"auth": "bearer",
"token_env": "GH_TOKEN"
}
]
}
To use it:
mkdir -p ~/.specify
# Copy the JSON above into ~/.specify/auth.json
chmod 600 ~/.specify/auth.json