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: "[email protected]" — 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 dependencies → ERR_PNPM_EEXIST.

The struggle — three fixes before finding the root

Fix 1: align packageManager (commit 6e90b6b)

// package.json
- "packageManager": "[email protected]"
+ "packageManager": "[email protected]"

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 [email protected] [email protected]
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 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.
  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      ║
╚══════════════════════════════════════╝
      
$