Lockfiles and Dependency Adds: When the Agent Installs a Package You Did Not Ask For

Marcus Webb

Marcus Webb

September 30, 2026

Lockfiles and Dependency Adds: When the Agent Installs a Package You Did Not Ask For

The pull request was titled “Add date range filter to orders dashboard”. The diff stat said a little over 3,100 lines added. The component itself was about 180 of them. Nearly all the rest was lockfile.

I reviewed the component carefully. It was good: accessible, keyboard-friendly, sensible defaults for “last 7 days” and “this month”. The tests passed. I had my cursor on the approve button when I scrolled past package.json and saw three new lines in dependencies that nobody had asked for.

The agent had installed a date picker library, and with it the older, heavier date library that the picker depends on. Our dashboard already used a small, modular date library throughout. We now had two date libraries in the same bundle, formatting dates in two slightly different ways, and a lockfile diff long enough that no human was ever going to read it.

That review changed how I handle dependencies on any project where an agent can run the package manager.

What was actually in the diff

Once I stopped and read the parts I would normally skim, there were four separate problems in that one pull request.

New direct dependencies. The date picker, the date library it relies on, and a small class-name helper the agent added for styling. We already had a class-name helper. It is not a large package, but it is the same job done by a second tool, which is how codebases end up with three ways of doing everything.

The wrong lockfile. Our project uses pnpm. The agent ran npm install, which dutifully created a package-lock.json alongside our pnpm-lock.yaml. CI then failed, because it installs with pnpm in frozen-lockfile mode and the pnpm lockfile no longer matched package.json. The agent saw the red build, ran pnpm install to fix it, and committed both lockfiles. The build went green. The stray npm lockfile stayed.

Transitive churn. Running a fresh install also let the package manager re-resolve some version ranges, so a handful of unrelated transitive packages moved up by minor versions. None of it was dangerous, but it meant the lockfile diff mixed “things we added on purpose” with “things that moved because someone ran install”, and there was no way to tell which was which by reading it.

Install scripts. One of the picker’s dependencies had a postinstall script. It was harmless, just a funding message, but it ran on the developer’s machine with the developer’s permissions when the agent installed it, and again in CI. Nobody had looked at it before it ran.

Small brass balance scale weighing a tiny gear beside loose screws on a workbench

Why agents reach for a package

I do not think the agent did anything unreasonable given what it was told. The task said “add a date range filter”. Installing a well-known date picker is a completely normal way to do that, and it is what a lot of the code the model learned from does.

A few things push agents towards installing rather than reusing:

  • Installing is the fastest route to a working feature. Building a range picker from existing primitives takes more steps and more chances to get something wrong. A package gets a green build in one command.
  • The agent does not check what is already there unless asked. It may read package.json, but it does not reliably ask “do we already have something that does this?” before installing.
  • Popularity in training data. The older, heavier date library is enormously represented in public code. The agent reaches for what it has seen most, not what fits your bundle best.
  • It cannot feel weight. A person adding a dependency to a frontend project usually has a vague sense of what it costs. The agent does not see the bundle analyser unless you put it in front of it.

So the fix is not to scold the agent. It is to give it the information and the rule it was missing.

What it cost

I measured before and after, because “it adds weight” is not an argument anyone acts on.

The orders dashboard route grew by roughly 70 KB gzipped, most of it the second date library and its locale data. On our users’ typical connection that was a noticeable delay on first load for a page people open many times a day. The picker also shipped its own CSS, which overrode two of our design-system styles in a way that only showed up in dark mode.

The two date libraries also disagreed about one thing that mattered. Our existing code treated the end of a date range as the end of the selected day. The new picker returned the start of that day. Filtering orders for “1 to 7 March” quietly excluded everything placed on 7 March. The tests the agent wrote used the picker’s own output, so they agreed with the bug.

That, more than the kilobytes, is the real cost of duplicate dependencies. Two libraries that do the same job will make slightly different choices, and the seams between them are where the bugs live.

The rules I added

All of these are small. Together they turned “the agent installs whatever it likes” into “the agent proposes, a person decides”.

An explicit dependency rule in the agent instructions. The project’s instructions file now says: do not add, remove or upgrade dependencies without asking. Before proposing a new one, check whether an existing dependency already covers the need. If you still think a new package is right, stop and explain which package, why the existing ones cannot do it, roughly how large it is, and whether it has install scripts. The file also lists what we already use for dates, validation, class names, HTTP and state, so the agent does not have to guess.

Since adding that, the agent has stopped and asked four times. Three times the answer was “use what we have”, and it did, without complaint. Once it proposed a small, well-maintained library for virtualised lists that we genuinely did not have, with a clear explanation. I said yes.

Pin the package manager. The packageManager field in package.json declares pnpm and its version, and Corepack enforces it, so npm install in that directory fails with a clear message instead of creating a second lockfile. A CI step also fails if any lockfile other than ours exists in the repository. Belt and braces, both a few lines.

Make a person review dependency changes. A code owners entry puts package.json and the lockfile under review by one named person on the team. It does not slow down ordinary pull requests, because most do not touch those files. The ones that do get a second pair of eyes that is specifically looking for new packages.

Review the lockfile by summary, not by line. Nobody reads 2,900 lines of YAML. What I read now is a short summary: which direct dependencies changed, which new packages appear anywhere in the tree, and which of them have install scripts. The package manager can tell you why a given package is present, and a small CI job posts the list of added and removed packages as a comment. That is a list you can actually review.

A bundle budget in CI. Each main route has a size budget. If a change pushes a route over its budget, the build fails with the size difference. This turns “it adds weight” into a red check with a number on it, which the agent also sees and responds to.

Do not run dependency install scripts by default. Recent pnpm versions no longer run install scripts for dependencies unless you explicitly allow them, and we keep a short allowlist for the few packages that genuinely need a build step. With npm you can set ignore-scripts in the project’s config. Either way, a new package should not get to run code on a developer’s machine just because an agent installed it.

Tangle of power strips and cables piled inside a wooden cabinet

Check that the package is the one you think it is

One more habit, because it catches a different failure. Models occasionally suggest package names that are almost right: a plausible-sounding name that does not exist, or one letter away from the real one. If someone has registered that name, installing it runs their code.

Before approving any new dependency, I open its registry page and check four things: that it is the package I expect from the project’s own documentation, that it has more than one release and a history longer than a few weeks, who maintains it, and whether it has install scripts. It takes two minutes. I have not caught a malicious package this way yet, but I have caught the agent proposing an abandoned fork instead of the maintained original.

When the agent’s package is the right call

Not every unrequested dependency is wrong, and I do not want to swing to the other extreme where the agent hand-writes everything. A well-maintained library is often better than 200 lines of custom code that only one person understands.

My test for accepting one is three questions. Does it replace code we would otherwise have to write and maintain ourselves? Is it small, or does it only ship what we import? Does it avoid duplicating something we already use? If all three are yes, the agent found something useful. If any is no, we use what we have.

How the date filter ended up

We removed the picker, the second date library and the extra class-name helper. The agent rebuilt the range filter using our existing date library and a headless popover component already in our design system. It took one more round and about forty minutes. The dashboard route went back to its previous size, the end-of-day bug disappeared because there was only one set of date rules again, and the stray npm lockfile was deleted.

In the final pull request, neither package.json nor the lockfile changed at all. Of all the dependency diffs I review, that is the one I like best.

More articles for you