Summary
Since 2026-10-21, pnpm on Vercel builds no longer sends our npm auth token for registry.npmjs.org, so installs of private scoped packages fail with a 404. The token is valid and pnpm reads it. The build container now runs an npm-proxy process on 127.0.0.1:8402. Inside the pnpm process, registry.npmjs.org resolves to that proxy host when pnpm looks up credentials. The token is keyed to //registry.npmjs.org/, so the lookup misses, pnpm sends the request with no Authorization header, and npm returns 404 for the private tarball.
Writing the same token a second time, keyed to //127.0.0.1:8402/, fixes it. We have shipped that as a workaround, but it depends on an undocumented port.
Environment
- Two projects on our team, both affected.
- Build image: Amazon Linux 2023 x86_64, Node v24.22.1 (
/node24), Vercel CLI 65.1.0 in the build log. - pnpm 11 through corepack (
ENABLE_EXPERIMENTAL_COREPACK=1withpackageManagerpinned inpackage.json). Two different pnpm 11 minor versions fail the same way. - The install environment has
VERCEL_LOCAL_NPM_PROXY=1andVERCEL_ENABLE_PNPM_VERIFICATION_CACHE=1. We did not set either. - The private packages are scoped packages on the public npm registry. The token comes from a team shared env var.
Install command:
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc && pnpm install --frozen-lockfileWhat fails
[ERR_PNPM_FETCH_404] GET https://registry.npmjs.org/@<scope>/<package>/-/<package>-<version>.tgz: Not Found - 404
No authorization header was set for the request.pnpm whoami in the same build fails with ERR_PNPM_WHOAMI_UNAUTHORIZED. pnpm only throws that when it has no header to send. npm whoami gives ENEEDAUTH, so npm doesn't find the token either.
Builds that restore the build cache still pass, because pnpm takes the package from the restored store and never fetches it. We only noticed once a new package version, or an uncached redeploy, forced a real fetch.
Timeline
- 2026-10-21 05:36 UTC: last build that fetched a private package successfully. The build log shows Vercel CLI 64.5.0.
- 2026-10-21 11:21 UTC: first failure. The build log shows Vercel CLI 64.7.0.
Nothing in our repos or the env var changed between those two builds, and re-saving the env var didn't help.
Evidence
We added diagnostics to the install command on a throwaway branch. They printed token lengths and booleans, never the token itself.
-
The token is valid inside the build. With
Authorization: Bearer <token>,curl https://registry.npmjs.org/-/whoamireturns 200, and so does the private tarball URL. The tarball returns 404 without the header. -
pnpm reads the token. A
fs.readFileSynchook shows pnpm reading the.npmrcwith the token on the auth line. pnpm's resolved config holdsconfigByUri["//registry.npmjs.org/"]["@"].authTokenas a non-empty string of the right length. -
The credential lookup key is wrong. We patched a copy of pnpm's
whoamihandler to log its steps:[dbg] creds //registry.npmjs.org/ @ authToken=string(40,npm_=true,ws=false)[dbg] registryUrl=https://registry.npmjs.org/ nerf=//127.0.0.1:8402/[dbg] authHeaderValueByURI keys=["//registry.npmjs.org/"] scoped=[][dbg] header=undefined startsBearer=false
nerfDart (from @pnpm/config.nerf-dart) only parses the URL with new URL() and returns //host/path. Given https://registry.npmjs.org/, it returned //127.0.0.1:8402/ on Vercel. The same patched pnpm in a local amazonlinux:2023 container with Node 24.22.1 returns //registry.npmjs.org/, sends the header, and gets the expected 401 for a dummy token.
4. The proxy is running. ss -ltnp in the build shows:
LISTEN 0 4096 127.0.0.1:8402 0.0.0.0:* users:(("npm-proxy",pid=47,fd=4))- A plain Node process in the same build is not affected.
node -ewithnew URL("https://registry.npmjs.org/").hostprintsregistry.npmjs.org, andNODE_OPTIONSonly holds--max_old_space_size=8192. The rewrite only shows up inside pnpm. We could not find what does it. - Unsetting
VERCEL_LOCAL_NPM_PROXY,VERCEL_ENABLE_PNPM_VERIFICATION_CACHE, or both, before running pnpm changes nothing. - The binaries are stock. Node, corepack and corepack's copy of pnpm all match the official release hashes.
- The workaround works. After appending
//127.0.0.1:8402/:_authToken=$NPM_TOKENto~/.npmrc,pnpm whoamiprints our npm user, and the uncached install fetches the private package and the build completes.
Workaround in place
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc && echo "//127.0.0.1:8402/:_authToken=$NPM_TOKEN" >> ~/.npmrc && pnpm install --frozen-lockfileQuestions
- Is the npm proxy meant to change which host pnpm uses for credential lookup? Credentials the user configured for
registry.npmjs.orgshould still reachregistry.npmjs.org. - Will port 8402 stay the same? Our workaround hardcodes it.
- Can a project opt out of the proxy? Unsetting
VERCEL_LOCAL_NPM_PROXYin the install command doesn't do it. - Are other customers installing private npm packages with pnpm hitting this? It's easy to miss, because cached builds keep passing.