AI coding

A Lockfile Diff Budget: Capping an Agent's Dependency Change

Antonio Evans8 min read

A split scene: on the left, a Git branch graph with a lockfile node glowing red and a counter showing 'transitive: 47, lines changed: 1200' exceeding limits.
Key points

Key points

  1. 1A lockfile diff budget caps two numbers: transitive entries added or removed, and lines changed in the lockfile.
  2. 2Parse the diff, compute both numbers with a script, and exit non-zero when either crosses a threshold you set.
  3. 3Run the check in a pre-commit hook and in CI so an agent branch fails before a human reads the pull request.
  4. 4Calibrate thresholds from your own commit history, not from a generic default, and raise them only through review.

Why an agent's lockfile diff needs a budget

A coding agent asked to "fix the build" or "add caching" can touch the lockfile for reasons you never approved. It may bump a transitive package to resolve a conflict, add a helper library, or re-resolve the whole tree when an install runs. The npm documentation notes that package-lock.json is automatically generated for any operation where npm modifies node_modules or package.json, and that it records the exact tree so other installs reproduce it (npm docs). Every changed line in that file is a dependency decision, and most reviewers read it as noise.

That noise hides risk. Research on AI-generated code describes hallucinated and unverified dependencies as a common failure mode (Endor Labs). More directly, the Cloud Security Alliance documents the PromptMink campaign, in which packages were crafted to be selected by AI coding agents, and a single malicious transitive dependency entered a project through an agent co-authored commit (CSA Labs). An agent will not read GitHub stars or recall your team's incident history. A numeric cap does not judge a package, but it forces any large change in the dependency graph to arrive as an explicit, separate decision.

The review standard from Google's engineering practices puts this in plain terms: reviewers should approve a change once it definitely improves code health, and they should not spend review time on noise. Checking in changes that worsen the system is not justified outside emergencies (Google review standard). A lockfile budget moves the trivial noise to automation, so the human review covers design, not 400 re-sorted lines.

The two numbers, and how to count them

Count transitive entries by comparing the set of package paths or names in the old and new lockfiles. Count lines changed with git diff --numstat, which reports added and deleted lines per file. Sum the two for a total line count, and treat it as separate from the entry count so a pure reordering does not look like a dependency change.

The file format decides where to look. In package-lock.json, packages maps package locations to objects, and the root project sits under a key of "" (npm docs). pnpm uses importers for direct dependencies and a packages or snapshots section for resolved entries. Poetry keeps its own structure. Write one extractor per ecosystem, each returning the same shape: a set of entry keys and the raw text for line counting.

A useful distinction: count direct dependencies from the manifest separately, then apply a much tighter budget to them than to transitive entries. Adding one direct dependency is a deliberate act; adding 30 transitive ones is a side effect. When a single npm install on a small project produces dozens or hundreds of transitive packages, the gap between direct and transitive is where surprises live (pkgpulse dependency tree analysis).

EcosystemEntry source in the lockfileDirect sourceLines counted from
npmkeys of packages, excluding ""dependencies in package.jsongit diff --numstat on the lockfile
pnpmkeys of snapshots or packagesimporters sectionsame
Poetryentries in [package] blocks[tool.poetry.dependencies]same

Choosing limits that match your project

Do not start from a borrowed number. Measure. Take the last 100 or so merged commits that touched the lockfile and compute the entry delta and line delta for each. Plot or sort them, then pick a threshold at roughly the 90th percentile of routine changes. That is your default human-scale budget.

A worked example. Suppose your history shows a median of 2 new transitive entries and 40 changed lines, and the 90th percentile is 6 entries and 120 lines. You set a default budget of 8 entries and 150 lines. Now an agent branch arrives with 14 new entries and 610 changed lines. The hook fails it with a message listing both numbers and the resolved package names. The agent, or the human directing it, must either narrow the change or ask for an exemption in the commit message. Either way, the dependency decision is now visible.

Change typeSuggested entry budgetSuggested line budgetRationale
Routine bug fix, no dependency intent00Any lockfile change must be intentional
One added direct dependency10200Realistic transitive fan-out for a normal library
Security patch to an existing package4120Same tree, newer versions
Toolchain or major upgradeunbounded, separate branchunbounded, separate branchReview by hand, never inside a feature branch

Tune per directory or workspace if you use a monorepo. A package that installs a large test framework should carry a bigger budget than a small service, and a shared budget across the repo will either block the toolchain or wave through service changes.

These budgets pair well with enforcing what the lockfile is for. Google's reviewer guide asks whether a change is more complex than it should be, and whether it pulls in speculative functionality (what to look for in a review). A lockfile change that no manifest line justifies is exactly that kind of speculative addition.

Writing the check

The script needs four steps: find the base commit, extract the old and new entry sets, count changed lines, compare, exit. Here is a compact Python version for npm. It assumes CI checks out the branch and that BASE_REF points at the target branch.

import json, os, subprocess, sys

BASE = os.environ.get("BASE_REF", "origin/main")
LOCK = "package-lock.json"
MAX_ENTRIES = int(os.environ.get("MAX_LOCK_ENTRIES", "8"))
MAX_LINES = int(os.environ.get("MAX_LOCK_LINES", "150"))

def git(*args):
    return subprocess.run(["git", *args], capture_output=True, text=True)

def entries(text):
    try:
        data = json.loads(text or "{}")
    except json.JSONDecodeError:
        return None
    packages = data.get("packages") or data.get("dependencies") or {}
    return {k for k in packages if k != ""}

old = git("show", f"{BASE}:{LOCK}").stdout
new_path = LOCK
new = open(new_path).read() if os.path.exists(new_path) else ""

old_set, new_set = entries(old), entries(new)
if old_set is None or new_set is None:
    print(f"lockfile-budget: could not parse {LOCK}; failing closed")
    sys.exit(1)

added = new_set - old_set
removed = old_set - new_set
entry_delta = len(added) + len(removed)

numstat = git("diff", "--numstat", BASE, "--", LOCK).stdout.split()
line_delta = (int(numstat[0]) + int(numstat[1])) if numstat else 0

print(f"lockfile-budget: {entry_delta} entries (limit {MAX_ENTRIES}), "
      f"{line_delta} lines (limit {MAX_LINES})")
for name in sorted(added)[:20]:
    print(f"  + {name}")

if entry_delta > MAX_ENTRIES or line_delta > MAX_LINES:
    print("lockfile-budget: FAIL. Split this change or request an exemption.")
    sys.exit(1)

Two design choices matter. First, fail closed on parse errors: a lockfile the script cannot read is not a small diff. Second, print the added entries, not just the counts, so the person who reads the failure can see what the agent pulled in without opening the diff. The excerpt from the npm ls walkthrough shows how quickly a dependency tree grows and how hard it is to read by eye (dev.to on npm ls).

For a real install check in CI, npm ls --all lists the full transitive tree and returns a non-zero exit code when node_modules does not match package.json, which is a useful companion to the budget check (Stack Overflow on npm ls). Use npm ci in CI so installs reproduce the lockfile exactly rather than rewriting it, since a plain npm install can modify the lockfile (Stack Overflow on lockfile rewrites).

Wiring it into pre-commit and CI

The pre-commit framework runs hooks on every commit and handles installing and running tools written in any language (pre-commit.com). A local hook keeps the script in the repo, so an agent that commits locally hits the same check a human does.

repos:
  - repo: local
    hooks:
      - id: lockfile-budget
        name: lockfile diff budget
        entry: python tools/lockfile_budget.py
        language: system
        files: ^package-lock\.json$
        pass_filenames: false

Install it with pre-commit install, then run pre-commit run --all-files once to seed the environment (pre-commit.com). Be aware of a known footgun: a hook that regenerates a lockfile can produce a different result than the same command run by hand, which makes local and CI results disagree (Poetry issue 4978). Your budget script should only read the diff, never rewrite the lockfile, which sidesteps that class of problem entirely.

In CI, run the same script against the merge base. Set BASE_REF to the target branch and require the job. Because a pre-commit hook can be skipped with --no-verify, the CI job is the enforcement point; the hook is the fast feedback loop. Scan at commit time also catches dependency risk before it enters local history, which is the pattern Sonar's pre-commit blueprint describes (Sonar).

The budget check is not a vulnerability scanner. It is a gate on the size of change, and it works best next to a scanner that reads licenses and advisories. A curated list of npm security practices covers pinning, scanning, and deterministic resolution, and it complements the numeric gate (npm security best practices).

Handling exemptions without eroding the budget

Every budget needs an escape hatch, or the first legitimate large upgrade will get the check deleted. Use an explicit marker in the commit message, parsed by the same script:

feat: upgrade test toolchain

lockfile-budget-exempt: toolchain-only; reviewed in DEP-412

The script reads the commit message and requires both keywords, a reason and a ticket reference. CI can then route exempt branches to a named reviewer or label instead of merging automatically. Track exemption frequency monthly; a rising rate means the budget is too tight or the team is bypassing it, and either signal is worth a conversation.

Keep exemptions out of the diff itself. A comment in the lockfile will be wiped by the next install, and an allowlist file inside the repo invites an agent to edit it. The commit message is durable, attributable and visible in git log.

Getting agents to respect the budget up front

A budget that fails after the work is done wastes a run. Tell the agent the rules before it starts. Put the constraints in the repository instructions the agent reads, and state the two numbers explicitly: do not add transitive dependencies beyond the budget, do not run a re-resolve, and if a package must be added, name it and explain why in the commit body.

This is the same principle as scoping any repository task. Agented work is defined by acceptance criteria and a definition of done, and the agent should make bounded edits and check the actual result (AI Engineer coding agents). A lockfile budget is simply the definition of done for dependency changes. The terminal agent research describes the same pattern from the harness side: explicit phases, lazy tool discovery and context control keep a long-running agent from drifting into unrequested work (arXiv, Building AI Coding Agents for the Terminal).

If your team runs several agents across several ecosystems, a shared skill that encodes the budget rules and the pre-commit config keeps every agent on the same policy. Quanta Skills is the library behind the experts, and you can install a skill into your coding agent so the budget check and its exemption format travel with the repo (https://quantaintelligence.ai/skills). For teams that want the agent to produce the entire budget check as a reviewed deliverable, an expert reads your context and hands back finished work (https://quantaintelligence.ai/agents).

A rollout checklist

Start with measurement, not enforcement. Run the script in report-only mode for two weeks and collect the numbers; you will find that a handful of commits account for most of the diff.

StepActionDone when
1Write the extractor for each lockfile format in the repoScript prints correct entry counts on a known commit
2Collect deltas from the last 100 lockfile changesYou have a sorted list per ecosystem
3Set default budgets at the 90th percentileConfig is committed and documented
4Add the pre-commit hookpre-commit run --all-files passes locally
5Add the CI job with BASE_REFA test branch with a large delta fails
6Define the exemption formatOne exempt commit is recorded in git log
7Add the rule to agent instructionsA new agent session asks before adding a package

Two more operational notes. First, pin your package manager version in CI, because lockfile differences across versions are a common source of spurious diffs and will look like budget violations. Second, review the budget itself quarterly like any other policy. If routine changes now sit at 12 entries, the 8-entry limit is generating noise, not signal.

The larger point is that dependency review is a design question and automated gates are how you protect the reviewer's attention for it. Google's reviewer guide places design first and treats every other check as support for that judgment (review guide). A lockfile budget is a small piece of machinery that keeps the dependency graph inside the bounds you chose, so an agent cannot quietly move it while you are reading the interesting part of the diff.

FAQ

FAQ

What exactly counts as a transitive entry?

Any package record in the lockfile that is not your root project. In npm's package-lock.json these are the keys under packages other than "", each holding a resolved version and integrity hash (npm docs). Count added and removed entries separately, then sum them.

Should the budget cover direct dependencies too?

Yes, but with a separate and tighter rule. Direct dependencies are declared in the manifest and should be reviewed line by line. Treat any new direct dependency as requiring an explicit note in the commit, and reserve the numeric budget for the transitive fan-out that follows it.

What happens when a legitimate upgrade exceeds the limit?

Use the exemption marker in the commit message with a reason and a ticket reference. The CI job routes the branch to a named reviewer instead of merging it silently. Track how often this happens: frequent exemptions mean the threshold is mis-set (Google review standard).

Does the check replace a dependency scanner?

No. The budget limits the size of change; a scanner judges the content for vulnerabilities, malware and license problems. Run both, and prefer npm ci so the installed tree matches the lockfile exactly (npm security best practices).

Can an agent bypass the pre-commit hook?

Yes, with --no-verify. That is why the same script must run in CI against the merge base, with the job required before merge. The hook gives fast local feedback; CI is the enforcement point (pre-commit.com).

Sources

Sources

  1. package-lock.json
  2. The Most Common Security Vulnerabilities in AI-Generated Code
  3. Nation-State npm Malware Targets AI Coding Agents
  4. The Standard of Code Review | eng-practices - Google
  5. npm Dependency Trees: Most Nested Packages 2026
  6. What to look for in a code review | eng-practices - Google
  7. Using npm's ls command for Fun and Insight
  8. How to view the dependency tree of a given npm module?
  9. Why does "npm install" rewrite package-lock.json?
  10. pre-commit
  11. pre-commit hook generate different lockfile than running ` ...
  12. SCA Pre-commit Hook with SonarQube CLI
  13. Collection of npm package manager Security Best Practices
  14. Coding agents - AI Engineer
  15. Building AI Coding Agents for the Terminal
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles