
The deploy that broke on its own — pnpm, audit fix and ERR_PNPM_EEXIST
The deploy that broke on its own
There’s a type of silent bug worse than a crash: the one that shows up when nobody touched anything. That’s exactly what was happening to the Portfolio in August 2026 — the Vercel deploy started rejecting the build with an esoteric error:
ERR_PNPM_EEXIST
Nobody had touched the code. The site ran beautifully locally. But Vercel said no. The investigation revealed a story of two tools fighting each other: the security cron that updates dependencies, and the pnpm that package.json declared.
Context — the guardian that became the villain
The Portfolio lives in production on Vercel (samuelmedeiros.vercel.app) and self-hosted on port 3001. The deploy was manual: vercel --token "$VERCEL_TOKEN" --prod.
The cast:
package.jsondeclaredpackageManager: "pnpm@9.12.3"— that’s what Vercel used for the builddependency-sast-scan.sh(security cron, Friday 8am) runspnpm audit fix— updates deps and regenerates the lockfile- The
pnpmon the WSL PATH… was sometimes the Windows one (/mnt/c/.../npm), version 10.34.5
The audit fix ran with pnpm 10, producing a v10 lockfile. packageManager still said 9.12.3. On Vercel: pnpm 9 trying to read a v10 lock + flattening the node_modules of a dep with bundled dependencies → ERR_PNPM_EEXIST.
The struggle — three fixes before finding the root
Fix 1: align packageManager (commit 6e90b6b)
// package.json
- "packageManager": "pnpm@9.12.3"
+ "packageManager": "pnpm@10.34.5"
Vercel started using pnpm 10 (the same version as the lockfile). Progress — but the error persisted somewhere else.
Fix 2: kill node-linker=hoisted (commit 0aa26b2)
The .npmrc had node-linker=hoisted (inherited from an old config). Hoisted makes pnpm flatten the internal node_modules of @parcel/watcher-wasm (a dep with bundledDependencies) — and the rename conflicts with the restored build cache:
Hoisted flattens the internal node_modules of @parcel/watcher-wasm
(bundledDependencies) → rename conflicts with the restored build cache
→ ERR_PNPM_EEXIST. On Vercel/Linux, isolated is the default and works.
# .npmrc — after
store-dir=/tmp/.pnpm-store
Fix 3: align the CI (commit d90d212)
GitHub Actions was still installing pnpm 9.12.3 in pnpm/action-setup — out of sync with packageManager. Bumped to 10.34.5.
The real root cause
The underlying problem wasn’t pnpm 9 or 10 — it was the PATH. The security cron ran pnpm audit fix with the Windows pnpm (the PATH included /mnt/c/.../npm), not the WSL one. Every Friday it regenerated the lockfile with a version different from the declared one — and the following week’s deploy broke “on its own”.
Resolution — the alignment rule
After the three fixes, the principle became clear and became a rule:
local pnpm == packageManager in package.json == pnpm in CI
And the practical rule for security: check the PATH version before running audit fix — the right binary is ~/projetos/.hermes/node/bin/pnpm, not the Windows one.
The deploy started passing again. And the most valuable lesson: the tool meant to protect the project (security scan) almost took down production — because the environment where it ran wasn’t the same as the build environment.
Metrics
| Metric | Before | After |
|---|---|---|
| packageManager | pnpm@9.12.3 | pnpm@10.34.5 |
| Lockfile | v10 (regenerated by Windows) | v10 (aligned with packageManager) |
| node-linker | hoisted (flattened bundled deps) | removed (isolated default) |
| CI pnpm/action-setup | 9.12.3 | 10.34.5 |
| Vercel deploy | ERR_PNPM_EEXIST | stable |
| Commits | 529 | 530 |
Lessons
- “Works locally” doesn’t mean “works on Vercel” — the build environment is different: another pnpm version, another node_modules, another cache. If the lockfile and packageManager diverge, the deploy breaks.
- A security tool can take down production —
pnpm audit fix(which exists to protect) regenerated the lockfile with the wrong version. Security automation must run in the SAME environment as the build. - PATH is a trap in WSL —
/mnt/c/.../npmin the PATH means the wrong binary can be picked silently. Always pin the absolute path of the right binary. - The esoteric error has a simple root cause — ERR_PNPM_EEXIST looked like magic, but it was just divergent pnpm versions + the wrong linker. Layer-by-layer diagnosis: packageManager → .npmrc → CI → PATH.