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

Fixing npm 401 Unauthorized on a Private Registry

An install or publish against your private registry fails with 401 Unauthorized even though you 'logged in'. This page shows the exact error, the token and .npmrc mistakes that cause it, and how to authenticate correctly per scope.

Exact symptoms and error messages

Exact symptoms and error messages It appears on npm install for a scoped package or on npm publish to the private host. Exact symptoms and error messages It appears on npm install for a scoped package or on npm publish to the private host.
Exact symptoms and error messages — the core idea of this section at a glance.
npm error code E401
npm error 401 Unauthorized - GET https://npm.pkg.github.com/@acme%2fui
npm error Unable to authenticate, need: Basic realm="GitHub Package Registry"

It appears on npm install for a scoped package or on npm publish to the private host.

Root cause analysis

A 401 means the request reached the registry but carried no valid token for that host. The usual causes are an auth line keyed to the wrong host, a token without the required scope (read:packages / write:packages), or an unexpanded ${NODE_AUTH_TOKEN} because the variable is not set in the environment. Per-host auth is exactly the resolution model described in Private Registries and Access Control.

Root cause analysis A 401 means the request reached the registry but carried no valid token for that host. Root cause analysis A 401 means the request reached the registry but carried no valid token for that host.
Root cause analysis — the core idea of this section at a glance.

A 401 is specifically an authentication failure — the request reached the registry but carried no credential it accepted — which distinguishes it from a 404 (the scope was never mapped, so npm asked the wrong registry) and a 403 (authenticated but not authorized). Knowing which of the three you have narrows the fix immediately: 401 means the token or its host binding is wrong, not that the package or your permissions are.

The most common concrete cause is a mismatch between the auth line's host and the registry host. npm matches //host/:_authToken by exact host, including the trailing slash and any port, so //npm.pkg.github.com/ will not authenticate a request to https://npm.pkg.github.com if the stored key is subtly different. The second is an unexpanded ${TOKEN} because the environment variable is absent — the .npmrc then sends an empty token and the registry rejects it.

A 401 means the request reached the registry but carried no valid token for that host, which distinguishes it from a 404 (the scope was never mapped, so npm asked the wrong registry) and a 403 (authenticated but not authorized). The usual causes are an auth line keyed to the wrong host, a token without the required scope, or an unexpanded environment variable because it is not set. Because host matching is exact, most 401 confusion comes down to a mismatch between the auth line's host and the registry host.

The exact-match requirement is the subtle part. npm matches the //host/:_authToken line against the registry host precisely — including the trailing slash and any port — so a mapping to https://npm.pkg.github.com requires an auth line keyed to exactly //npm.pkg.github.com/. A subtly different key produces an anonymous request and a 401 that looks like a token problem but is really a host-binding problem, which is why reading the failing host carefully is the first diagnostic step.

Resolution and configuration patch

Match the auth line's host to the registry host exactly and provide a scoped token:

Resolution and configuration patch Match the auth line's host to the registry host exactly and provide a scoped token: Resolution and configuration patch Match the auth line's host to the registry host exactly and provide a scoped token:
Resolution and configuration patch — the core idea of this section at a glance.
# .npmrc
@acme:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
# Export a token with the right scopes before install
export NODE_AUTH_TOKEN=ghp_xxx   # read:packages (+ write:packages to publish)
npm install

When the token is correct but still rejected, confirm it carries the scopes the operation needs — reading a private package requires read:packages, publishing requires write:packages:

# Fail early in CI if auth is broken, before install runs
npm whoami --registry=https://npm.pkg.github.com || {
  echo 'registry auth failed'; exit 1;
}

Running whoami as an early step turns a confusing mid-install 401 into a clear, up-front failure that names the registry, which is far easier to debug than a stack of resolution errors.

Match the auth line's host to the registry host exactly, and provide a scoped token from the environment:

# .npmrc
@acme:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
# Export a token with the right scopes before install
export NODE_AUTH_TOKEN=ghp_xxx   # read:packages (+ write:packages to publish)
npm whoami --registry=https://npm.pkg.github.com   # verify before install
npm install

The whoami check confirms the token authenticates against the specific host before an install fails halfway through, turning a confusing mid-install 401 into a clear, up-front signal.

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 which registry a scope resolves to
npm config get @acme:registry
# Verify the token is present and valid
npm whoami --registry=https://npm.pkg.github.com
# Inspect the exact request npm makes
npm install --loglevel=http 2>&1 | grep 401

Prevention and CI guardrails

  • Key the _authToken line to the exact registry host, including the trailing slash.
  • Mint tokens with only the scopes the job needs (read:packages, write:packages).
  • Reference the token via ${NODE_AUTH_TOKEN} and inject it from secrets, never commit it.
  • Assert npm whoami --registry=<host> succeeds as an early CI step so auth fails fast.
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.
  • Key the _authToken line to the exact registry host, including the trailing slash.
  • Mint tokens with only the scopes the job needs (read:packages, write:packages).
  • Reference the token via an environment variable and inject it from secrets, never commit it.
  • Assert npm whoami --registry=<host> succeeds as an early CI step so auth fails fast.

401, 403, and 404 — reading the status code

The three failure codes point at three different fixes, and treating them interchangeably wastes hours. A 401 means the request was unauthenticated: the token is missing, wrong, or bound to the wrong host. A 403 means it authenticated but lacks permission: the token is valid but does not have access to that package or scope. A 404 usually means the scope was never mapped to the private registry, so npm queried the public one, which genuinely does not have your package.

Status code triage Which layer to fix based on the registry's status code. Which status code? 401 token / host binding 403 scopes / access 404 scope mapping
401 is the token, 403 is permissions, 404 is the scope mapping.

The diagnostic order follows from that. On a 401, check the _authToken host binding and that the environment variable expanded. On a 403, check the token's scopes and the account's access to the org or package. On a 404, check that @scope:registry points at the private host. Reading the code first — rather than blindly regenerating tokens — takes you to the right layer immediately.

Making registry auth fail fast in CI

Auth problems are cheapest to fix when they surface early and name the registry, so build that signal into the pipeline rather than letting a 401 emerge halfway through an install. A dedicated verification step, run before any install, converts an opaque failure into a clear one.

Fail-fast auth A whoami step verifies auth before install runs. whoami step before install names the registry clear failure fix the credential not guesswork
Verifying auth up front names the failing registry instead of erroring mid-install.
- name: Verify registry auth
  run: npm whoami --registry=https://npm.pkg.github.com
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

When this step fails, the log says exactly which registry rejected the credential, so you know immediately whether the problem is the token, its scopes, or the host binding — instead of inferring it from a resolution error three minutes into the job. It also documents, for the next engineer, exactly which token the pipeline expects and where.

Reading 401, 403, and 404 apart

The three failure codes point at three different fixes, and treating them interchangeably wastes time. A 401 means the request was unauthenticated: the token is missing, wrong, or bound to the wrong host. A 403 means it authenticated but lacks permission: the token is valid but does not have access to that package or scope. A 404 usually means the scope was never mapped to the private registry, so npm queried the public one, which does not have your private package.

Status code triage Which layer to fix based on the status code. Which status code? 401 token / host binding 403 scopes / access 404 scope mapping
401 is the token, 403 is permissions, 404 is the scope mapping.

The diagnostic order follows from that. On a 401, check the _authToken host binding and that the environment variable expanded. On a 403, check the token's scopes and the account's access to the organization or package. On a 404, check that the @scope:registry line points at the private host. Reading the code first — rather than blindly regenerating tokens — takes you to the right layer immediately, which is the difference between a five-minute fix and an hour of guessing. The status code is the registry telling you which of authentication, authorization, or routing failed, and the fix for each lives in a different part of the configuration.

Making registry auth fail fast in CI

Auth problems are cheapest to fix when they surface early and name the registry, so building that signal into the pipeline beats letting a 401 emerge halfway through an install. A dedicated verification step, run before any install, converts an opaque mid-install failure into a clear one that names the host and identity.

Making registry auth fail fast in CI Auth problems are cheapest to fix when they surface early and name the registry, so building that signal into the pipeli Making registry auth fail fast in CI Auth problems are cheapest to fix when they surface early and name the registry, so building that signal into the pipeline beats letting a 401 emerge halfway th
Making registry auth fail fast in CI — the core idea of this section at a glance.
- name: Verify registry auth
  run: npm whoami --registry=https://npm.pkg.github.com
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

When this step fails, the log says exactly which registry rejected the credential, so you know immediately whether the problem is the token, its scopes, or the host binding — instead of inferring it from a resolution error three minutes into the job. It also documents, for the next engineer, exactly which token the pipeline expects and where it comes from. Failing fast on auth is a small, cheap step that turns the most common private-registry failure from a confusing late error into an early, self-explanatory one, which is especially valuable in CI where the person debugging is often not the person who set up the credential.

Token scopes and the difference from a 403

A 401 that persists despite a token often comes down to the token's scopes not matching what the operation needs, which is where a 401 and a 403 start to blur. Reading a private package requires read:packages; publishing requires write:packages; and a token minted without the right scope is rejected. The distinction is that a 401 typically means the credential was not accepted at all — wrong host binding, unexpanded variable, or an invalid token — while a 403 means a valid, authenticated token lacks permission for the specific action.

Token scopes Which scope each operation needs. Operation Scope Note install read:packages read-only publish write:packages + read leaked token narrow scope bounded blast
Mint tokens with exactly the scopes the job needs — least privilege limits a leak.

In practice, the fix for a scope problem is to mint a token carrying exactly the scopes the job needs and no more, following least privilege. A CI job that only installs needs read:packages; a publish job needs write:packages too. Keeping tokens narrowly scoped limits the blast radius if one leaks and makes the permission model explicit — a read-only token cannot accidentally publish, and a token scoped to one organization cannot touch another. When auth fails, checking the token's scopes against the operation is part of the same early diagnosis as checking the host binding: verify with npm whoami that the token authenticates, then confirm it carries the scope the failing operation requires.

Frequently Asked Questions

Why does npm login not fix the 401?

npm login writes auth for the default registry. A scoped private host needs its own //host/:_authToken line; without it, requests to that host are anonymous and return 401.

The token works locally but 401s in CI — why?

The environment variable the .npmrc references is not set in the CI job, so ${NODE_AUTH_TOKEN} expands to empty. Inject it from the job's secrets with the correct scopes.

What's the difference between a 401 and a 403 here?

A 401 is an authentication failure — no valid credential reached the registry. A 403 means the credential authenticated but lacks permission for that package or scope. Fix the token/host binding for a 401; fix the token's scopes or access for a 403.

Why does the token work locally but 401 in CI?

The environment variable your .npmrc references is not set in the CI job, so ${NODE_AUTH_TOKEN} expands to empty and the registry rejects the anonymous request. Inject the token from the job's secrets with the required scopes.

How do I make a 401 easier to debug?

Add an early npm whoami --registry=<host> step. It fails fast and names the registry, turning a confusing mid-install error into a clear up-front signal about which credential is wrong.

What's the difference between a 401 and a 403 on a private registry?

A 401 is an authentication failure — no valid credential reached the registry, so the token is missing, wrong, or bound to the wrong host. A 403 means it authenticated but lacks permission for that package or scope. Fix the token/host binding for a 401; fix the scopes or access for a 403.

Why does my token work locally but 401 in CI?

The environment variable your .npmrc references is not set in the CI job, so it expands to empty and the registry rejects the anonymous request. Inject the token from the job's secrets with the required scopes, and verify with an early npm whoami step.

Why does a 401 persist even with a valid token?

Usually the _authToken line's host does not exactly match the registry host — npm matches including the trailing slash and port. Key the auth line to exactly the registry host, and confirm with npm whoami --registry=<host>.

Does a 401 mean my token has the wrong scopes?

Sometimes — a token minted without read:packages (to install) or write:packages (to publish) is rejected. But a 401 more often means the credential was not accepted at all (wrong host binding or unexpanded variable), while a scope/permission problem on a valid token is usually a 403. Verify with npm whoami, then check the scopes.

Related

Private Registries and Access Control