tarek.kharsa

home / lessons / pi · lesson 5

SecurityPi · lesson 5 of 82 min read

Treat the lockfile as code

Three packages may run install scripts. Every other dependency change has to ask.

Problem

Supply-chain attacks arrive through lockfile changes nobody reads and install scripts that run on every machine.

Idea

Pin exact versions, disable install scripts except a short allowlist with reasons, and block lockfile commits unless they're on purpose.

Supply-chain attacks rarely arrive through code anyone reads. They arrive through a dependency bump in a lockfile nobody opens, or a postinstall script that runs on every developer's machine and every CI runner. This repo's policy is one line:

Treat npm dep and lockfile changes as reviewed code.

The engineering is in how each attack path gets closed.

Every path a dependency can take, and its gate

A silent upgrade

Exact versions only. Every direct dependency is pinned (no ^ or ~), and a script fails the check if one isn't.

Code at install time

No install scripts by default. Installs run with --ignore-scripts, locally and in CI.

A real need for a script

A three-entry allowlist for the install users get, each with a reason, like esbuild's "postinstall selects and verifies the platform-specific esbuild binary."

A lockfile change in passing

A pre-commit gate blocks any commit that stages the lockfile unless you confirm it with an environment variable.

The lockfile gate, in detail

package-lock.json is staged.

Review lockfile changes before committing:
  - confirm every new/updated package is intentional
  - confirm npm age gates were active for resolution
  - review any new lifecycle scripts in the dependency tree

Detected package version changes:
  - changed <package> <old> -> <new>

If this lockfile change is intentional, commit with:
  …_ALLOW_LOCKFILE_CHANGE=1 git commit ...
  1. Diff, don't guessIt compares the lockfile at HEAD with the staged one, entry by entry, and lists what was added, removed or changed.
  2. Quiet when nothing external changedChanges that only touch the repo's own workspace packages pass through, so the gate doesn't train people to ignore it.
  3. A checklist, not just a blockAge gates (refusing versions published too recently to have been vetted) and new lifecycle scripts are what most attacks rely on, so the hook asks about exactly those.

Agents can't slip a dependency change into an unrelated commit, and neither can humans.

Lists that can't rot

  • If an allowlisted install-script package disappears from the tree, the check fails and asks you to remove its entry.
  • A nightly npm audit fails on moderate advisories or worse. Accepting one requires naming the package and explaining why it doesn't apply, and the script warns about accepted entries that no longer match.
  • The same job runs npm audit signatures to verify registry signatures.
  • The curl installer pins every transitive dependency for users; plain npm install -g doesn't, and the README says so.

One dependency has a rule of its own: before updating undici, the HTTP client underneath Node's fetch, read its changelog and check whether anything affects the app.

Steal this

Make dependency changes loud. Pin exact versions, turn off install scripts and allowlist the few that need them with a reason, block lockfile commits unless someone says "yes, on purpose," and make every exception list fail when it goes stale.

Use it in your code

A prompt for your coding agent

Run it from the root of your repo. It checks whether the idea fits before it changes anything.

Harden how this project handles dependency changes.

The idea: treat dependency and lockfile changes as reviewed code. Pin exact versions, install without lifecycle scripts, allow only a short documented list of packages to run install scripts, and block lockfile commits unless someone confirms the change is intentional.

1. Check how dependencies are declared (ranges or exact versions), how CI installs them, and whether install scripts run.
2. Find which dependencies actually have install scripts, and for each say whether it needs them and why.
3. Propose and implement: a check that direct dependencies use exact versions; installs with scripts disabled (for npm, --ignore-scripts) plus an allowlist with a reason per package; and a pre-commit check that stops lockfile commits unless an environment variable confirms them, printing what changed.
4. If there's no scheduled audit, add one that fails on moderate or worse advisories and needs a written reason to accept one.

Before changing CI, show me any dependency that breaks without its install script.

Read the source

Where this comes from

Pi (earendil-works/pi), the minimal, extensible agent harness. The files behind this lesson: