Skip to content

apm marketplace add fails to authenticate when fetching marketplace.json from private repositories #669

@jacobokeeffe-ow

Description

@jacobokeeffe-ow

Summary

apm marketplace add cannot register a private GitHub repository as a marketplace. It reports "No marketplace.json found" despite the file existing at the repo root, because the command does not apply authentication when fetching the file. Other commands like apm install authenticate correctly against the same private repo.

Environment

  • APM CLI version: 0.8.11
  • OS: macOS (darwin, arm64)
  • Auth: gh auth login active, token available via git credential fill, GITHUB_TOKEN, and GITHUB_APM_PAT

Steps to reproduce

  1. Have a private GitHub repository with a valid marketplace.json at the root
  2. Ensure authentication is configured (any of: GITHUB_TOKEN, GITHUB_APM_PAT, GITHUB_APM_PAT_{ORG}, or git credential fill)
  3. Run:
GITHUB_TOKEN=ghp_valid_token apm marketplace add OWNER/REPO -v

Expected behavior

APM uses the available token to fetch marketplace.json from the private repo and registers the marketplace.

Actual behavior

[*] Registering marketplace 'REPO'...
    Repository: OWNER/REPO
    Branch: main
[x] No marketplace.json found in 'OWNER/REPO'. Checked:
marketplace.json, .github/plugin/marketplace.json,
.claude-plugin/marketplace.json

Evidence that auth works for other commands

The same token and repo work fine with apm install:

GITHUB_TOKEN=ghp_valid_token apm install OWNER/REPO --dry-run
# [+] OWNER/REPO — validates successfully

And the file is confirmed to exist via the GitHub API:

gh api repos/OWNER/REPO/contents/marketplace.json
# Returns valid marketplace.json content

Tokens tested (all fail identically)

  • GITHUB_TOKEN
  • GITHUB_APM_PAT
  • GITHUB_APM_PAT_{ORG}
  • git credential fill (confirmed working)

Root cause analysis

We traced this through the source code. The bug is a semantic mismatch between "file not found" and "repo not accessible" in _fetch_file().

The auth fallback doesn't trigger on 404

_fetch_file() in src/apm_cli/marketplace/client.py:138-176 uses the GitHub Contents API with unauth_first=True:

return auth_resolver.try_with_fallback(
    source.host,
    _do_fetch,
    org=source.owner,
    unauth_first=True,    # <--- tries without token first
)

The fallback mechanism in AuthResolver.try_with_fallback() (auth.py:278-290) only escalates to an authenticated retry on exceptions:

if unauth_first:
    try:
        return operation(None, git_env)          # 1. Try with NO token
    except Exception:
        if auth_ctx.token:
            return operation(auth_ctx.token, git_env)  # 2. Retry WITH token
        raise

But _do_fetch handles HTTP 404 by returning None instead of raising:

# client.py:158-159
if resp.status_code == 404:
    return None

For private repos, GitHub returns 404 (not 403) to avoid leaking repo existence. Since _do_fetch returns None rather than raising an exception, the auth fallback never fires. The None propagates back and APM concludes the file doesn't exist — when it actually never tried with a token.

Why apm install works with the same private repo

apm install uses GitHubPackageDownloader which:

  • Resolves tokens eagerly per-dependency via _resolve_dep_token()
  • Uses git clone with the token embedded in the HTTPS URL
  • Tries authenticated first (auth → SSH → unauth) — the inverse of marketplace's order
  • Git raises actual exceptions on failure, so the fallback chain works correctly
Aspect marketplace add apm install
Protocol GitHub REST API (Contents) git clone over HTTPS
Auth strategy unauth_first=True Auth-first (token in URL)
Fallback direction unauth → auth → credential-fill auth → SSH → unauth
404 handling Returns None (no retry) Git raises exception (triggers fallback)

Suggested fixes (in order of preference)

  1. Make _do_fetch raise on 404 during the unauthenticated phase, so try_with_fallback escalates to the authenticated retry. Only treat 404-with-a-valid-token as "file not found." This preserves the rate-limit optimization for public repos while fixing private repos.

  2. Use unauth_first=False for the marketplace fetch — try authenticated first, like apm install does. Simplest change but uses a token request even for public repos.

  3. Two-phase approach: first verify repo accessibility (GET /repos/{owner}/{repo}), then probe for files.

Workaround

Install plugins directly using subdirectory syntax, bypassing the marketplace:

apm install OWNER/REPO/plugin-subdir

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedDirection approved, safe to start workbugSomething isn't workinggood first issueGood for newcomers

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions