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.

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.

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.

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.

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.

Related

Private Registries and Access Control