The deploy that broke on its own — pnpm, audit fix and ERR_PNPM_EEXIST
Portfolio·

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.json declared packageManager: "pnpm@9.12.3" — that’s what Vercel used for the build
  • dependency-sast-scan.sh (security cron, Friday 8am) runs pnpm audit fix — updates deps and regenerates the lockfile
  • The pnpm on 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 dependenciesERR_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

  1. “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.
  2. A security tool can take down productionpnpm audit fix (which exists to protect) regenerated the lockfile with the wrong version. Security automation must run in the SAME environment as the build.
  3. PATH is a trap in WSL/mnt/c/.../npm in the PATH means the wrong binary can be picked silently. Always pin the absolute path of the right binary.
  4. 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.
~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$