Skip to content

Verified against pnpm 10, 11 and 12.

Quick start

Terminal window
export VLT_TOKEN=<your token>
pnpm config set registry https://registry.vlt.io/acme/npm/ --location=project
pnpm config set @acme:registry https://registry.vlt.io/acme/main/ --location=project

Then add auth by logging in through the browser:

Terminal window
pnpm login --registry=https://registry.vlt.io/acme/npm/
pnpm login --registry=https://registry.vlt.io/acme/main/ --scope=@acme

Now pnpm install works.

The config file

pnpm reads registry and auth settings from .npmrc, same keys as npm. Commit the routing only, with no credentials:

registry=https://registry.vlt.io/acme/npm/
@acme:registry=https://registry.vlt.io/acme/main/

Registry and auth keys belong in .npmrc; every other pnpm setting now lives in pnpm-workspace.yaml.

${VLT_TOKEN} does not work in a project .npmrc

Unlike npm, yarn, bun and deno, pnpm deliberately ignores ${...} placeholders in a repository .npmrc for registry, proxy and credential values. A file containing //registry.vlt.io/acme/npm/:_authToken=${VLT_TOKEN} sends no Authorization header at all — you get a 401 that looks like a bad token, with no warning explaining why.

This is a supply-chain fix (a malicious repo could otherwise redirect your token to a registry it controls), landed in pnpm 11.5.3, so it applies to every current version. Use one of the two options below instead.

Which option is available depends on the version, and there is one unlucky release with neither:

pnpm version${VLT_TOKEN} in .npmrcpnpm_config_… env var
11.5.2 and olderworksnot supported
11.5.3blockednot supported
11.6.0 – 11.26.0blockedworks
12.xblockedworks

So on the pinned 11.26.0 above, use pnpm login or the environment variable — never a placeholder in the file.

Terminal window
pnpm login --registry=https://registry.vlt.io/acme/npm/
pnpm login --registry=https://registry.vlt.io/acme/main/ --scope=@acme

Browser-based, and the credential is stored in your global pnpm config rather than in the repo.

pnpm accepts any config key as a pnpm_config_-prefixed variable, which sidesteps the placeholder restriction entirely:

Terminal window
env "pnpm_config_//registry.vlt.io/acme/npm/:_authToken=$VLT_TOKEN" pnpm install

Two notes:

  • Use env rather than export — the variable name contains /, : and ., which the shell will not accept in an assignment.
  • Needs pnpm 11.6.0 or newer. On older versions the variable is ignored and you get ERR_PNPM_FETCH_401 … No authorization header was set for the request.

Install from the npm mirror

Terminal window
pnpm add abbrev

To move an existing project onto the mirror:

Terminal window
rm -rf pnpm-lock.yaml node_modules
pnpm install

Install from your main registry

Terminal window
pnpm add @acme/my-package

Public packages

Public packages on main install without a token — the registry line alone is enough, with no auth line:

@acme:registry=https://registry.vlt.io/acme/main/

Publish to main

{
"name": "@acme/my-package",
"version": "1.0.0",
"publishConfig": {
"registry": "https://registry.vlt.io/acme/main/"
}
}
Terminal window
pnpm publish

publishConfig.registry decides the destination, and in pnpm it is the strongest setting — it beats @acme:registry and even an explicit --registry flag, which is the weakest of the four. (npm resolves these in a different order, so don’t assume a publish script behaves identically under both.) Add --access public for a package anyone can install.

Two flags you will probably want:

  • --no-git-checks — pnpm refuses to publish unless the git tree is clean and you are on the publish branch. Verified: an uncommitted file fails with ERR_PNPM_GIT_UNCLEAN.
  • --dry-run — pack and report without uploading.

With a personal token, every publish returns a one-time-password challenge with a vlt.io URL — open it, approve, re-run. A service token skips the prompt and is what you want in CI; see CI & automation.

Log in

Terminal window
pnpm login --registry=https://registry.vlt.io/acme/main/ --scope=@acme

This uses the browser login flow and prints an authentication URL. --scope also records the scope-to-registry mapping for you. In current pnpm the credential is written to your global pnpm config, not to .npmrc.

pnpm token is not implemented in pnpm — it reports Not implemented in pnpm. Use the npm CLI directly. Manage tokens at vlt.io or with npm token.

Gotchas

  • pnpm 12 + the npm mirror is broken. See the note at the top.
  • ${VAR} in a project .npmrc is ignored, silently. See above.
  • pnpm config set writes to your machine-global config by default. Always pass --location=project, or credentials end up in your user config unexpectedly. --location accepts only project and global — there is no user.
  • always-auth no longer exists in pnpm. Setting it produces an unrecognized-setting warning and does nothing; it isn’t needed.
  • Trailing slashes must match between the registry URL and the auth line.
  • --npmrc-auth-file (alias --userconfig) points pnpm at a different .npmrc for auth. That file is treated as trusted, so ${VAR} expansion does work there — only use it in environments that build trusted repositories.