# pnpm and Windows broke this blog's build twice — a shadowed Chocolatey install, then a blocked symlink

> pnpm's version switcher corrupted its cache behind a shadowed Chocolatey install, and Windows separately blocked a build from symlinking its dependencies.

- Source: https://oharu121.com/blog/pnpm-version-switcher-enoent-chocolatey-shadowed-path/
- Published: 2026-08-22T12:05:09+09:00
- Updated: 2026-08-23T04:54:40+09:00
- Tags: pnpm, Developer Tooling

---
**Key takeaways**

- pnpm's own self-managed version switcher, triggered by the `packageManager` field in `package.json`, can fail partway through downloading its pinned version and leave itself unable to run at all, on any command.
- This is not a fluke or a stale-install problem. Multiple open pnpm GitHub issues report the identical ENOENT failure from completely unrelated environments: Docker images, CI runners, other people's machines.
- Reinstalling pnpm fixes the broken cache, but the fix can look incomplete if a second, forgotten install of the same tool sits earlier in a shell's PATH.
- `which pnpm` had already surfaced that second install in the very first diagnostic pass. It just was not the line that looked broken, so it got read past.
- Putting the working install ahead in `PATH` fixed the shell it ran in and nothing past it. The identical error came back in a fresh shell the next day, because the Chocolatey install was still there, only outranked, not removed.
- A separate build later crashed with `EPERM` on a symlink, because Windows blocks symlink creation for a standard account by default. Developer Mode fixes it, and no elevated shell is needed.

## Introduction

While normalizing a new thumbnail image for a different article in this repo, `pnpm thumbnails:fix` broke completely: not a normal command failure, but pnpm refusing to run at all, on any command, in any shell. That blocked everything downstream, including the verification gates this blog's own pipeline requires before a draft counts as checked. The obvious first guess was a stale or broken install, and that guess was wrong in an instructive way: **the actual cause was pnpm's own self-managed version-switching feature failing to finish installing itself**, compounded by a second, forgotten pnpm install sitting earlier in one shell's PATH than the copy that had just been repaired. This article covers what that self-switch failure looks like, the evidence that it is a known upstream bug rather than anything specific to this machine, how the shadowed install turned a one-line fix into a multi-step diagnosis, why that fix did not survive into the next session, and a second, unrelated Windows failure that hit later on the same machine while publishing a different article: a build blocked from symlinking its own dependencies.

## The switcher broke itself

`pnpm thumbnails:fix` failed before running a single line of the underlying script:

```
[WARN] Unsupported engine: wanted: {"node":">=24"} (current: {"node":"v22.12.0","pnpm":"11.3.0"})
 ERROR  Failed to switch pnpm to v11.3.0. Looks like pnpm CLI is missing at "C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin" or is incorrect
spawnSync C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin\pnpm ENOENT
```

This repo's `package.json` pins `"packageManager": "pnpm@11.3.0"`. Modern pnpm reads that field itself and, if the exact pinned version is not already sitting in its per-user tool cache, downloads and installs it there on the fly before re-executing into it. **That cache lives at `PNPM_HOME/.tools/@pnpm+win-x64/<version>/`, and this is pnpm managing its own version, not a separate tool like Corepack doing it.**

The cache folder for `11.3.0` existed. It just did not contain a `bin` subfolder:

```
total 15
drwxr-xr-x 1 <user> 197609   0 Aug 22 10:11 .
drwxr-xr-x 1 <user> 197609   0 Aug 22 10:11 ..
drwxr-xr-x 1 <user> 197609   0 Aug 22 10:11 node_modules
-rw-r--r-- 1 <user> 197609  43 Aug 22 10:11 package.json
-rw-r--r-- 1 <user> 197609 438 Aug 22 10:11 pnpm-lock.yaml
-rw-r--r-- 1 <user> 197609  39 Aug 22 10:11 pnpm-workspace.yaml
```

**Everything a self-install writes before placing the executable was there; the one file that makes the install usable was not.** Retrying `pnpm --version` reproduced the identical error, which ruled out a one-off network blip: the self-managed switcher had left its own cache in a state it could not recover from on its own, and every subsequent pnpm invocation, regardless of which physical pnpm binary triggered it, hit the same missing path.

## Not a fluke, and not this machine's fault

The instinct at this point was to blame the install itself. A quick search before acting on that instinct changed the diagnosis: **this exact failure, word for word, shows up across environments that share nothing with each other.** Renovate's Docker images, Render.com deployments, and a handful of standalone projects all report the identical `Failed to switch pnpm to v<X>. Looks like pnpm CLI is missing... ENOENT` message, tracked in open pnpm issues going back across several point releases.

**pnpm's self-managed version switcher is the fragile part, not any particular way of installing pnpm.** A Docker image, a CI runner, and a personal Windows machine share no install method, no OS, and no history, yet they hit the same failure with the same wording, because they all share the one thing that matters: the switcher's own logic for downloading and unpacking a pinned version.

## The fix, and the shadow it didn't clear

When the agent started experimenting with environment-variable workarounds to route around the broken state without explaining them first, I stopped it and asked why. That question changed the direction of the fix: instead of working around the broken pnpm, I asked the agent to reinstall pnpm properly, and approved running the official installer directly:

```powershell
iwr https://get.pnpm.io/install.ps1 -useb | iex
```

That produced a fresh shim and a clean `PNPM_HOME`. In a PowerShell session with the updated `PATH` loaded, `pnpm --version` printed `11.3.0` cleanly. The fix worked, in that shell.

`pnpm install`, run next in a separate Bash session, failed with the **exact same** ENOENT error, referencing the exact same broken cache path, as if nothing had been reinstalled at all. The reflex at that point is to suspect the reinstall did not take, or that the cache had broken itself a second time. Neither was true:

```bash
which pnpm
# /c/ProgramData/chocolatey/bin/pnpm
```

Bash was not running the pnpm that had just been fixed. It was running a **completely different, older install via Chocolatey**, sitting earlier in Bash's `PATH` than the fresh one, and old enough to still be exposed to the same broken-switcher behaviour. **This evidence had already appeared once, in the very first diagnostic pass**, in a `which pnpm` check run alongside the original error. It read past unnoticed then, because the loud ENOENT block was the obvious thing to react to and a single unremarkable file path sitting next to it was not.

*Figure — PathResolution: Both shells searched the same two folders on PATH. Only the order differed, and that order is what decided which pnpm binary each one actually ran.*

**The fix, once the cause was visible, was one line**: put the right install's `bin` folder ahead of Chocolatey's in `PATH` for that shell.

```bash
export PATH="/c/Users/<user>/AppData/Local/pnpm/bin:$PATH"
which pnpm
# /c/Users/<user>/AppData/Local/pnpm/bin/pnpm
```

## Verification

With the right binary resolved, `pnpm install` completed and populated `node_modules` for the first time on this machine, and `pnpm thumbnails:fix` ran the normalization script instead of failing before it started. **`pnpm --version` in both shells now agrees: `11.3.0`, from the same install, every time.**

## A second, unrelated Windows failure: builds blocked from symlinking their own dependencies

A separate failure surfaced later, on the same machine, while publishing an unrelated article: `pnpm build`'s Vercel adapter crashed partway through, after the server bundle itself had already built successfully:

```
EPERM: operation not permitted, symlink '.pnpm\sharp@0.35.3_@types+node@24.13.3\node_modules\sharp' -> 'C:\repositories\personal\oharu-tech-blog\.vercel\output\functions\_render.func\node_modules\sharp'
```

The adapter bundles a serverless function's dependencies by symlinking them out of pnpm's content-addressed store, and creating a symlink is exactly the operation Windows blocks for a standard account by default. **The fix is Developer Mode, not an elevated shell**: Settings → Privacy & security → For developers → Developer Mode grants the `SeCreateSymbolicLinkPrivilege` a non-admin session otherwise lacks, and turning it on there (without running anything as Administrator) was enough. The registry key it sets, `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock\AllowDevelopmentWithoutDevLicense`, is worth checking before assuming a symlink failure needs a full elevated session: absent or `0` means Developer Mode is off, and that alone reproduced this build.

The same build printed a second, easy-to-conflate warning right before the crash:

```
The local Node.js version (26) is not supported by Vercel Serverless Functions.
Your project will use Node.js 24 as the runtime instead.
Consider switching your local version to 24.
```

Node 26 is this machine's *Current* release line, not the LTS line Vercel's functions actually run on. **The warning did not stop the build; the EPERM error immediately after it did**, which made it easy to read the two as one failure when they are unrelated. The Node mismatch is a runtime-support warning Astro's Vercel adapter downgrades around; the symlink error is a Windows permission gap with nothing to do with which Node version is running.

## The fix that didn't hold

**The `export` that fixed the shadowed install earlier fixed the shell it ran in and nothing beyond it.** A shell-scoped environment variable does not persist to a new terminal, so the next day, a fresh session hit the identical error again:

```
ERROR  Failed to switch pnpm to v11.3.0. Looks like pnpm CLI is missing at "C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin" or is incorrect
spawnSync C:\Users\<user>\AppData\Local\pnpm\.tools\@pnpm+win-x64\11.3.0\bin\pnpm ENOENT
```

The agent re-ran the earlier diagnosis from scratch: delete the broken `.tools\@pnpm+win-x64\11.3.0` folder and retry, to check whether this was a fresh interrupted download or the same deterministic failure. It was the same failure. `where pnpm`, in a shell carrying no override, again listed the Chocolatey binary first, and invoking the AppData install directly by its full path switched to `11.3.0` cleanly. **The working install had never been broken. It had only ever been outranked, and the earlier fix had only changed the ranking for one shell.**

This time the fix targeted the competing install itself: `choco uninstall pnpm -y`. That failed on the first attempt too, with `UnauthorizedAccessException: Access to the path 'C:\ProgramData\chocolatey\bin\pnpm.exe' is denied` — Chocolatey's files live under `C:\ProgramData`, and removing them needs an elevated shell the agent cannot open on its own. I ran the uninstall myself from PowerShell opened as Administrator. With the Chocolatey package gone, `where pnpm` now returns exactly one path, in every shell, with no export to remember to carry forward.

## Summary

**The account of "a stale Chocolatey install" was wrong, and worth being wrong about out loud.** What actually broke was pnpm's own self-managed version switcher failing to finish installing a pinned version, a bug documented across unrelated environments with the identical error text. What made the fix take two passes instead of one was a second, older pnpm sitting earlier in one shell's `PATH`, evidence for which had already surfaced in the first diagnostic command and gone unread. Reinstalling pnpm fixed the switcher. Checking `which pnpm` first, before trusting the version number a shell reports, would have caught the shadowed install on the first pass instead of the second. **A `PATH` reorder is scoped to the shell that ran it, so it was never going to be the durable fix**; removing the shadowing install outright, in an elevated shell, was.

## References

- [pnpm#9183: Failed to switch pnpm to vXX, ENOENT after a partial self-install](https://github.com/pnpm/pnpm/issues/9183)
- [pnpm#9046: Failed to switch pnpm to vv10.2.0](https://github.com/pnpm/pnpm/issues/9046)
- [pnpm#9192: Looks like pnpm CLI is missing at \\pnpm\\.tools\\](https://github.com/pnpm/pnpm/issues/9192)
- [pnpm#9715: Automatic download of pnpm version breaks when a home directory is a symlink](https://github.com/pnpm/pnpm/issues/9715)
