Skip to content
Back to field notes
DependenciesJune 6, 20266 min read

No lockfile committed: dependency drift in AI-built apps

When no lockfile is committed, the build that ships to production can resolve different dependency versions than the ones you tested locally. This note shows how to detect the gap and pin the tree before launch.

DependenciesSupply chainLockfile

The version you tested is not always the version that ships

The concrete launch risk is this: with no lockfile committed, the install that runs on your build server can resolve to different dependency versions than the ones you ran locally. The code you tested and the code you deployed are no longer guaranteed to be the same tree. This is a missing package-lock problem, and in an AI-built app it is easy to ship without noticing.

A lockfile (package-lock.json for npm, pnpm-lock.yaml for pnpm, yarn.lock for Yarn) records the exact resolved version of every package in your tree, direct and transitive, plus an integrity hash. Without it, your package.json holds version ranges like ^1.4.0, which mean "1.4.0 or any compatible newer release." A fresh install resolves each range against whatever the registry serves at that moment.

So the observable fact is narrow and checkable: either a lockfile is committed to your repo or it is not. What follows from a missing one is a possibility, not a certainty. If a maintainer published a new compatible release between your last local install and your production build, your deploy can pull it. Most of the time that release is fine. The risk is the window where it is not: a transitive package that was compromised, yanked, or shipped a breaking change inside a range your package.json silently accepts.

  • Observable now: is package-lock.json (or pnpm-lock.yaml / yarn.lock) tracked in git, and does your deploy use it.
  • Only possible: that an unpinned range will resolve to a version you never tested. The lockfile is what closes that window.
  • This sits in the software-supply-chain risk class. It is not an exposed secret or an open route, so it does not show up on the public URL at all.

Why AI-built apps hit this

AI scaffolding optimizes for an app that runs, not for a repository that is reproducible. Three habits combine to leave the lockfile out.

The generated .gitignore is often too broad. Some AI tools, and a lot of copied boilerplate, ignore lockfiles outright or lump them in with build artifacts. The app still works locally because the lockfile exists on disk; it just never gets committed. The gap is invisible until someone clones the repo fresh or the CI cache is cold.

The deploy shortcut hides it further. When the build platform runs npm install (rather than npm ci) against a package.json with no committed lock, it re-resolves the tree from scratch on every deploy. Two deploys a week apart can produce two different trees from the identical commit. Because the app boots both times, nothing flags it.

  • The app runs on the author's machine, so the missing lock looks like it is working.
  • AI tools rarely run npm ci, the command that requires and respects a lockfile, so the failure mode never surfaces during generation.
  • A package.json full of caret ranges is the default the tools emit, which maximizes drift surface unless a lockfile pins it.

Check whether you have the gap

This is a repo-level and build-level check, not something you read off the live site. Run it against your own project before launch.

First, confirm the lockfile is committed. From the repo root, ask git whether it is tracked, not just present on disk: check that git knows about package-lock.json, pnpm-lock.yaml, or yarn.lock. A file sitting in your working directory but listed in .gitignore is the trap, because it disappears the moment someone clones. Open .gitignore and confirm no line matches your lockfile name.

Second, confirm the build uses it. Look at your deploy or CI config for the install command. The safe pattern for npm is npm ci, which installs strictly from package-lock.json and fails loudly if the lock and package.json disagree. The drift-prone pattern is npm install with no committed lock, which re-resolves ranges every time. For pnpm the strict form is pnpm install --frozen-lockfile; for Yarn it is yarn install --immutable.

  • Safe result: a lockfile is tracked in git, and your build runs npm ci (or the frozen-lockfile equivalent). The deployed tree matches the committed lock byte for byte.
  • Suspicious result: no lockfile in git, or a lockfile present but the build runs plain npm install. Either way the production tree is resolved fresh and can differ from what you tested.
  • Edge case worth checking: a committed lockfile plus a build that still runs npm install. npm install can quietly update the lock to satisfy a newly published version, so the strict ci command is what actually freezes the tree.

Commit the package-lock and install strictly

The fix is small and the same shape on every package manager.

Generate and commit the lockfile. Run a clean install locally so the lock reflects the tree you tested (npm install, pnpm install, or yarn), then commit package-lock.json, pnpm-lock.yaml, or yarn.lock. Remove any line in .gitignore that excludes it. From now on the lockfile travels with the code, and a fresh clone resolves to the exact versions you verified.

Switch the build to a strict install. Change your deploy and CI install step to npm ci (or pnpm install --frozen-lockfile, or yarn install --immutable). These commands refuse to silently mutate the lock and fail the build if it is out of sync with package.json, which turns drift from a silent runtime surprise into a visible build error you fix before shipping.

What pinning does not cover: a lockfile freezes versions, it does not vouch for them. If the version you pinned is itself vulnerable, outdated, or a typosquatted package you added by mistake, the lock faithfully reproduces that problem on every install. Pinning makes your tree reproducible and auditable; it does not make it safe. That is a separate review. See npm audit before launch for reading vulnerability output, and outdated dependencies as a launch blocker for deciding what to update before you ship.

  • Commit the lockfile, then make a fresh clone and run the strict install to prove the tree is reproducible.
  • Use one package manager per repo. A second lockfile from a different tool reintroduces ambiguity about which tree actually installs.
  • Keep updating dependencies deliberately, on purpose, by running the update command and committing the changed lock, rather than letting an unpinned range drift on its own schedule.

Where VibeCodeGuard fits, and what it does not prove

Be direct about this one: a missing lockfile is not something VibeCodeGuard can detect. Whether your repo committed package-lock.json, and which install command your build runs, are repo-level and CI-level facts. They never appear on the deployed public URL, and VibeCodeGuard's Launch Check only inspects the public surface: response headers, source maps, exposed build and deploy artifacts, secret-shaped strings in the client bundle, public storage, and open CORS. The lockfile gap lives entirely upstream of that surface, so the scan is the wrong tool for it. The fix is the local check above, run in your repo and your CI config before deploy.

Where the two connect is order of operations. Pin your tree and install strictly first, so the bundle you eventually deploy was built from versions you actually tested. Then run a Launch Check against that deployed URL to catch the public-surface problems the dependency work cannot see, such as an exposed source map or a leaked key in the bundle. They are complementary passes, not substitutes.

Next step: from your repo root, confirm a lockfile is tracked in git and switch your build's install command to the strict form (npm ci or the frozen-lockfile equivalent). Once the tree is pinned and rebuilt, run a focused Launch Check against the public URL before sharing it.

A committed lockfile and a clean public scan together still do not prove your dependencies are safe. The lockfile proves your tree is reproducible; the scan proves what an outside visitor can reach. Neither one confirms that the versions you pinned are free of known vulnerabilities, and neither reads your code for logic or authorization gaps. Pair both with a vulnerability audit and manual review.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.