Fastest fix: delete the stray package-lock.json, reinstall with your real manager, then pin it in package.json (packageManager plus devEngines.packageManager) and add a preinstall: only-allow pnpm guard. The pin is what stops the AI from doing it again. Everything below is the full cleanup and lock-down.
You committed to pnpm two years ago. Your repo has pnpm-lock.yaml, your CI runs pnpm install --frozen-lockfile, your Dockerfile pins corepack enable && corepack prepare pnpm@11. Cursor or Claude Code does not care. It runs npm install lodash, creates a package-lock.json next to your existing pnpm-lock.yaml, hoists differently than pnpm’s strict (non-flat) layout, and then stages both files. CI breaks because the workspace no longer matches the npm-flat layout, a previously-resolved phantom dependency now fails, and your colleague pulls and gets a tangled node_modules. This is one of the most common low-grade AI failures and it compounds: once both lockfiles exist, every later install is at risk.
Which bucket are you in
Run this first so you fix the right thing:
ls -la package-lock.json pnpm-lock.yaml yarn.lock bun.lock 2>/dev/null
grep -E '"packageManager"|"devEngines"' package.json
| Symptom | Likely cause | Jump to |
|---|---|---|
package-lock.json exists next to pnpm-lock.yaml | AI ran bare npm install | Step 1, then Step 2-3 |
| Two lockfiles in git history, neither pinned | No packageManager field | Step 2 |
| AI keeps recreating the wrong lockfile each session | No preinstall guard or rules file | Step 3, Step 5 |
npx <tool> resolves the wrong version in a monorepo | npx bypasses the workspace | Step 6 |
| Local works, CI breaks with identical-looking errors | corepack not enabled on CI | Step 7 |
Common causes
Ordered by how often each shows up.
1. AI defaults to npm because it dominates training data
The vast majority of public Node tutorials use npm install. The model has the npm command vocabulary trained deep. Without an explicit project-level signal, it reaches for npm first.
How to spot it: the AI suggests npm install <pkg> despite the repo having pnpm-lock.yaml or yarn.lock.
2. packageManager field missing from package.json
Corepack (bundled with Node 16.9+) reads "packageManager": "pnpm@11.x" in package.json to decide the canonical manager. Without it, the AI has no machine-readable signal and neither does corepack.
How to spot it: grep packageManager package.json returns nothing.
3. AI ran npm to “fix” a problem it diagnosed wrong
It saw a missing-dependency error, jumped to npm install <pkg>, and never considered that this creates a competing lockfile. Speed bias over correctness.
How to spot it: a package-lock.json shows up in a commit that was supposed to fix something else.
4. Mixed managers across team members or environments
One dev uses npm, another pnpm, CI uses yarn. The AI inherits the inconsistency and picks based on whatever it sees in the current open file’s directory.
How to spot it: multiple lockfile types in version-control history; teammates have different node_modules layouts.
5. AI generated npx commands that bypass the workspace manager
npx prisma generate runs, but it does not respect pnpm’s workspace structure. In monorepos it can resolve to a different prisma version than the workspace pins.
How to spot it: npx <tool> commands appear in AI-generated scripts; tool version differs between local and CI.
6. AI added “compatibility” scripts to package.json
Sometimes the AI invents "scripts": { "install:npm": "npm install" } for “convenience”. Now any teammate who runs that script poisons the lockfile.
How to spot it: scripts in package.json reference both npm and pnpm/yarn commands.
Before you start
- Identify your canonical package manager. If unclear, ask the team. Committing to one is half the fix.
- Make sure no in-progress AI changes are staged that include
package-lock.jsonor straynode_modulespaths. - Decide how you will enforce:
devEngines/packageManager(corepack), apreinstallguard, a CI check, or all three. Use all three for anything shared.
Information to collect
- Which lockfiles exist:
ls -la package-lock.json pnpm-lock.yaml yarn.lock bun.lock. package.jsoncontent, specifically thepackageManager,devEngines, andenginesfields..npmrc,.pnpmrc,.yarnrc.ymlif present.- CI config: which install command runs in pipelines.
- Dockerfile / devcontainer setup: what is installed on container start.
- Recent commits where the wrong lockfile was created or both exist.
Step-by-step fix
Ordered: clean up the current mess, then prevent recurrence.
Step 1: Delete the wrong lockfile and reinstall correctly
If your canonical manager is pnpm and a package-lock.json appeared:
rm -rf node_modules package-lock.json
pnpm install
git add package.json pnpm-lock.yaml
git rm --cached package-lock.json 2>/dev/null
Repeat the analogous steps if your canonical manager is yarn or npm. Add package-lock.json (or whichever is foreign) to .gitignore so it never gets staged again.
Step 2: Declare the canonical manager in package.json
Use both fields. As of June 2026 the packageManager field is the one corepack reads to pick the exact version; devEngines.packageManager (added in pnpm v11) lets you assert a range and a failure mode, and is the modern, recommended way to enforce the manager since the old only-allow repo is now archived.
{
"packageManager": "pnpm@11.7.0",
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": ">=11.0.0 <12.0.0",
"onFail": "error"
}
},
"engines": {
"node": ">=22"
}
}
Notes for June 2026:
- pnpm 11 requires Node 22+ and is now pure ESM, so set
engines.nodeto>=22to match. Current stable is 11.7.0. - With corepack enabled (
corepack enable), runningyarnor a wrongpnpmversion in this repo will be redirected or rejected to match the pin. onFail: "error"makes a mismatched manager hard-fail instead of silently continuing. You can override per machine with thepmOnFailsetting without editing the manifest.- Corepack auto-updates the
packageManagerfield by default; setCOREPACK_ENABLE_AUTO_PIN=0if you want it left exactly as written.
Step 3: Add a preinstall guard
In package.json:
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
only-allow is a tiny package that inspects the npm_config_user_agent env var and aborts if the wrong manager is invoked. Running npm install prints a clear error and exits non-zero.
Important limitation (verified June 2026): the preinstall hook only fires on npm install / npm ci. It does not block npm install <package> (adding one package), because npm resolves and writes package-lock.json before running preinstall. That is exactly the command the AI tends to run. So only-allow is a backstop, not your only line of defense; the devEngines pin in Step 2 and the CI check in Step 4 cover the gap. (The only-allow GitHub repo is archived as of 2026 but still works; devEngines.packageManager is its officially recommended successor.)
Step 4: Add a CI check that fails on a foreign lockfile
In CI, run this before install so a bad PR cannot merge even if the local guards were bypassed:
if [ -f "package-lock.json" ] || [ -f "yarn.lock" ] || [ -f "bun.lock" ]; then
echo "ERROR: Found a non-pnpm lockfile. This repo uses pnpm only."
exit 1
fi
This is the check that actually catches npm install <package>, since that command does create package-lock.json despite the preinstall guard.
Step 5: Tell the AI explicitly via a rules file
In .cursor/rules/package-manager.mdc (the current Cursor format; the legacy .cursorrules still works) or CLAUDE.md:
This repo uses pnpm 11. Never use npm or yarn commands.
- Install: pnpm install
- Add package: pnpm add <pkg>
- Add dev dep: pnpm add -D <pkg>
- Remove: pnpm remove <pkg>
- Run script: pnpm run <script> or pnpm <script>
- Workspaces: pnpm --filter <pkg> <cmd>
Do NOT generate or run:
- npm install / npm ci / npm install <pkg>
- yarn install / yarn add
- npx (use pnpm dlx instead if needed)
The preinstall hook and devEngines pin will block the wrong manager,
but do not generate npm/yarn commands in the first place.
This puts the policy directly in the AI’s working context, which is the layer that stops the wrong command from being suggested at all.
Step 6: Use pnpm dlx / yarn dlx instead of npx
For one-off tool invocations:
# Instead of:
npx prisma generate
# Use:
pnpm dlx prisma generate
# or, better, pin it as a dev dep and run:
pnpm exec prisma generate
pnpm dlx respects the workspace’s resolver. It also tends to be faster than npx because of pnpm’s content-addressed store.
Step 7: Lock the dev environment via corepack
In your README, Dockerfile, and CI setup:
corepack enable
corepack prepare pnpm@11.7.0 --activate
Now every dev environment and CI runner uses the exact same pnpm version, derived from the packageManager field. The AI cannot accidentally invoke a different version even if it tries pnpm correctly. If you only enable corepack locally and not on CI, you get a class of bug whose symptoms look identical to the wrong-manager problem.
How to confirm it’s fixed
- Only one lockfile exists in the repo (
pnpm-lock.yamlonly, oryarn.lockonly, etc.). - Running
npm installin the repo fails fast with theonly-allowmessage. - Running
npm install <some-pkg>does NOT leave a committedpackage-lock.json, because CI rejects it (the preinstall guard alone will not catch this case). corepack prepare pnpm@11.7.0 --activate && pnpm --versionprints the pinned version.- The AI, when asked to “add a new package”, generates
pnpm add(notnpm install) on the first try. - A fresh clone plus
pnpm install --frozen-lockfileproduces a workingnode_modules.
Long-term prevention
- Always set
packageManager(and, on pnpm 11+,devEngines.packageManager) for any new repo. This is the single most effective guard. - Treat the CI lockfile check as the real enforcement, since the
preinstallguard cannot catchnpm install <pkg>. - Educate the AI via a rules file once; do not rely on per-prompt corrections.
- For monorepos, prefer pnpm or yarn berry. npm workspaces are the weakest and the AI’s defaults hurt more there.
- Pin the manager version (not just the name) via corepack to avoid drift.
Common pitfalls
- Leaving
packageManagerunset and assuming “well,pnpm-lock.yamlis right there, the AI will figure it out.” It will not. - Relying on
only-allowalone and being surprised whennpm install lodashstill writespackage-lock.json. Add the CI check. - Deleting
package-lock.jsonbut forgetting the prevention layers, so the AI recreates it next session. - Using
npxin AI-generated scripts for monorepo tools, which picks up the wrong version silently. - Letting one teammate “just use npm because pnpm is annoying.” They generate the broken lockfile and the AI then imitates the pattern from their commits.
- Committing both lockfiles “just in case.” They will diverge and one will be wrong.
- Setting
engines.nodeto an old range after upgrading to pnpm 11, which requires Node 22+.
For related issues see AI package-lock conflicts, AI overwrote env vars, and AI build passes locally but fails in cloud. The official docs are the pnpm package.json reference and corepack.
FAQ
Q: Why does the AI default to npm even when pnpm-lock.yaml is visible?
Most public training data uses npm. The lockfile is a weak signal the AI may notice or ignore; the packageManager field is a much stronger, machine-readable one. Set it.
Q: I added the only-allow preinstall guard, but npm install lodash still created package-lock.json. Why?
npm resolves and writes the lockfile before it runs the preinstall script, so the guard fires too late on npm install <pkg>. It only reliably blocks bare npm install / npm ci. Use the CI lockfile check (Step 4) and the devEngines pin (Step 2) to cover the gap.
Q: Should I use packageManager or the new devEngines.packageManager?
Use both. packageManager pins the exact version corepack runs; devEngines.packageManager (pnpm v11+, added 2026) lets you express a version range and an onFail behavior, and is the officially recommended successor to the now-archived only-allow repo.
Q: How do I prompt the AI to migrate from npm to pnpm?
“This repo is migrating from npm to pnpm 11. Delete package-lock.json and node_modules. Run pnpm import to convert the lockfile. From now on use pnpm exclusively. Add packageManager: pnpm@11.7.0, a devEngines.packageManager pnpm range, and engines.node: >=22 to package.json.”
Q: Will the preinstall guard break if a developer is offline?
only-allow is a tiny local script; it needs no network. It just reads the npm_config_user_agent env var to decide which manager invoked it. It works offline.
Tags: #Troubleshooting #AI coding #npm #pnpm #yarn #package-manager