Back to publishing & release Automate semantic versioning Publish to the npm registry Harden the supply chain

Scoping GitHub Packages to a Single Organization

Consumers can install your @org/* packages from GitHub Packages only if their client is configured and authorized for that org. This page shows the exact configuration and the permission mistakes that block installs.

Exact symptoms and error messages

Installs of an org-scoped package fail with a 404 or 401 despite the package existing:

Exact symptoms and error messages Installs of an org-scoped package fail with a 404 or 401 despite the package existing: Exact symptoms and error messages Installs of an org-scoped package fail with a 404 or 401 despite the package existing:
Exact symptoms and error messages — the core idea of this section at a glance.
npm error 404 Not Found - GET https://npm.pkg.github.com/@org%2fwidgets
npm error 404  '@org/widgets@^1.0.0' is not in this registry.

Root cause analysis

GitHub Packages resolves @org/* only when the scope is mapped to npm.pkg.github.com and the token has read:packages plus access to the org. A 404 usually means the scope is not mapped (so npm asks the public registry, which does not have it); a 401 means the token lacks org access. This is the per-scope routing model from Private Registries and Access Control.

Root cause analysis GitHub Packages resolves @org/ only when the scope is mapped to npm.pkg.github.com and the token has read:packages plus Root cause analysis GitHub Packages resolves @org/ only when the scope is mapped to npm.pkg.github.com and the token has read:packages plus access to the org.
Root cause analysis — the core idea of this section at a glance.

The 404-not-401 behavior confuses people because it looks like the package is missing. What actually happens is a routing failure: if @org is not mapped to npm.pkg.github.com, npm sends the request to the default public registry, which has never heard of your private package and correctly returns 404. The package exists; npm just asked the wrong server. This is the per-scope routing model that makes private and public dependencies coexist in one install.

When the scope is mapped and you still get 404 or 401, the cause shifts to authorization: the token lacks read:packages, or the consuming account or repository has no access to the org's packages. GitHub ties package visibility to repositories and org membership, so a token that can read the source may still be unable to read the package unless it is granted package access explicitly.

Resolution and configuration patch

Resolution and configuration patch Grant the token (or the consuming repository, via permissions: packages: read) access to the org's packages, and set pac Resolution and configuration patch Grant the token (or the consuming repository, via permissions: packages: read) access to the org's packages, and set package visibility so member repositories c
Resolution and configuration patch — the core idea of this section at a glance.
# .npmrc
@org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}

Grant the token (or the consuming repository, via permissions: packages: read) access to the org's packages, and set package visibility so member repositories can resolve it.

For a consuming GitHub Actions workflow, grant the built-in GITHUB_TOKEN package read access rather than minting a personal token:

jobs:
  build:
    permissions:
      contents: read
      packages: read
    steps:
      - uses: actions/setup-node@v4
        with:
          registry-url: https://npm.pkg.github.com
      - run: npm ci
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The packages: read permission lets the job's token resolve org-scoped packages without a long-lived personal access token, which keeps the credential scoped to the job and expiring with it.

CLI validation and debug commands

CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows. CLI validation and debug commands CLI validation and debug commands in production JavaScript package workflows.
CLI validation and debug commands — the core idea of this section at a glance.
# Confirm the scope maps to GitHub Packages
npm config get @org:registry
# Verify token identity against the host
npm whoami --registry=https://npm.pkg.github.com
# Try the install with HTTP logging
npm install @org/widgets --loglevel=http

Prevention and CI guardrails

  • Map the org scope in a committed .npmrc (auth token referenced, not inlined).
  • Grant consuming workflows permissions: packages: read so GITHUB_TOKEN can resolve.
  • Set package visibility deliberately — internal packages should stay org-visible only.
  • Document the required token scopes so new consumers do not hit a silent 404.
Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows. Prevention and CI guardrails Prevention and CI guardrails in production JavaScript package workflows.
Prevention and CI guardrails — the core idea of this section at a glance.

Package visibility and who can install

GitHub Packages ties install access to package visibility and org membership, which is a different axis from repository access. A package can be private to the org, visible to selected repositories, or public, and a consumer resolves it only if their identity is on the right side of that setting. Getting a 404 or 401 despite a correct .npmrc usually means the visibility or grant is the missing piece, not the scope mapping.

Install access What determines whether an identity can install the package. Can this identity see the package? scope unmapped 404 from public no grant 401 / 403 org member + grant installs
Scope routing plus package visibility together decide who can install.

For internal packages, the goal is usually 'installable by any repo in the org, invisible outside it'. Set the package's visibility to the org, grant the consuming repositories access, and use GITHUB_TOKEN with packages: read in their workflows. External collaborators need an explicit grant and a token carrying read:packages. Thinking of it as two separate questions — is the scope routed to GitHub Packages, and is this identity allowed to see the package — keeps the debugging fast.

Publishing to the org registry from CI

Publishing an org-scoped package is the mirror image of consuming one: the scope must route to GitHub Packages, and the token needs write:packages plus access to the org. Doing it from CI with the job's token keeps a standing personal token out of the picture.

Org publish Route the scope, grant write, pin publishConfig. packages: write token scope publishConfig.registry pin the host npm publish org-scoped release
Publishing needs scope routing, write:packages, and a pinned publishConfig.
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/setup-node@v4
        with:
          registry-url: https://npm.pkg.github.com
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Setting publishConfig.registry in package.json pins the publish to GitHub Packages regardless of the local .npmrc, so a developer's default registry cannot accidentally push an internal package to the public index — a small guard that prevents an embarrassing leak.

Frequently Asked Questions

Why does the install 404 instead of 401?

Because the scope is not mapped to GitHub Packages, npm queries the public registry, which genuinely does not have your private package — so it returns 404 rather than an auth error.

Can external collaborators install org packages?

Only if granted access to the org's packages and using a token with read:packages. Package visibility and org membership govern who can resolve them.

Why do I get a 404 when the package clearly exists?

Because @org is not mapped to GitHub Packages, so npm asks the public registry, which does not have your private package and returns 404. Map the scope with @org:registry=https://npm.pkg.github.com and the request goes to the right host.

Can any repo in my org install the package?

Only if the package's visibility and grants allow it. Set the package visible to the org, grant consuming repositories access, and give their workflows packages: read. Repository access alone does not imply package access.

How do I stop an internal package leaking to the public registry?

Set publishConfig.registry to GitHub Packages in package.json so the publish is pinned there regardless of a developer's default registry, and keep the package's visibility scoped to the org.

Related

Private Registries and Access Control