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:
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.
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.
GitHub Packages resolves an @org/* package only when the scope is mapped to npm.pkg.github.com and the token has read:packages plus access to the organization. A 404 usually means the scope is not mapped — so npm asks the public registry, which does not have your private package — while a 401 means the token lacks organization access. The confusion is that a missing scope mapping produces a 404 that looks like the package does not exist, when it exists but npm asked the wrong registry.
The two permission axes — whether an identity can see the source repository and whether it can install the package — are separate, which is the other common source of trouble. A token that can read your code may still be unable to read your package, because package visibility and organization membership govern install access independently of repository access. Framing every access problem as two questions — is the scope routed to GitHub Packages, and is this identity allowed to see the package — turns a class of baffling errors into a quick diagnosis.
Resolution and configuration patch
# .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.
Map the org scope and grant the consuming identity package-read access:
# .npmrc
@org:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
# consuming workflow: let GITHUB_TOKEN resolve org packages
permissions:
contents: read
packages: read
steps:
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Set the package's visibility so member repositories can resolve it, and the packages: read permission lets a workflow's built-in token install without a personal access token.
CLI validation and debug commands
# 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: readsoGITHUB_TOKENcan 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.
- Map the org scope in a committed
.npmrc(auth token referenced, not inlined). - Grant consuming workflows
permissions: packages: readsoGITHUB_TOKENresolves. - Set package visibility deliberately — internal packages stay org-visible only.
- Document the required token scopes so a new consumer does not hit a silent 404.
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.
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.
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.
Package visibility versus repository access
A frequent source of confusion with GitHub Packages is conflating two different permission axes: whether an identity can see the source repository, and whether it can install the package. They are separate. A package can be private to the organization, visible to selected repositories, or public, and a consumer resolves it only if their identity sits on the right side of that setting — regardless of their access to the source. A token that can read your code may still be unable to read your package.
For internal packages the usual goal is installable by any repository in the organization but invisible outside it, which means setting the package visibility to the organization, granting the consuming repositories access, and giving their workflows packages: read on the built-in token. External collaborators need an explicit grant and a token carrying the read scope. Thinking of every access problem as two questions — is the scope routed to GitHub Packages, and is this identity allowed to see the package — turns a baffling 404 or 401 into a mechanical diagnosis: the 404 is almost always the scope mapping, and the 401 is almost always the package grant.
Publishing to the organization registry from CI
Publishing an org-scoped package is the mirror of consuming one: the scope must route to GitHub Packages, and the token needs write:packages plus access to the organization. Doing it from CI with the job's built-in token keeps a standing personal token out of the picture, and pinning publishConfig.registry in the manifest ensures the publish goes to GitHub Packages regardless of a developer's default registry.
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 }}
The packages: write permission lets the workflow's token publish, and setting the registry on the setup step directs the publish to the org registry. For a monorepo publishing several packages, each package's repository.directory should point at its subdirectory so provenance and source links bind correctly. Publishing from CI this way makes the org-scoped release reproducible and keeps the credential scoped to the job rather than a long-lived personal token.
Consuming org packages from external collaborators
Org-scoped packages are straightforward for members but need explicit handling for external collaborators, who are not automatically granted package access. An outside contributor, a partner organization, or a CI system outside your GitHub organization needs a token carrying read:packages and an explicit grant to the specific packages or the organization's package visibility set to allow them. Without the grant, the same 401 or 404 appears that a misconfigured member would see, but the fix is a permission grant rather than a scope mapping.
The decision of how widely to grant access is a security one. Making a package's visibility broader than necessary — public when it should be org-only, or granted to more repositories than need it — expands the surface of who can pull your internal code. The principle is least privilege: grant package-read access to exactly the identities and repositories that need it, and no more. For an external collaborator, a scoped, expiring personal access token with only read:packages limits both what they can reach and how long the access lasts, which is the same short-lived, narrowly-scoped credential discipline that protects publishing — applied here to consumption.
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.
Why does my org-scoped install 404 when the package exists?
Because @org is not mapped to GitHub Packages, so npm queries 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 organization, grant consuming repositories access, and give their workflows packages: read. Repository access alone does not imply package access — they are separate axes.
How do I publish an org-scoped package from CI?
Grant the job packages: write, set the registry to npm.pkg.github.com on the Node setup step, and publish with the built-in GITHUB_TOKEN. Pin publishConfig.registry so the publish targets GitHub Packages regardless of a developer's default registry.
How do external collaborators install our org-scoped packages?
They need a token with read:packages and an explicit grant to the packages, since external identities are not automatically granted access. Provide a scoped, expiring personal access token limited to read:packages, following least privilege so access is narrow and time-limited.
Can I use GitHub Packages and the public npm registry together?
Yes — map only your org scope to GitHub Packages (@org:registry=...) and leave the default registry public. One install then resolves your private org packages from GitHub Packages and their public dependencies from npm, each verified against the lockfile.
Related
- Private Registries and Access Control — the overview for private distribution and access control.