All skills

Agent skill

JavaScript supply chain checks

Tighten dependency installs, lockfiles, and build permissions in JavaScript and TypeScript projects.

Use this skill

Install it into a project with the Skills CLI, or read the files below and adapt them to your own agent.

npx skills add shan8851/agent-skills --skill javascript-supply-chain-hardening
View source on GitHub
SKILL.mdView this file on GitHub
---
name: javascript-supply-chain-hardening
description: Harden JavaScript and TypeScript projects against npm supply-chain risk with package-manager policy, build-script trust policy, lockfile hygiene, pinning, and CI install gates.
---

# JavaScript Supply-Chain Hardening

Use this when you need to reduce supply-chain risk in a JavaScript or TypeScript repository, especially after npm registry/package compromise news, before shipping a public package, or when cleaning up inconsistent package-manager usage.

This is not a silver bullet. It is a pragmatic first line of defence: make installs deterministic, slow down adoption of brand-new package releases, remove package-manager drift, and ensure CI proves the repo still builds.

## What Done Looks Like

A repo is only hardened when the policy is committed and verified:

- One package manager is used consistently, preferably `pnpm` for new hardening work.
- `package.json` declares a `packageManager` version.
- The repo has a committed lockfile for that package manager.
- New package releases are delayed before install where supported, usually with a 3-day age gate.
- CI installs with a frozen lockfile.
- CI runs the repo's real quality gates: `validate`, `check`, or the available lint/typecheck/test/build scripts.
- Package build scripts are denied by default or explicitly approved with a reviewed trust policy.
- Node, pnpm, Docker base images, and CI actions are pinned intentionally instead of floating silently.
- Pin update automation exists, or there is an explicit manual review cadence.
- Published packages use provenance/two-factor release controls where the registry supports them.
- Old lockfiles and stale CI commands are removed unless intentionally kept.

## Quick Audit

Start by inspecting the repo before changing anything:

```bash
git status --short
node --version
corepack --version || true
ls
```

Then check package-manager state:

```bash
node -e "const p=require('./package.json'); console.log({packageManager:p.packageManager, scripts:Object.keys(p.scripts||{}).sort()})"
for f in package-lock.json yarn.lock pnpm-lock.yaml bun.lock bun.lockb; do [ -e "$f" ] && echo "$f"; done
find .github/workflows -maxdepth 2 -type f 2>/dev/null || true
find . -maxdepth 3 \( -iname 'Dockerfile*' -o -name 'docker-compose*.yml' -o -name 'compose*.yml' \) 2>/dev/null || true
```

Classify the repo:

- App, library, CLI, monorepo, or documentation site.
- Private/internal or public/published.
- Current package manager: npm, yarn, pnpm, bun, or mixed.
- Existing CI provider and install commands.
- Dockerfiles or deployment workflows that install dependencies independently.
- Existing quality scripts in `package.json`.
- Packages that need install-time build scripts, such as native modules, bundlers, ORMs, or browser automation packages.

Do not blindly migrate a repo with unclear constraints. If a tool choice is intentional, preserve it and harden around it.

## pnpm Hardening Checklist

For a typical repo, pnpm is a good default because it supports deterministic installs, strict dependency layout, and delayed adoption of new releases.

1. Enable Corepack if needed:

   ```bash
   corepack enable || true
   ```

2. Add or update `packageManager` in `package.json`:

   ```json
   {
     "packageManager": "pnpm@10.23.0"
   }
   ```

   Use the current stable pnpm version for the project. Keep CI and docs on the same major version.

3. Add `.npmrc` with a release-age gate:

   ```ini
   minimum-release-age=4320
   ```

   `4320` minutes is 3 days. This slows down adoption of newly published dependency versions, which gives the ecosystem time to detect obvious malicious releases.

4. Add an install-script trust policy. In pnpm 10, approve only the packages whose install/build scripts are expected and reviewed:

   ```bash
   pnpm approve-builds
   ```

   This writes the approved package list into project config, commonly `onlyBuiltDependencies` and/or ignored build-script entries. Review the generated config like source code. Keep the list short; examples that may legitimately need approval include `esbuild`, `sharp`, `@prisma/client`, `playwright`, or other native/tooling packages actually used by the repo.

   For newer pnpm versions, use the current equivalent trust-policy setting such as `allowBuilds`. Do not blindly copy an old config shape across pnpm majors; check the installed pnpm docs/help and keep the package-manager version pinned.

5. Install and verify the lockfile:

   ```bash
   pnpm install
   pnpm install --frozen-lockfile
   ```

6. Remove obsolete lockfiles only after pnpm install succeeds:

   ```bash
   rm -f package-lock.json yarn.lock bun.lock bun.lockb
   ```

7. Update scripts, docs, Dockerfiles, and CI that still call `npm`, `npx`, `yarn`, or `bun` unless the mixed setup is deliberate.

8. Run the repo's real gates. Inspect scripts first, then run only the scripts that exist:

   ```bash
   node -e "const p=require('./package.json'); console.log(Object.keys(p.scripts||{}).sort().join('\n'))"
   ```

   Prefer one aggregate gate if present:

   ```bash
   pnpm run validate
   # or
   pnpm run check
   ```

   If there is no aggregate script, run the available individual gates, for example:

   ```bash
   pnpm run lint
   pnpm run typecheck
   pnpm test
   pnpm run build
   ```

## GitHub Actions Pattern

A minimal Node CI job should install pnpm before enabling pnpm cache in `actions/setup-node`:

```yaml
name: CI

on:
  pull_request:
  push:
    branches: [main]

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 10.23.0
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm run validate
```

If the repo has no aggregate `validate` script, run the scripts that actually exist:

```yaml
      - run: pnpm run lint
      - run: pnpm run typecheck
      - run: pnpm test
      - run: pnpm run build
```

For apps that need environment variables at build time, use dummy values with the correct shape in CI. Never put production secrets in workflow files.

## Pinning Policy for CI, Docker, and Workflows

Pin the toolchain wherever dependencies are installed. A repo is not hardened if local installs use pnpm but Docker or CI still floats versions.

Check for these files and update them together:

```bash
find .github/workflows -maxdepth 2 -type f 2>/dev/null || true
find . -maxdepth 3 \( -iname 'Dockerfile*' -o -name 'docker-compose*.yml' -o -name 'compose*.yml' \) 2>/dev/null || true
```

Good defaults:

- Pin `packageManager` in `package.json`, e.g. `pnpm@10.23.0`.
- Pin `pnpm/action-setup` to the same pnpm major/minor used locally.
- Pin Node in CI and deployment images, e.g. `node-version: 22` or `FROM node:22.11.0-slim` rather than `node:latest`.
- For higher-security repos, pin GitHub Actions and Docker base images by digest/SHA, then use Dependabot or Renovate to keep those pins updated.
- Make Docker builds use the same lockfile discipline:

```Dockerfile
FROM node:22.11.0-slim
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml .npmrc ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
```

Avoid floating tags like `node:latest`, unpinned package-manager installs like `npm i -g pnpm`, and CI snippets that silently upgrade the package manager.

Pinning creates maintenance work. Add automation or write down the manual cadence:

```yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly
  - package-ecosystem: docker
    directory: /
    schedule:
      interval: weekly
```

For stricter repos, prefer digest/SHA pins plus Dependabot/Renovate updates over mutable tags that never get reviewed.

## Docker Build Hygiene

If the repo ships a container, also check the Docker context and install behaviour:

- Add a `.dockerignore` so secrets, local caches, `.git`, and `node_modules` are not copied into the build context.
- Copy dependency manifests before source files so Docker layer caching does not hide install drift.
- Use `pnpm install --frozen-lockfile` or `pnpm fetch` + offline install patterns; do not use plain `pnpm install` in production Docker builds.
- Do not run package-manager installs from remote shell scripts inside Dockerfiles.
- If using multi-stage builds, ensure every stage that installs dependencies uses the same pinned package-manager policy.

A minimal `.dockerignore` starting point:

```gitignore
.git
node_modules
.pnpm-store
.env
.env.*
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
.DS_Store
```


## Lockfile Review

A lockfile change is code. Review it instead of treating it as generated noise.

Check for:

- Unexpected new direct dependencies.
- Large dependency-tree changes from a tiny package.json edit.
- New install scripts in transitive packages.
- Git, tarball, file, or non-registry dependency sources.
- Brand-new package versions when no release-age gate was active.
- New or changed approved-build/trust-policy entries.
- Package name confusion: typosquats, abandoned packages, or surprising maintainers.

Useful commands:

```bash
git diff -- package.json pnpm-lock.yaml .npmrc pnpm-workspace.yaml .github/workflows Dockerfile 'Dockerfile*'
pnpm audit --audit-level moderate || true
pnpm outdated || true
```

Treat `audit` output as triage input, not gospel. The aim is to understand risk, not blindly churn dependencies.

## Existing npm/yarn/bun Projects

Prefer improving the repo in-place over doing a performative migration.

- If npm is kept, use `npm ci` in CI and commit `package-lock.json`.
- If yarn is kept, use immutable installs and a committed lockfile.
- If bun is kept, make sure CI uses the same bun version and lockfile strategy.
- If migrating, remove old lockfiles and update all install docs/scripts in the same change.

Mixed package managers are a smell. Keep them only when there is a documented reason.

## Published Packages and CLIs

For public packages, hardening local development is useful, but it does not automatically protect downstream users.

Before release:

- Confirm package contents with the package manager's dry-run/pack command:

  ```bash
  pnpm pack --dry-run
  ```

- Verify CI is green from a clean install.
- Check that generated files, bins, exports, and README instructions still match reality.
- Use npm provenance for CI-based publishing where supported:

  ```bash
  pnpm publish --provenance
  ```

- Require 2FA for registry accounts and avoid long-lived publish tokens where trusted publishing is available.
- Publish only after package-manager and lockfile changes are understood.

A CI-only hardening change does not always require an immediate release. If package contents, runtime dependencies, or user-facing install instructions changed, batch it into the next patch release.

## Common Pitfalls

1. **Thinking an age gate fixes the existing lockfile.** It does not. It only affects future resolution/install behaviour. Still review suspicious lockfile changes.

2. **Using the wrong config key.** In `.npmrc`, use pnpm's kebab-case config: `minimum-release-age=4320`, not camelCase.

3. **Installing pnpm too late in GitHub Actions.** `actions/setup-node` with `cache: pnpm` expects pnpm to exist. Run `pnpm/action-setup` first.

4. **Leaving stale CI commands behind.** A migrated repo should not still run `npm ci`, `yarn install`, or `bun install` unless that is intentional.

5. **Approving every install script.** Trust policy is only useful if the allowlist is small and reviewed. If everything is approved, you have mostly recreated the default risk.

6. **Floating Docker and CI toolchains.** `node:latest`, unpinned pnpm installs, and mutable action references can bypass the repo policy. Pin versions; use digest/SHA pins for stricter environments.

7. **Pinning without updates.** Frozen versions reduce surprise but can rot. Pair pins with Dependabot/Renovate or an explicit review cadence.

8. **Copying secrets into Docker images.** A missing `.dockerignore` can leak `.env`, local caches, or repo metadata into image layers/build contexts.

9. **Leaking secrets into CI.** Use dummy build-time values with the right shape. Do not paste real tokens, API keys, or database URLs into workflow files.

10. **Breaking framework builds by blocking dependency build scripts.** Some frameworks and ORMs need explicit generation steps. For example, Prisma apps often need `prisma generate` before `next build`.

11. **Treating formatting as security.** If a legacy repo fails only on old formatting noise, keep install/build security green first and plan formatting cleanup separately.

## Verification Checklist

- [ ] `package.json` declares the intended package manager.
- [ ] Exactly one intended lockfile is committed.
- [ ] `.npmrc` or equivalent install policy is committed where supported.
- [ ] CI uses frozen/immutable installs.
- [ ] CI runs real repo gates.
- [ ] Install-script trust policy is present and reviewed where pnpm supports it.
- [ ] Dockerfiles and deployment workflows use the same package-manager and lockfile policy.
- [ ] Node, pnpm, CI actions, and Docker base images are pinned intentionally.
- [ ] Pin update automation or a manual review cadence exists.
- [ ] `.dockerignore` prevents secrets/caches from entering container build contexts where Docker is used.
- [ ] Published packages use provenance/2FA/trusted publishing where supported.
- [ ] Docs and scripts no longer reference obsolete package-manager commands.
- [ ] Lockfile changes have been reviewed.
- [ ] Local working tree is clean after the hardening change.