Skip to content

Unversioned Scaffolding Commands Pull Stale Templates

Unpinned npx scaffolding silently resolves to old versions when the runtime falls outside the latest engines window; agents treat the obsolete output as ground truth.

When an agent runs npx create-something with no @version, the assumption is "no version means latest". npm's resolver does not work that way. It walks back the manifest list to the most recent version whose engines.node field satisfies the current Node runtime, which on an older or mismatched runtime can be a release that shipped years before the latest tag.

The Pattern

Coding agents reach for scaffolding generators the way developers do — npx create-next-app, npx create-react-app, npx @microsoft/generator-sharepoint, pnpm create vite. The unpinned form is the documented one in most tool READMEs, and the agent reproduces what it sees. When the command succeeds and emits a project tree, the agent treats the result as canonical and continues — wiring routes, adding tests, deploying CI — against whatever structure landed on disk.

Why It Fails

The npm CLI's manifest resolver, npm/npm-pick-manifest, prefers the defaultTag (latest) when its manifest satisfies the requested range, then prioritises versions whose engines requirement is satisfied by the active runtime. nodeVersion defaults to process.version. When upstream tightens engines.node faster than a runtime upgrades — a common situation in long-lived agent containers, default CI runners, and devcontainers — the resolver walks the version list backwards to the most recent published version whose engines.node does satisfy. That version can be old.

The case study in Your agent just scaffolded a project from 2020 (Microsoft for Developers, 2026) documents an agent receiving SharePoint Framework v1.11.0 (July 2020) instead of v1.23.0 because the test machine's Node version sat outside the latest generator's engines window. The command exited cleanly. Per the Microsoft writeup: "All the agent sees is output. Command ran, exit code 0, files appeared. Done."

Two failure modes follow:

  • Silent obsolescence. No warning, no diagnostic, no telemetry that the resolver fell back. The next agent decision treats the obsolete project shape as authoritative.
  • Pattern propagation. Once the agent has an old template on disk, it replicates the deprecated patterns elsewhere — the Pattern Replication Risk failure compounds the cost.

This is distinct from two existing staleness modes. It is not stale retrieval (Stale Repository Retrieval Induces Incorrect Code), where a RAG index serves obsolete snippets. It is not config drift (Stale AI Configuration Artifacts), where CLAUDE.md describes code that has moved. This is a generation-time resolver fallback — the failure happens at scaffold time, not at retrieval or read time, and the artifact is created wrong from minute one.

Why It Works (the remediation mechanism)

Three structural controls close the gap, all rooted in the same observation: the resolver behaviour is documented and deterministic, so the failure is preventable at the boundary.

  • Pin the entry point. npx create-next-app@15.3 skips the engines-fallback path entirely — the resolver is given a single version to consider. Microsoft's blog recommends pinning in prompts and in agent tool extensions.
  • Pin the runtime. A .nvmrc or .node-version file with a version manager that auto-switches keeps process.version aligned with what the latest generator publishes its engine window for. With both ends aligned, the unpinned form returns latest.
  • Verify after generation. A post-scaffold check that reads the generated package.json and compares against the registry's latest is the only one of the three that catches the failure when both pins are missed.

When This Backfires

The fix is not free, and the failure surface is narrower than "every npx call".

  • Mature, version-stable generators on locked runtimes. When Node and the generator both float to latest and the runtime is current, the unpinned form returns the same thing as @latest. The pin is dead weight that ages the prompt — every "scaffold a Next app" prompt in the library decays the day Next.js ships a major.
  • Generators that download from a branch, not a published version. Vercel/Next.js Discussion #35794 documents that create-next-app pulls the project template from the canary branch directly; pinning the CLI version with npx create-next-app@X does not pin the generated next version on disk. The post-scaffold verification step is the load-bearing control here.
  • Solo developers verifying scaffold output by eye. The silent-failure mode requires that nobody check package.json after generation. A human glancing at the version field catches this without instrumentation.
  • Pinning everywhere is not a security panacea. Pinning Is Futile (arxiv:2502.06662) finds that pinning direct dependencies can increase exposure to malicious package updates in larger graphs. The recommendation here is to pin the scaffolder entry point — not to extend pinning across the entire dependency tree.

Example

Before — unpinned scaffolder, mismatched runtime:

# Agent prompt: "Scaffold a SharePoint Framework project."
$ npx @microsoft/generator-sharepoint --solution-name MyWebPart
# Generator installed.
# Files written.
# $ echo $?
# 0

The agent moves on. The generated package.json carries @microsoft/sp-core-library at the 2020 version. Every component the agent now writes follows a structure deprecated five years ago.

After — pinned entry point, post-scaffold verification:

# Agent prompt: "Scaffold a SharePoint Framework project. Pin to the latest
# generator on npm. After generation, read package.json and compare the
# scaffolder version against `npm view @microsoft/generator-sharepoint version`.
# If they differ, stop and report."
$ npx @microsoft/generator-sharepoint@latest --solution-name MyWebPart
$ node -e "console.log(require('./package.json').devDependencies['@microsoft/generator-sharepoint'])"
# ^1.23.0
$ npm view @microsoft/generator-sharepoint version
# 1.23.0

The pinned entry point skips the engines-fallback walkback; the verification step catches the residual cases (branch-pulled templates, registry mirror drift) where the pin alone is not sufficient.

Key Takeaways

  • npm's resolver prefers an engines-compatible older version over a latest-tagged version whose engines window excludes the active Node — documented in npm/npm-pick-manifest.
  • Unpinned npx scaffolding can silently produce a years-old project structure; the agent sees exit code 0 and treats the result as ground truth (Microsoft for Developers, 2026).
  • Pin the scaffolder entry point, pin the runtime via .nvmrc/.node-version, and verify the resolved version in the generated package.json against the registry's latest — the three together close the failure mode.
  • This is a generation-time staleness, distinct from retrieval staleness and config drift; the artifact is wrong from the moment it lands.
Feedback