June 11th, 2026
0 reactions

Your agent just scaffolded a project from 2020

Principal Developer Advocate

Your agent ran a scaffold command. Project generated, dependencies resolved, no errors. Everything looks fine. Except it’s based on the project structure from 2020, and neither you nor the agent noticed.

How npx picks the right-but-wrong version

When an agent scaffolds a project or runs a CLI tool, it often reaches for npx without specifying a version. Something like:

npx create-some-app my-project

Notice, that there’s no version pinned anywhere. The agent typed the package name and assumed it’d get the latest. That’s where things break.

When you run npx without a version, npm resolves the latest version that’s compatible with your current Node runtime. Since npm-pick-manifest v9.1.0 (shipped mid-2024), npm prioritizes versions whose engines field matches your Node version over the latest tag. If an old version has no engine constraints at all, npm considers it compatible with everything.

Say a package has these two versions on the registry:

// v2.0.0 (latest)
{ "engines": { "node": ">=22.14.0 <23.0.0" } }

// v1.3.0 (old, no engines field)
{}

On Node 24, npm skips v2.0.0 (upper bound excludes 24) and lands on v1.3.0, because no engines means “works everywhere.” The resolution chain looks like this:

Step What npm does Result
1 Check latest tag (v2.0.0) node <23.0.0 — skip
2 Check next highest (v1.9.0) node <23.0.0 — skip
3 Check v1.3.0 No engines field — compatible
4 Install v1.3.0 Done

This is how npx works for everyone, agents included. It reads package.json, not docs. It doesn’t know that a newer version exists and would work fine if you just had the right Node version. It follows the engine constraints mechanically, and if those constraints exclude your runtime, it moves on to the next compatible version. This behavior surprised some folks when it first shipped, but it’s been in npm since 10.8.2.

That’s the trap: the scaffold succeeds, the files look fine, nothing flags the mismatch. It all seems to work, just with something ancient.

The Node version manager trap

Things get worse if you use a Node version manager like fnm or nvm.

Version managers let you install multiple Node versions and easily switch between them. It’s convenient: you can run Node 22 for one project and Node 24 for another. But agents don’t think about this. When the agent opens a terminal and runs npx, it gets whatever Node version happens to be active. It doesn’t look for version requirements in the package it’s about to install. Instead, it uses the Node version that’s readily available.

So if you switched to Node 24 for a different project yesterday and forgot to switch back, your agent is now scaffolding with a Node version that pushes npm into its fallback behavior. The agent doesn’t know better. It ran the command, got output, and kept going.

How would you even catch this? npx succeeds, the files appear, the agent reports success. You’d have to inspect the generated package.json, notice the version mismatch, trace it back to your Node version, and connect that to npm’s engine resolution. That’s a lot of detective work for something that looks like it worked.

Old versions become a catch-all

Specifying engine constraints is the right thing to do. It keeps your package off runtimes you haven’t tested, and no one should stop doing it. But there’s a catch.

Any package that’s been around long enough will likely have older versions that didn’t specify engine constraints. Those unconstrained versions are technically compatible with every Node version, because they never said otherwise. So when npm looks for a compatible version, the latest unconstrained version becomes the catch-all package that npx will use.

This doesn’t affect every package equally. Most popular packages (Next.js, ESLint, Prettier, Angular CLI) use open-ended lower bounds like "node": ">=20". Those cover any newer Node version, so npm resolves to the latest just fine. The problem hits packages that use tight upper bounds ("node": ">=22.14.0 <23.0.0") or caret-bounded ranges ("node": "^16 || ^14 || ^12"). Those ranges exclude Node versions outside the tested range, which is exactly when npm starts looking for older alternatives.

Enterprise and platform tools are more likely to use this pattern. They certify specific Node LTS versions and explicitly exclude untested runtimes. Responsible engineering, but it means agents get silently downgraded when they run npx without pinning a version.

And the trend is toward more engine constraints, not fewer. Package authors are increasingly dropping support for older runtimes, and that’s good practice. But it also means more versions of more packages will trigger this fallback when paired with a Node version that’s outside the supported range.

Why agents get hit the hardest

A developer running npx might notice the version mismatch. Maybe the scaffold output mentions v1.11, maybe the generated files look unfamiliar, maybe the developer just knows what version they expect. Agents don’t have any of that context and without explicit instructions, they likely won’t pay attention to it.

All the agent sees is output. Command ran, exit code 0, files appeared. Done. It doesn’t compare expected vs. actual versions, doesn’t cross-reference the generated project structure against what it knows about the current version. It trusts the tool, and the tool silently gave it something old.

We found this while evaluating the Agent Experience of scaffolding SharePoint Framework (SPFx) projects. The agent ran npx without a version, our test machine was on a Node version outside SPFx’s supported range, and npm silently resolved v1.11.0 (published July 2020) instead of the latest v1.23.0. Walked back 12 versions and 6 years. The agent knew v1.23.0 was the latest. But when npx resolved v1.11.0, it saw a successful scaffold and moved on. It didn’t stop to ask: why did I get a version from 2020 when I know the latest is from 2026?

Any package with tight engine bounds would do the same thing. Agents inherit whatever runtime environment they land in and don’t validate it. Node version, npm cache, environment variables: they all shape what a command produces, and the agent takes every one at face value.

What you can do

Here are a few practices for you to consider to avoid this trap.

Pin versions in your prompts

If you’re asking an agent to scaffold a project, include the version you want. “Scaffold a project with create-next-app@15.3” is better than “scaffold a Next.js project.” It nudges the agent toward including a version specifier in the npx command. Not foolproof (the agent might still omit it), but it helps.

Pin versions in your extensions

If you’re building agent extensions (MCP servers, instruction files) that scaffold projects, hardcode the version or make it a required parameter. Don’t let the resolution algorithm decide. Your tool description should produce a command like npx some-package@1.5.0 with the version baked in.

Control your Node version

Use .node-version or .nvmrc in your project root and make sure your version manager respects it. Some version managers auto-switch when you cd into a directory (fnm does this with --use-on-cd). It won’t fix the agent’s behavior, but it reduces the chance of your terminal running an unexpected Node version when the agent starts.

Watch your upper bounds

If you’re a package author, a constraint like "node": ">=22.14.0 <23.0.0" means anyone on Node 23+ gets silently downgraded via npx. Maybe you only tested that range, fair. But if your package works on newer Node versions, a lower-bound-only constraint ("node": ">=22.14.0") avoids the silent fallback without giving up protection against genuinely incompatible runtimes.

Verify the output

After any scaffold command, check the version in the generated package.json. We trust scaffolding tools to give us the latest. With agents, trust but verify.

The real lesson

Runtime environments are invisible to agents. The agent sees your prompt, has access to tools, and runs commands. It doesn’t see your Node version or your version manager state. Either one can change what a command produces, and the agent can’t tell the difference between “worked correctly” and “worked, but with a 6-year-old package.”

If you’re building technology that developers use through agents, assume the runtime is unpredictable. Pin versions, communicate versions, and validate outputs. Don’t rely on resolution algorithms designed for humans who read terminal output. Agents might, but there’s no guarantee. They execute commands and take the results at face value.

Category

Author

Waldek Mastykarz
Principal Developer Advocate

Waldek is a Principal Developer Advocate at Microsoft focusing on AI Coding Agents. He researches AI Coding Agents, and evaluates and improves Agent Experience for Microsoft's products and services.

0 comments